feat(den): document den-api with OpenAPI (#1371)

Generate an OpenAPI spec and Swagger UI from den-api's existing Hono and zod validators so the API stays self-describing. Add route metadata, typed responses, and hide API key creation endpoints from production docs.

Co-authored-by: src-opn <src-opn@users.noreply.github.com>
This commit is contained in:
Source Open
2026-04-06 13:48:10 -07:00
committed by GitHub
parent 4f1905882b
commit d9e5a33e62
22 changed files with 1790 additions and 81 deletions

View File

@@ -11,18 +11,25 @@
},
"dependencies": {
"@better-auth/api-key": "^1.5.6",
"@openwork-ee/den-db": "workspace:*",
"@openwork-ee/utils": "workspace:*",
"@daytonaio/sdk": "^0.150.0",
"@hono/node-server": "^1.13.8",
"@hono/zod-validator": "^0.7.6",
"better-call": "^1.3.2",
"@hono/standard-validator": "^0.2.2",
"@hono/swagger-ui": "^0.6.1",
"@openwork-ee/den-db": "workspace:*",
"@openwork-ee/utils": "workspace:*",
"@standard-community/standard-json": "^0.3.5",
"@standard-community/standard-openapi": "^0.2.9",
"@standard-schema/spec": "^1.1.0",
"better-auth": "^1.5.6",
"better-call": "^1.3.2",
"dotenv": "^16.4.5",
"hono": "^4.7.2",
"hono-openapi": "^1.3.0",
"openapi-types": "^12.1.3",
"zod": "^4.3.6"
},
"devDependencies": {
"@types/json-schema": "^7.0.15",
"@types/node": "^20.11.30",
"tsx": "^4.15.7",
"typescript": "^5.5.4"

View File

@@ -1,12 +1,16 @@
import "./load-env.js"
import { createDenTypeId } from "@openwork-ee/utils/typeid"
import { swaggerUI } from "@hono/swagger-ui"
import { cors } from "hono/cors"
import { Hono } from "hono"
import { logger } from "hono/logger"
import type { RequestIdVariables } from "hono/request-id"
import { requestId } from "hono/request-id"
import { describeRoute, openAPIRouteHandler, resolver } from "hono-openapi"
import { z } from "zod"
import { env } from "./env.js"
import type { MemberTeamsContext, OrganizationContextVariables, UserOrganizationsContext } from "./middleware/index.js"
import { buildOperationId, emptyResponse, htmlResponse, jsonResponse } from "./openapi.js"
import { registerAdminRoutes } from "./routes/admin/index.js"
import { registerAuthRoutes } from "./routes/auth/index.js"
import { registerMeRoutes } from "./routes/me/index.js"
@@ -17,6 +21,21 @@ import { sessionMiddleware } from "./session.js"
type AppVariables = RequestIdVariables & AuthContextVariables & Partial<UserOrganizationsContext> & Partial<OrganizationContextVariables> & Partial<MemberTeamsContext>
const healthResponseSchema = z.object({
ok: z.literal(true),
service: z.literal("den-api"),
}).meta({ ref: "DenApiHealthResponse" })
const openApiDocumentSchema = z.object({
openapi: z.string(),
info: z.object({
title: z.string(),
version: z.string(),
}).passthrough(),
paths: z.record(z.string(), z.unknown()),
components: z.object({}).passthrough().optional(),
}).passthrough().meta({ ref: "OpenApiDocument" })
const app = new Hono<{ Variables: AppVariables }>()
app.use("*", logger())
@@ -45,13 +64,42 @@ if (env.corsOrigins.length > 0) {
app.use("*", sessionMiddleware)
app.get("/", (c) => {
return c.redirect("https://openworklabs.com", 302)
})
app.get(
"/",
describeRoute({
tags: ["System"],
summary: "Redirect API root",
description: "Redirects the API root to the OpenWork marketing site instead of serving API content.",
responses: {
302: emptyResponse("Redirect to the OpenWork marketing site."),
},
}),
(c) => {
return c.redirect("https://openworklabs.com", 302)
},
)
app.get("/health", (c) => {
return c.json({ ok: true, service: "den-api" })
})
app.get(
"/health",
describeRoute({
tags: ["System"],
summary: "Check den-api health",
description: "Returns a lightweight health payload for den-api.",
responses: {
200: {
description: "den-api is reachable",
content: {
"application/json": {
schema: resolver(healthResponseSchema),
},
},
},
},
}),
(c) => {
return c.json({ ok: true, service: "den-api" })
},
)
registerAdminRoutes(app)
registerAuthRoutes(app)
@@ -59,6 +107,89 @@ registerMeRoutes(app)
registerOrgRoutes(app)
registerWorkerRoutes(app)
app.get(
"/openapi.json",
describeRoute({
tags: ["Documentation"],
summary: "Get OpenAPI document",
description: "Returns the machine-readable OpenAPI 3.1 document for the Den API so humans and tools can inspect the API surface.",
responses: {
200: jsonResponse("OpenAPI document returned successfully.", openApiDocumentSchema),
},
}),
openAPIRouteHandler(app, {
documentation: {
openapi: "3.1.0",
info: {
title: "Den API",
version: "dev",
description: "OpenAPI spec for the Den control plane API.",
},
tags: [
{ name: "System", description: "Service health and operational routes." },
{ name: "Documentation", description: "OpenAPI document and Swagger UI routes." },
{ name: "Organizations", description: "Organization-scoped Den API routes." },
{ name: "Organization Invitations", description: "Organization invitation creation, preview, acceptance, and cancellation routes." },
{ name: "Organization API Keys", description: "Organization API key management routes." },
{ name: "Organization Members", description: "Organization member management routes." },
{ name: "Organization Roles", description: "Organization custom role management routes." },
{ name: "Organization Teams", description: "Organization team management routes." },
{ name: "Organization Templates", description: "Organization shared template routes." },
{ name: "Organization LLM Providers", description: "Organization LLM provider catalog, configuration, and access routes." },
{ name: "Organization Skills", description: "Organization skill authoring and sharing routes." },
{ name: "Organization Skill Hubs", description: "Organization skill hub management and access routes." },
{ name: "Workers", description: "Worker lifecycle, billing, and runtime routes." },
{ name: "Worker Billing", description: "Worker subscription and billing status routes." },
{ name: "Worker Runtime", description: "Worker runtime inspection and upgrade routes." },
{ name: "Worker Activity", description: "Worker heartbeat and activity reporting routes." },
{ name: "Authentication", description: "Authentication and desktop sign-in handoff routes." },
{ name: "Admin", description: "Administrative reporting routes." },
{ name: "Users", description: "Current user and membership routes." },
],
components: {
securitySchemes: {
bearerAuth: {
type: "http",
scheme: "bearer",
bearerFormat: "session-token",
},
denApiKey: {
type: "apiKey",
in: "header",
name: "x-api-key",
},
},
},
},
includeEmptyPaths: true,
exclude: ["/docs", "/openapi.json"],
excludeMethods: ["OPTIONS"],
defaultOptions: {
ALL: {
operationId: (route) => buildOperationId(route.method, route.path),
},
},
}),
)
app.get(
"/docs",
describeRoute({
tags: ["Documentation"],
summary: "Serve Swagger UI",
description: "Serves Swagger UI so developers can browse and try the Den API from a browser.",
responses: {
200: htmlResponse("Swagger UI page returned successfully."),
},
}),
swaggerUI({
url: "/openapi.json",
persistAuthorization: true,
displayOperationId: true,
defaultModelsExpandDepth: 1,
}),
)
app.notFound((c) => {
return c.json({ error: "not_found" }, 404)
})

View File

@@ -1,11 +1,11 @@
import { zValidator } from "@hono/zod-validator"
import { validator as zValidator } from "hono-openapi"
import type { ZodSchema } from "zod"
function invalidRequestResponse(result: { success: false; error: { issues: unknown } }, c: { json: (body: unknown, status?: number) => Response }) {
function invalidRequestResponse(result: { success: false; error: unknown }, c: { json: (body: unknown, status?: number) => Response }) {
return c.json(
{
error: "invalid_request",
details: result.error.issues,
details: result.error,
},
400,
)

View File

@@ -0,0 +1,102 @@
import { resolver } from "hono-openapi"
import { z } from "zod"
function toPascalCase(value: string) {
return value
.replace(/[^a-zA-Z0-9]+/g, " ")
.trim()
.split(/\s+/)
.filter(Boolean)
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
.join("")
}
export function buildOperationId(method: string, path: string) {
const parts = path
.split("/")
.filter(Boolean)
.filter((part) => part !== "v1")
.map((part) => {
if (part.startsWith(":")) {
return `by-${part.slice(1)}`
}
if (part === "*") {
return "wildcard"
}
return part
})
return [method.toLowerCase(), ...parts]
.map(toPascalCase)
.join("")
.replace(/^[A-Z]/, (char) => char.toLowerCase())
}
const validationIssueSchema = z.object({
message: z.string(),
path: z.array(z.union([z.string(), z.number()])).optional(),
}).passthrough()
export const invalidRequestSchema = z.object({
error: z.literal("invalid_request"),
details: z.array(validationIssueSchema),
}).meta({ ref: "InvalidRequestError" })
export const unauthorizedSchema = z.object({
error: z.literal("unauthorized"),
}).meta({ ref: "UnauthorizedError" })
export const forbiddenSchema = z.object({
error: z.literal("forbidden"),
message: z.string().optional(),
}).meta({ ref: "ForbiddenError" })
export const notFoundSchema = z.object({
error: z.string(),
message: z.string().optional(),
}).meta({ ref: "NotFoundError" })
export const successSchema = z.object({
success: z.literal(true),
}).meta({ ref: "SuccessResponse" })
export const emptyObjectSchema = z.object({}).passthrough().meta({ ref: "OpaqueObject" })
export function jsonResponse(description: string, schema: z.ZodTypeAny) {
return {
description,
content: {
"application/json": {
schema: resolver(schema),
},
},
}
}
export function htmlResponse(description: string) {
return {
description,
content: {
"text/html": {
schema: resolver(z.string()),
},
},
}
}
export function textResponse(description: string) {
return {
description,
content: {
"text/plain": {
schema: resolver(z.string()),
},
},
}
}
export function emptyResponse(description: string) {
return { description }
}

View File

@@ -1,10 +1,12 @@
import { asc, desc, eq, isNotNull, sql } from "@openwork-ee/den-db/drizzle"
import { AuthAccountTable, AuthSessionTable, AuthUserTable, WorkerTable, AdminAllowlistTable } from "@openwork-ee/den-db/schema"
import type { Hono } from "hono"
import { describeRoute } from "hono-openapi"
import { z } from "zod"
import { getCloudWorkerAdminBillingStatus } from "../../billing/polar.js"
import { db } from "../../db.js"
import { queryValidator, requireAdminMiddleware } from "../../middleware/index.js"
import { invalidRequestSchema, jsonResponse, unauthorizedSchema } from "../../openapi.js"
import type { AuthContextVariables } from "../../session.js"
type UserId = typeof AuthUserTable.$inferSelect.id
@@ -13,6 +15,18 @@ const overviewQuerySchema = z.object({
includeBilling: z.string().optional(),
})
const adminOverviewResponseSchema = z.object({
viewer: z.object({
id: z.string(),
email: z.string(),
name: z.string().nullable(),
}),
admins: z.array(z.object({}).passthrough()),
summary: z.object({}).passthrough(),
users: z.array(z.object({}).passthrough()),
generatedAt: z.string().datetime(),
}).meta({ ref: "AdminOverviewResponse" })
function normalizeEmail(value: string | null | undefined) {
return value?.trim().toLowerCase() ?? ""
}
@@ -84,7 +98,21 @@ async function mapWithConcurrency<T, R>(items: T[], limit: number, mapper: (item
}
export function registerAdminRoutes<T extends { Variables: AuthContextVariables }>(app: Hono<T>) {
app.get("/v1/admin/overview", requireAdminMiddleware, queryValidator(overviewQuerySchema), async (c) => {
app.get(
"/v1/admin/overview",
describeRoute({
tags: ["Admin"],
summary: "Get admin overview",
description: "Returns a high-level administrative overview of users, sessions, workers, admins, and optional billing data for Den operations.",
responses: {
200: jsonResponse("Administrative overview returned successfully.", adminOverviewResponseSchema),
400: jsonResponse("The admin overview query parameters were invalid.", invalidRequestSchema),
401: jsonResponse("The caller must be an authenticated admin.", unauthorizedSchema),
},
}),
requireAdminMiddleware,
queryValidator(overviewQuerySchema),
async (c) => {
const user = c.get("user")
const query = c.req.valid("query")
const includeBilling = parseBooleanQuery(query.includeBilling)
@@ -289,5 +317,6 @@ export function registerAdminRoutes<T extends { Variables: AuthContextVariables
users: userRows,
generatedAt: new Date().toISOString(),
})
})
},
)
}

View File

@@ -3,9 +3,11 @@ import { and, eq, gt, isNull } from "@openwork-ee/den-db/drizzle"
import { AuthSessionTable, AuthUserTable, DesktopHandoffGrantTable } from "@openwork-ee/den-db/schema"
import { normalizeDenTypeId } from "@openwork-ee/utils/typeid"
import type { Hono } from "hono"
import { describeRoute } from "hono-openapi"
import { z } from "zod"
import { jsonValidator, requireUserMiddleware } from "../../middleware/index.js"
import { db } from "../../db.js"
import { invalidRequestSchema, jsonResponse, notFoundSchema, unauthorizedSchema } from "../../openapi.js"
import type { AuthContextVariables } from "../../session.js"
const createGrantSchema = z.object({
@@ -17,6 +19,26 @@ const exchangeGrantSchema = z.object({
grant: z.string().trim().min(12).max(128),
})
const desktopHandoffGrantResponseSchema = z.object({
grant: z.string(),
expiresAt: z.string().datetime(),
openworkUrl: z.string().url(),
}).meta({ ref: "DesktopHandoffGrantResponse" })
const desktopHandoffExchangeResponseSchema = z.object({
token: z.string(),
user: z.object({
id: z.string(),
email: z.string().email(),
name: z.string().nullable(),
}),
}).meta({ ref: "DesktopHandoffExchangeResponse" })
const grantNotFoundSchema = z.object({
error: z.literal("grant_not_found"),
message: z.string(),
}).meta({ ref: "DesktopHandoffGrantNotFoundError" })
function readSingleHeader(value: string | null) {
const first = value?.split(",")[0]?.trim() ?? ""
return first || null
@@ -90,7 +112,21 @@ function buildOpenworkDeepLink(input: {
}
export function registerDesktopAuthRoutes<T extends { Variables: AuthContextVariables }>(app: Hono<T>) {
app.post("/v1/auth/desktop-handoff", requireUserMiddleware, jsonValidator(createGrantSchema), async (c) => {
app.post(
"/v1/auth/desktop-handoff",
describeRoute({
tags: ["Authentication"],
summary: "Create desktop handoff grant",
description: "Creates a short-lived desktop handoff grant and deep link so a signed-in web user can continue the same account in the OpenWork desktop app.",
responses: {
200: jsonResponse("Desktop handoff grant created successfully.", desktopHandoffGrantResponseSchema),
400: jsonResponse("The handoff request body was invalid.", invalidRequestSchema),
401: jsonResponse("The caller must be signed in to create a desktop handoff grant.", unauthorizedSchema),
},
}),
requireUserMiddleware,
jsonValidator(createGrantSchema),
async (c) => {
const user = c.get("user")
const session = c.get("session")
if (!user?.id || !session?.token) {
@@ -120,9 +156,23 @@ export function registerDesktopAuthRoutes<T extends { Variables: AuthContextVari
denBaseUrl,
}),
})
})
},
)
app.post("/v1/auth/desktop-handoff/exchange", jsonValidator(exchangeGrantSchema), async (c) => {
app.post(
"/v1/auth/desktop-handoff/exchange",
describeRoute({
tags: ["Authentication"],
summary: "Exchange desktop handoff grant",
description: "Exchanges a one-time desktop handoff grant for the user's session token and basic profile so the desktop app can sign the user in.",
responses: {
200: jsonResponse("Desktop handoff grant exchanged successfully.", desktopHandoffExchangeResponseSchema),
400: jsonResponse("The handoff exchange request body was invalid.", invalidRequestSchema),
404: jsonResponse("The handoff grant was missing, expired, or already used.", grantNotFoundSchema),
},
}),
jsonValidator(exchangeGrantSchema),
async (c) => {
const input = c.req.valid("json")
const now = new Date()
@@ -171,5 +221,6 @@ export function registerDesktopAuthRoutes<T extends { Variables: AuthContextVari
name: row.user.name,
},
})
})
},
)
}

View File

@@ -1,9 +1,26 @@
import type { Hono } from "hono"
import { describeRoute } from "hono-openapi"
import { auth } from "../../auth.js"
import { emptyResponse } from "../../openapi.js"
import type { AuthContextVariables } from "../../session.js"
import { registerDesktopAuthRoutes } from "./desktop-handoff.js"
export function registerAuthRoutes<T extends { Variables: AuthContextVariables }>(app: Hono<T>) {
app.on(["GET", "POST"], "/api/auth/*", (c) => auth.handler(c.req.raw))
app.on(
["GET", "POST"],
"/api/auth/*",
describeRoute({
tags: ["Authentication"],
summary: "Handle Better Auth flow",
description: "Proxies Better Auth sign-in, sign-out, session, and verification flows under the Den API auth namespace.",
responses: {
200: emptyResponse("Better Auth handled the request successfully."),
302: emptyResponse("Better Auth redirected the user to continue the auth flow."),
400: emptyResponse("Better Auth rejected the request as invalid."),
401: emptyResponse("Better Auth rejected the request because authentication failed."),
},
}),
(c) => auth.handler(c.req.raw),
)
registerDesktopAuthRoutes(app)
}

View File

@@ -1,16 +1,57 @@
import type { Hono } from "hono"
import { describeRoute } from "hono-openapi"
import { z } from "zod"
import { requireUserMiddleware, resolveUserOrganizationsMiddleware, type UserOrganizationsContext } from "../../middleware/index.js"
import { jsonResponse, unauthorizedSchema } from "../../openapi.js"
import type { AuthContextVariables } from "../../session.js"
const meResponseSchema = z.object({
user: z.object({}).passthrough(),
session: z.object({}).passthrough(),
}).meta({ ref: "CurrentUserResponse" })
const meOrganizationsResponseSchema = z.object({
orgs: z.array(z.object({
id: z.string(),
isActive: z.boolean(),
}).passthrough()),
activeOrgId: z.string().nullable(),
activeOrgSlug: z.string().nullable(),
}).meta({ ref: "CurrentUserOrganizationsResponse" })
export function registerMeRoutes<T extends { Variables: AuthContextVariables & Partial<UserOrganizationsContext> }>(app: Hono<T>) {
app.get("/v1/me", requireUserMiddleware, (c) => {
app.get(
"/v1/me",
describeRoute({
tags: ["Users"],
summary: "Get current user",
description: "Returns the currently authenticated user and active session details for the caller.",
responses: {
200: jsonResponse("Current user and session returned successfully.", meResponseSchema),
401: jsonResponse("The caller must be signed in to read profile data.", unauthorizedSchema),
},
}),
requireUserMiddleware,
(c) => {
return c.json({
user: c.get("user"),
session: c.get("session"),
})
})
},
)
app.get("/v1/me/orgs", resolveUserOrganizationsMiddleware, (c) => {
app.get(
"/v1/me/orgs",
describeRoute({
tags: ["Users", "Organizations"],
summary: "List current user's organizations",
description: "Lists the organizations visible to the current user and marks which organization is currently active.",
responses: {
200: jsonResponse("Current user organizations returned successfully.", meOrganizationsResponseSchema),
},
}),
resolveUserOrganizationsMiddleware,
(c) => {
const orgs = (c.get("userOrganizations") ?? []) as NonNullable<UserOrganizationsContext["userOrganizations"]>
return c.json({
@@ -21,5 +62,6 @@ export function registerMeRoutes<T extends { Variables: AuthContextVariables & P
activeOrgId: c.get("activeOrganizationId") ?? null,
activeOrgSlug: c.get("activeOrganizationSlug") ?? null,
})
})
},
)
}

View File

@@ -1,4 +1,5 @@
import type { Hono } from "hono"
import { describeRoute, resolver } from "hono-openapi"
import { z } from "zod"
import {
buildOrganizationApiKeyMetadata,
@@ -14,13 +15,136 @@ import { ensureApiKeyManager, idParamSchema, orgIdParamSchema } from "./shared.j
const createOrganizationApiKeySchema = z.object({
name: z.string().trim().min(2).max(64),
})
}).meta({ ref: "CreateOrganizationApiKeyRequest" })
const validationIssueSchema = z.object({
message: z.string(),
path: z.array(z.union([z.string(), z.number()])).optional(),
}).passthrough()
const invalidRequestSchema = z.object({
error: z.literal("invalid_request"),
details: z.array(validationIssueSchema),
}).meta({ ref: "InvalidRequestError" })
const unauthorizedSchema = z.object({
error: z.literal("unauthorized"),
}).meta({ ref: "UnauthorizedError" })
const organizationNotFoundSchema = z.object({
error: z.literal("organization_not_found"),
}).meta({ ref: "OrganizationNotFoundError" })
const forbiddenApiKeyManagerSchema = z.object({
error: z.literal("forbidden"),
message: z.string(),
}).meta({ ref: "OrganizationApiKeyForbiddenError" })
const apiKeyNotFoundSchema = z.object({
error: z.literal("api_key_not_found"),
}).meta({ ref: "OrganizationApiKeyNotFoundError" })
const apiKeyOwnerSchema = z.object({
userId: z.string(),
memberId: z.string(),
name: z.string(),
email: z.string().email(),
image: z.string().nullable(),
}).meta({ ref: "OrganizationApiKeyOwner" })
const organizationApiKeySchema = z.object({
id: z.string(),
configId: z.string(),
name: z.string().nullable(),
start: z.string().nullable(),
prefix: z.string().nullable(),
enabled: z.boolean(),
rateLimitEnabled: z.boolean(),
rateLimitMax: z.number().int().nullable(),
rateLimitTimeWindow: z.number().int().nullable(),
lastRequest: z.string().datetime().nullable(),
expiresAt: z.string().datetime().nullable(),
createdAt: z.string().datetime(),
updatedAt: z.string().datetime(),
owner: apiKeyOwnerSchema,
}).meta({ ref: "OrganizationApiKey" })
const organizationApiKeyListResponseSchema = z.object({
apiKeys: z.array(organizationApiKeySchema),
}).meta({ ref: "OrganizationApiKeyListResponse" })
const createdOrganizationApiKeySchema = z.object({
id: z.string(),
name: z.string().nullable(),
start: z.string().nullable(),
prefix: z.string().nullable(),
enabled: z.boolean(),
rateLimitEnabled: z.boolean(),
rateLimitMax: z.number().int().nullable(),
rateLimitTimeWindow: z.number().int().nullable(),
createdAt: z.string().datetime(),
updatedAt: z.string().datetime(),
}).meta({ ref: "CreatedOrganizationApiKey" })
const createOrganizationApiKeyResponseSchema = z.object({
apiKey: createdOrganizationApiKeySchema,
key: z.string().min(1),
}).meta({ ref: "CreateOrganizationApiKeyResponse" })
const apiKeyIdParamSchema = orgIdParamSchema.extend(idParamSchema("apiKeyId").shape)
const hideApiKeyGenerationRoute = () => process.env.NODE_ENV === "production"
export function registerOrgApiKeyRoutes<T extends { Variables: OrgRouteVariables }>(app: Hono<T>) {
app.get(
"/v1/orgs/:orgId/api-keys",
describeRoute({
tags: ["Organizations", "Organization API Keys"],
summary: "List organization API keys",
description: "Returns the API keys that belong to the selected organization.",
security: [{ bearerAuth: [] }],
responses: {
200: {
description: "Organization API keys",
content: {
"application/json": {
schema: resolver(organizationApiKeyListResponseSchema),
},
},
},
400: {
description: "Invalid request",
content: {
"application/json": {
schema: resolver(invalidRequestSchema),
},
},
},
401: {
description: "Unauthorized",
content: {
"application/json": {
schema: resolver(unauthorizedSchema),
},
},
},
403: {
description: "Forbidden",
content: {
"application/json": {
schema: resolver(forbiddenApiKeyManagerSchema),
},
},
},
404: {
description: "Organization not found",
content: {
"application/json": {
schema: resolver(organizationNotFoundSchema),
},
},
},
},
}),
requireUserMiddleware,
paramValidator(orgIdParamSchema),
resolveOrganizationContextMiddleware,
@@ -38,6 +162,55 @@ export function registerOrgApiKeyRoutes<T extends { Variables: OrgRouteVariables
app.post(
"/v1/orgs/:orgId/api-keys",
describeRoute({
tags: ["Organizations", "Organization API Keys"],
summary: "Create an organization API key",
description: "Creates a new API key for the selected organization.",
hide: hideApiKeyGenerationRoute,
security: [{ bearerAuth: [] }],
responses: {
201: {
description: "Organization API key created",
content: {
"application/json": {
schema: resolver(createOrganizationApiKeyResponseSchema),
},
},
},
400: {
description: "Invalid request",
content: {
"application/json": {
schema: resolver(invalidRequestSchema),
},
},
},
401: {
description: "Unauthorized",
content: {
"application/json": {
schema: resolver(unauthorizedSchema),
},
},
},
403: {
description: "Forbidden",
content: {
"application/json": {
schema: resolver(forbiddenApiKeyManagerSchema),
},
},
},
404: {
description: "Organization not found",
content: {
"application/json": {
schema: resolver(organizationNotFoundSchema),
},
},
},
},
}),
requireUserMiddleware,
paramValidator(orgIdParamSchema),
resolveOrganizationContextMiddleware,
@@ -86,6 +259,49 @@ export function registerOrgApiKeyRoutes<T extends { Variables: OrgRouteVariables
app.delete(
"/v1/orgs/:orgId/api-keys/:apiKeyId",
describeRoute({
tags: ["Organizations", "Organization API Keys"],
summary: "Delete an organization API key",
description: "Deletes an API key from the selected organization.",
security: [{ bearerAuth: [] }],
responses: {
204: {
description: "Organization API key deleted",
},
400: {
description: "Invalid request",
content: {
"application/json": {
schema: resolver(invalidRequestSchema),
},
},
},
401: {
description: "Unauthorized",
content: {
"application/json": {
schema: resolver(unauthorizedSchema),
},
},
},
403: {
description: "Forbidden",
content: {
"application/json": {
schema: resolver(forbiddenApiKeyManagerSchema),
},
},
},
404: {
description: "API key or organization not found",
content: {
"application/json": {
schema: resolver(z.union([organizationNotFoundSchema, apiKeyNotFoundSchema])),
},
},
},
},
}),
requireUserMiddleware,
paramValidator(apiKeyIdParamSchema),
resolveOrganizationContextMiddleware,

View File

@@ -2,11 +2,13 @@ import { eq } from "@openwork-ee/den-db/drizzle"
import { OrganizationTable } from "@openwork-ee/den-db/schema"
import { normalizeDenTypeId } from "@openwork-ee/utils/typeid"
import type { Hono } from "hono"
import { describeRoute } from "hono-openapi"
import { z } from "zod"
import { requireCloudWorkerAccess } from "../../billing/polar.js"
import { db } from "../../db.js"
import { env } from "../../env.js"
import { jsonValidator, paramValidator, queryValidator, requireUserMiddleware, resolveMemberTeamsMiddleware, resolveOrganizationContextMiddleware } from "../../middleware/index.js"
import { forbiddenSchema, invalidRequestSchema, jsonResponse, notFoundSchema, unauthorizedSchema } from "../../openapi.js"
import { acceptInvitationForUser, createOrganizationForUser, getInvitationPreview, setSessionActiveOrganization } from "../../orgs.js"
import { getRequiredUserEmail } from "../../user.js"
import type { OrgRouteVariables } from "./shared.js"
@@ -24,6 +26,37 @@ const acceptInvitationSchema = z.object({
id: z.string().trim().min(1),
})
const organizationResponseSchema = z.object({
organization: z.object({}).passthrough().nullable(),
}).meta({ ref: "OrganizationResponse" })
const paymentRequiredSchema = z.object({
error: z.literal("payment_required"),
message: z.string(),
polar: z.object({
checkoutUrl: z.string().nullable(),
productId: z.string().nullable().optional(),
benefitId: z.string().nullable().optional(),
}).passthrough(),
}).meta({ ref: "PaymentRequiredError" })
const invitationPreviewResponseSchema = z.object({}).passthrough().meta({ ref: "InvitationPreviewResponse" })
const invitationAcceptedResponseSchema = z.object({
accepted: z.literal(true),
organizationId: z.string(),
organizationSlug: z.string().nullable(),
invitationId: z.string(),
}).meta({ ref: "InvitationAcceptedResponse" })
const organizationContextResponseSchema = z.object({
currentMemberTeams: z.array(z.object({}).passthrough()),
}).passthrough().meta({ ref: "OrganizationContextResponse" })
const userEmailRequiredSchema = z.object({
error: z.literal("user_email_required"),
}).meta({ ref: "UserEmailRequiredError" })
function getStoredSessionId(session: { id?: string | null } | null) {
if (!session?.id) {
return null
@@ -37,7 +70,23 @@ function getStoredSessionId(session: { id?: string | null } | null) {
}
export function registerOrgCoreRoutes<T extends { Variables: OrgRouteVariables }>(app: Hono<T>) {
app.post("/v1/orgs", requireUserMiddleware, jsonValidator(createOrganizationSchema), async (c) => {
app.post(
"/v1/orgs",
describeRoute({
tags: ["Organizations"],
summary: "Create organization",
description: "Creates a new organization for the signed-in user after verifying that their account can provision OpenWork Cloud workspaces.",
responses: {
201: jsonResponse("Organization created successfully.", organizationResponseSchema),
400: jsonResponse("The organization creation request body was invalid.", invalidRequestSchema),
401: jsonResponse("The caller must be signed in to create an organization.", unauthorizedSchema),
402: jsonResponse("The caller needs an active cloud plan before creating an organization.", paymentRequiredSchema),
403: jsonResponse("API keys cannot create organizations.", forbiddenSchema),
},
}),
requireUserMiddleware,
jsonValidator(createOrganizationSchema),
async (c) => {
if (c.get("apiKey")) {
return c.json({
error: "forbidden",
@@ -89,9 +138,23 @@ export function registerOrgCoreRoutes<T extends { Variables: OrgRouteVariables }
.limit(1)
return c.json({ organization: organization[0] ?? null }, 201)
})
},
)
app.get("/v1/orgs/invitations/preview", queryValidator(invitationPreviewQuerySchema), async (c) => {
app.get(
"/v1/orgs/invitations/preview",
describeRoute({
tags: ["Organizations", "Organization Invitations"],
summary: "Preview organization invitation",
description: "Returns invitation preview details so a user can inspect an organization invite before accepting it.",
responses: {
200: jsonResponse("Invitation preview returned successfully.", invitationPreviewResponseSchema),
400: jsonResponse("The invitation preview query parameters were invalid.", invalidRequestSchema),
404: jsonResponse("The invitation could not be found.", notFoundSchema),
},
}),
queryValidator(invitationPreviewQuerySchema),
async (c) => {
const query = c.req.valid("query")
const invitation = await getInvitationPreview(query.id)
@@ -100,9 +163,26 @@ export function registerOrgCoreRoutes<T extends { Variables: OrgRouteVariables }
}
return c.json(invitation)
})
},
)
app.post("/v1/orgs/invitations/accept", requireUserMiddleware, jsonValidator(acceptInvitationSchema), async (c) => {
app.post(
"/v1/orgs/invitations/accept",
describeRoute({
tags: ["Organizations", "Organization Invitations"],
summary: "Accept organization invitation",
description: "Accepts an organization invitation for the current signed-in user and switches their active organization to the accepted workspace.",
responses: {
200: jsonResponse("Invitation accepted successfully.", invitationAcceptedResponseSchema),
400: jsonResponse("The invitation acceptance request body was invalid.", invalidRequestSchema),
401: jsonResponse("The caller must be signed in to accept an invitation.", unauthorizedSchema),
403: jsonResponse("API keys cannot accept organization invitations.", forbiddenSchema),
404: jsonResponse("The invitation could not be found.", notFoundSchema),
},
}),
requireUserMiddleware,
jsonValidator(acceptInvitationSchema),
async (c) => {
if (c.get("apiKey")) {
return c.json({
error: "forbidden",
@@ -146,10 +226,22 @@ export function registerOrgCoreRoutes<T extends { Variables: OrgRouteVariables }
organizationSlug: orgRows[0]?.slug ?? null,
invitationId: accepted.invitation.id,
})
})
},
)
app.get(
"/v1/orgs/:orgId/context",
describeRoute({
tags: ["Organizations"],
summary: "Get organization context",
description: "Returns the resolved organization context for a specific org, including the current member record and their team memberships.",
responses: {
200: jsonResponse("Organization context returned successfully.", organizationContextResponseSchema),
400: jsonResponse("The organization context path parameters were invalid.", invalidRequestSchema),
401: jsonResponse("The caller must be signed in to load organization context.", unauthorizedSchema),
404: jsonResponse("The organization could not be found.", notFoundSchema),
},
}),
requireUserMiddleware,
paramValidator(orgIdParamSchema),
resolveOrganizationContextMiddleware,

View File

@@ -2,10 +2,12 @@ import { and, eq, gt } from "@openwork-ee/den-db/drizzle"
import { AuthUserTable, InvitationTable, MemberTable } from "@openwork-ee/den-db/schema"
import { normalizeDenTypeId } from "@openwork-ee/utils/typeid"
import type { Hono } from "hono"
import { describeRoute } from "hono-openapi"
import { z } from "zod"
import { db } from "../../db.js"
import { sendDenOrganizationInvitationEmail } from "../../email.js"
import { jsonValidator, paramValidator, requireUserMiddleware, resolveOrganizationContextMiddleware } from "../../middleware/index.js"
import { forbiddenSchema, invalidRequestSchema, jsonResponse, notFoundSchema, successSchema, unauthorizedSchema } from "../../openapi.js"
import { getOrganizationLimitStatus } from "../../organization-limits.js"
import { listAssignableRoles } from "../../orgs.js"
import type { OrgRouteVariables } from "./shared.js"
@@ -16,11 +18,37 @@ const inviteMemberSchema = z.object({
role: z.string().trim().min(1).max(64),
})
const invitationResponseSchema = z.object({
invitationId: z.string(),
email: z.string().email(),
role: z.string(),
expiresAt: z.string().datetime(),
}).meta({ ref: "InvitationResponse" })
type InvitationId = typeof InvitationTable.$inferSelect.id
const orgInvitationParamsSchema = orgIdParamSchema.extend(idParamSchema("invitationId").shape)
export function registerOrgInvitationRoutes<T extends { Variables: OrgRouteVariables }>(app: Hono<T>) {
app.post("/v1/orgs/:orgId/invitations", requireUserMiddleware, paramValidator(orgIdParamSchema), resolveOrganizationContextMiddleware, jsonValidator(inviteMemberSchema), async (c) => {
app.post(
"/v1/orgs/:orgId/invitations",
describeRoute({
tags: ["Organizations", "Organization Invitations"],
summary: "Create organization invitation",
description: "Creates or refreshes a pending organization invitation for an email address and sends the invite email.",
responses: {
200: jsonResponse("Existing invitation refreshed successfully.", invitationResponseSchema),
201: jsonResponse("Invitation created successfully.", invitationResponseSchema),
400: jsonResponse("The invitation request body or path parameters were invalid.", invalidRequestSchema),
401: jsonResponse("The caller must be signed in to invite organization members.", unauthorizedSchema),
403: jsonResponse("The caller is not allowed to manage invitations for this organization.", forbiddenSchema),
404: jsonResponse("The organization could not be found.", notFoundSchema),
},
}),
requireUserMiddleware,
paramValidator(orgIdParamSchema),
resolveOrganizationContextMiddleware,
jsonValidator(inviteMemberSchema),
async (c) => {
const permission = ensureInviteManager(c)
if (!permission.ok) {
return c.json(permission.response, permission.response.error === "forbidden" ? 403 : 404)
@@ -107,9 +135,27 @@ export function registerOrgInvitationRoutes<T extends { Variables: OrgRouteVaria
})
return c.json({ invitationId, email, role, expiresAt }, existingInvitation[0] ? 200 : 201)
})
},
)
app.post("/v1/orgs/:orgId/invitations/:invitationId/cancel", requireUserMiddleware, paramValidator(orgInvitationParamsSchema), resolveOrganizationContextMiddleware, async (c) => {
app.post(
"/v1/orgs/:orgId/invitations/:invitationId/cancel",
describeRoute({
tags: ["Organizations", "Organization Invitations"],
summary: "Cancel organization invitation",
description: "Cancels a pending organization invitation so the invite link can no longer be used.",
responses: {
200: jsonResponse("Invitation cancelled successfully.", successSchema),
400: jsonResponse("The invitation cancellation path parameters were invalid.", invalidRequestSchema),
401: jsonResponse("The caller must be signed in to cancel invitations.", unauthorizedSchema),
403: jsonResponse("The caller is not allowed to manage invitations for this organization.", forbiddenSchema),
404: jsonResponse("The invitation or organization could not be found.", notFoundSchema),
},
}),
requireUserMiddleware,
paramValidator(orgInvitationParamsSchema),
resolveOrganizationContextMiddleware,
async (c) => {
const permission = ensureInviteManager(c)
if (!permission.ok) {
return c.json(permission.response, permission.response.error === "forbidden" ? 403 : 404)
@@ -136,5 +182,6 @@ export function registerOrgInvitationRoutes<T extends { Variables: OrgRouteVaria
await db.update(InvitationTable).set({ status: "canceled" }).where(eq(InvitationTable.id, invitationId))
return c.json({ success: true })
})
},
)
}

View File

@@ -9,6 +9,7 @@ import {
} from "@openwork-ee/den-db/schema"
import { createDenTypeId, normalizeDenTypeId } from "@openwork-ee/utils/typeid"
import type { Hono } from "hono"
import { describeRoute } from "hono-openapi"
import { z } from "zod"
import { db } from "../../db.js"
import {
@@ -20,6 +21,7 @@ import {
} from "../../middleware/index.js"
import { getModelsDevProvider, listModelsDevProviders } from "../../llm/models-dev.js"
import type { MemberTeamsContext } from "../../middleware/member-teams.js"
import { emptyResponse, forbiddenSchema, invalidRequestSchema, jsonResponse, notFoundSchema, unauthorizedSchema } from "../../openapi.js"
import type { OrgRouteVariables } from "./shared.js"
import { idParamSchema, memberHasRole, orgIdParamSchema } from "./shared.js"
@@ -93,6 +95,32 @@ const llmProviderWriteSchema = z.object({
}
})
const providerCatalogListResponseSchema = z.object({
providers: z.array(z.object({}).passthrough()),
}).meta({ ref: "LlmProviderCatalogListResponse" })
const providerCatalogResponseSchema = z.object({
provider: z.object({}).passthrough(),
}).meta({ ref: "LlmProviderCatalogResponse" })
const llmProviderListResponseSchema = z.object({
llmProviders: z.array(z.object({}).passthrough()),
}).meta({ ref: "LlmProviderListResponse" })
const llmProviderResponseSchema = z.object({
llmProvider: z.object({}).passthrough(),
}).meta({ ref: "LlmProviderResponse" })
const providerCatalogUnavailableSchema = z.object({
error: z.literal("provider_catalog_unavailable"),
message: z.string(),
}).meta({ ref: "ProviderCatalogUnavailableError" })
const conflictSchema = z.object({
error: z.string(),
message: z.string().optional(),
}).meta({ ref: "ConflictError" })
function createFailure(status: number, error: string, message?: string): RouteFailure {
return { status, error, message }
}
@@ -453,6 +481,17 @@ async function loadLlmProviders(input: {
export function registerOrgLlmProviderRoutes<T extends { Variables: OrgRouteVariables & Partial<MemberTeamsContext> }>(app: Hono<T>) {
app.get(
"/v1/orgs/:orgId/llm-provider-catalog",
describeRoute({
tags: ["Organizations", "Organization LLM Providers"],
summary: "List LLM provider catalog",
description: "Lists the provider catalog from models.dev so an organization can choose which LLM providers to configure.",
responses: {
200: jsonResponse("Provider catalog returned successfully.", providerCatalogListResponseSchema),
400: jsonResponse("The provider catalog path parameters were invalid.", invalidRequestSchema),
401: jsonResponse("The caller must be signed in to browse the provider catalog.", unauthorizedSchema),
502: jsonResponse("The external provider catalog was unavailable.", providerCatalogUnavailableSchema),
},
}),
requireUserMiddleware,
paramValidator(orgIdParamSchema),
resolveOrganizationContextMiddleware,
@@ -471,6 +510,18 @@ export function registerOrgLlmProviderRoutes<T extends { Variables: OrgRouteVari
app.get(
"/v1/orgs/:orgId/llm-provider-catalog/:providerId",
describeRoute({
tags: ["Organizations", "Organization LLM Providers"],
summary: "Get LLM provider catalog entry",
description: "Returns the full models.dev catalog record for one provider, including its config template and model list.",
responses: {
200: jsonResponse("Provider catalog entry returned successfully.", providerCatalogResponseSchema),
400: jsonResponse("The provider catalog path parameters were invalid.", invalidRequestSchema),
401: jsonResponse("The caller must be signed in to inspect provider catalog entries.", unauthorizedSchema),
404: jsonResponse("The requested provider catalog entry could not be found.", notFoundSchema),
502: jsonResponse("The external provider catalog was unavailable.", providerCatalogUnavailableSchema),
},
}),
requireUserMiddleware,
paramValidator(providerCatalogParamsSchema),
resolveOrganizationContextMiddleware,
@@ -506,6 +557,16 @@ export function registerOrgLlmProviderRoutes<T extends { Variables: OrgRouteVari
app.get(
"/v1/orgs/:orgId/llm-providers",
describeRoute({
tags: ["Organizations", "Organization LLM Providers"],
summary: "List organization LLM providers",
description: "Lists the LLM providers that the current organization member is allowed to see and potentially manage.",
responses: {
200: jsonResponse("Accessible organization LLM providers returned successfully.", llmProviderListResponseSchema),
400: jsonResponse("The provider list path parameters were invalid.", invalidRequestSchema),
401: jsonResponse("The caller must be signed in to list organization LLM providers.", unauthorizedSchema),
},
}),
requireUserMiddleware,
paramValidator(orgIdParamSchema),
resolveOrganizationContextMiddleware,
@@ -532,6 +593,18 @@ export function registerOrgLlmProviderRoutes<T extends { Variables: OrgRouteVari
app.get(
"/v1/orgs/:orgId/llm-providers/:llmProviderId/connect",
describeRoute({
tags: ["Organizations", "Organization LLM Providers"],
summary: "Get LLM provider connect payload",
description: "Returns one accessible organization LLM provider with the concrete model configuration needed to connect to it.",
responses: {
200: jsonResponse("Provider connection payload returned successfully.", llmProviderResponseSchema),
400: jsonResponse("The provider connect path parameters were invalid.", invalidRequestSchema),
401: jsonResponse("The caller must be signed in to connect to an organization LLM provider.", unauthorizedSchema),
403: jsonResponse("The caller does not have access to this organization LLM provider.", forbiddenSchema),
404: jsonResponse("The provider could not be found.", notFoundSchema),
},
}),
requireUserMiddleware,
paramValidator(orgLlmProviderParamsSchema),
resolveOrganizationContextMiddleware,
@@ -597,6 +670,17 @@ export function registerOrgLlmProviderRoutes<T extends { Variables: OrgRouteVari
app.post(
"/v1/orgs/:orgId/llm-providers",
describeRoute({
tags: ["Organizations", "Organization LLM Providers"],
summary: "Create organization LLM provider",
description: "Creates a new organization-scoped LLM provider from either a models.dev provider template or a pasted custom configuration.",
responses: {
201: jsonResponse("Organization LLM provider created successfully.", llmProviderResponseSchema),
400: jsonResponse("The provider creation request was invalid.", invalidRequestSchema),
401: jsonResponse("The caller must be signed in to create organization LLM providers.", unauthorizedSchema),
404: jsonResponse("A referenced provider, model, member, or team could not be found.", notFoundSchema),
},
}),
requireUserMiddleware,
paramValidator(orgIdParamSchema),
resolveOrganizationContextMiddleware,
@@ -698,6 +782,18 @@ export function registerOrgLlmProviderRoutes<T extends { Variables: OrgRouteVari
app.patch(
"/v1/orgs/:orgId/llm-providers/:llmProviderId",
describeRoute({
tags: ["Organizations", "Organization LLM Providers"],
summary: "Update organization LLM provider",
description: "Updates an existing organization LLM provider, including its provider config, selected models, secret, and access grants.",
responses: {
200: jsonResponse("Organization LLM provider updated successfully.", llmProviderResponseSchema),
400: jsonResponse("The provider update request was invalid.", invalidRequestSchema),
401: jsonResponse("The caller must be signed in to update organization LLM providers.", unauthorizedSchema),
403: jsonResponse("The caller is not allowed to update this organization LLM provider.", forbiddenSchema),
404: jsonResponse("The provider or a referenced resource could not be found.", notFoundSchema),
},
}),
requireUserMiddleware,
paramValidator(orgLlmProviderParamsSchema),
resolveOrganizationContextMiddleware,
@@ -822,6 +918,18 @@ export function registerOrgLlmProviderRoutes<T extends { Variables: OrgRouteVari
app.delete(
"/v1/orgs/:orgId/llm-providers/:llmProviderId",
describeRoute({
tags: ["Organizations", "Organization LLM Providers"],
summary: "Delete organization LLM provider",
description: "Deletes an organization LLM provider and removes its models and access rules.",
responses: {
204: emptyResponse("Organization LLM provider deleted successfully."),
400: jsonResponse("The provider deletion path parameters were invalid.", invalidRequestSchema),
401: jsonResponse("The caller must be signed in to delete organization LLM providers.", unauthorizedSchema),
403: jsonResponse("The caller is not allowed to delete this organization LLM provider.", forbiddenSchema),
404: jsonResponse("The provider could not be found.", notFoundSchema),
},
}),
requireUserMiddleware,
paramValidator(orgLlmProviderParamsSchema),
resolveOrganizationContextMiddleware,
@@ -866,6 +974,19 @@ export function registerOrgLlmProviderRoutes<T extends { Variables: OrgRouteVari
app.delete(
"/v1/orgs/:orgId/llm-providers/:llmProviderId/access/:accessId",
describeRoute({
tags: ["Organizations", "Organization LLM Providers"],
summary: "Remove LLM provider access grant",
description: "Removes one explicit member or team access grant from an organization LLM provider.",
responses: {
204: emptyResponse("Organization LLM provider access removed successfully."),
400: jsonResponse("The provider access deletion path parameters were invalid.", invalidRequestSchema),
401: jsonResponse("The caller must be signed in to manage provider access.", unauthorizedSchema),
403: jsonResponse("The caller is not allowed to manage access for this provider.", forbiddenSchema),
404: jsonResponse("The provider or access grant could not be found.", notFoundSchema),
409: jsonResponse("The request tried to remove a protected provider access entry.", conflictSchema),
},
}),
requireUserMiddleware,
paramValidator(orgLlmProviderParamsSchema.extend(idParamSchema("accessId").shape)),
resolveOrganizationContextMiddleware,

View File

@@ -2,9 +2,11 @@ import { and, eq } from "@openwork-ee/den-db/drizzle"
import { MemberTable } from "@openwork-ee/den-db/schema"
import { normalizeDenTypeId } from "@openwork-ee/utils/typeid"
import type { Hono } from "hono"
import { describeRoute } from "hono-openapi"
import { z } from "zod"
import { db } from "../../db.js"
import { jsonValidator, paramValidator, requireUserMiddleware, resolveOrganizationContextMiddleware } from "../../middleware/index.js"
import { emptyResponse, forbiddenSchema, invalidRequestSchema, jsonResponse, notFoundSchema, successSchema, unauthorizedSchema } from "../../openapi.js"
import { listAssignableRoles, removeOrganizationMember, roleIncludesOwner } from "../../orgs.js"
import type { OrgRouteVariables } from "./shared.js"
import { ensureOwner, idParamSchema, normalizeRoleName, orgIdParamSchema } from "./shared.js"
@@ -17,7 +19,25 @@ type MemberId = typeof MemberTable.$inferSelect.id
const orgMemberParamsSchema = orgIdParamSchema.extend(idParamSchema("memberId").shape)
export function registerOrgMemberRoutes<T extends { Variables: OrgRouteVariables }>(app: Hono<T>) {
app.post("/v1/orgs/:orgId/members/:memberId/role", requireUserMiddleware, paramValidator(orgMemberParamsSchema), resolveOrganizationContextMiddleware, jsonValidator(updateMemberRoleSchema), async (c) => {
app.post(
"/v1/orgs/:orgId/members/:memberId/role",
describeRoute({
tags: ["Organizations", "Organization Members"],
summary: "Update member role",
description: "Changes the role assigned to a specific organization member.",
responses: {
200: jsonResponse("Member role updated successfully.", successSchema),
400: jsonResponse("The member role update request was invalid.", invalidRequestSchema),
401: jsonResponse("The caller must be signed in to update member roles.", unauthorizedSchema),
403: jsonResponse("Only organization owners can update member roles.", forbiddenSchema),
404: jsonResponse("The member or organization could not be found.", notFoundSchema),
},
}),
requireUserMiddleware,
paramValidator(orgMemberParamsSchema),
resolveOrganizationContextMiddleware,
jsonValidator(updateMemberRoleSchema),
async (c) => {
const permission = ensureOwner(c)
if (!permission.ok) {
return c.json(permission.response, 403)
@@ -57,9 +77,27 @@ export function registerOrgMemberRoutes<T extends { Variables: OrgRouteVariables
await db.update(MemberTable).set({ role }).where(eq(MemberTable.id, member.id))
return c.json({ success: true })
})
},
)
app.delete("/v1/orgs/:orgId/members/:memberId", requireUserMiddleware, paramValidator(orgMemberParamsSchema), resolveOrganizationContextMiddleware, async (c) => {
app.delete(
"/v1/orgs/:orgId/members/:memberId",
describeRoute({
tags: ["Organizations", "Organization Members"],
summary: "Remove organization member",
description: "Removes a member from an organization while protecting the owner role from deletion.",
responses: {
204: emptyResponse("Member removed successfully."),
400: jsonResponse("The member removal request was invalid.", invalidRequestSchema),
401: jsonResponse("The caller must be signed in to remove organization members.", unauthorizedSchema),
403: jsonResponse("Only organization owners can remove members.", forbiddenSchema),
404: jsonResponse("The member or organization could not be found.", notFoundSchema),
},
}),
requireUserMiddleware,
paramValidator(orgMemberParamsSchema),
resolveOrganizationContextMiddleware,
async (c) => {
const permission = ensureOwner(c)
if (!permission.ok) {
return c.json(permission.response, 403)
@@ -94,5 +132,6 @@ export function registerOrgMemberRoutes<T extends { Variables: OrgRouteVariables
memberId: member.id,
})
return c.body(null, 204)
})
},
)
}

View File

@@ -2,9 +2,11 @@ import { and, eq } from "@openwork-ee/den-db/drizzle"
import { InvitationTable, MemberTable, OrganizationRoleTable } from "@openwork-ee/den-db/schema"
import { normalizeDenTypeId } from "@openwork-ee/utils/typeid"
import type { Hono } from "hono"
import { describeRoute } from "hono-openapi"
import { z } from "zod"
import { db } from "../../db.js"
import { jsonValidator, paramValidator, requireUserMiddleware, resolveOrganizationContextMiddleware } from "../../middleware/index.js"
import { emptyResponse, forbiddenSchema, invalidRequestSchema, jsonResponse, notFoundSchema, successSchema, unauthorizedSchema } from "../../openapi.js"
import { serializePermissionRecord } from "../../orgs.js"
import type { OrgRouteVariables } from "./shared.js"
import { createRoleId, ensureOwner, idParamSchema, normalizeRoleName, orgIdParamSchema, replaceRoleValue, splitRoles } from "./shared.js"
@@ -25,7 +27,25 @@ type OrganizationRoleId = typeof OrganizationRoleTable.$inferSelect.id
const orgRoleParamsSchema = orgIdParamSchema.extend(idParamSchema("roleId").shape)
export function registerOrgRoleRoutes<T extends { Variables: OrgRouteVariables }>(app: Hono<T>) {
app.post("/v1/orgs/:orgId/roles", requireUserMiddleware, paramValidator(orgIdParamSchema), resolveOrganizationContextMiddleware, jsonValidator(createRoleSchema), async (c) => {
app.post(
"/v1/orgs/:orgId/roles",
describeRoute({
tags: ["Organizations", "Organization Roles"],
summary: "Create organization role",
description: "Creates a custom organization role with a named permission map.",
responses: {
201: jsonResponse("Organization role created successfully.", successSchema),
400: jsonResponse("The role creation request was invalid.", invalidRequestSchema),
401: jsonResponse("The caller must be signed in to create organization roles.", unauthorizedSchema),
403: jsonResponse("Only organization owners can create organization roles.", forbiddenSchema),
404: jsonResponse("The organization could not be found.", notFoundSchema),
},
}),
requireUserMiddleware,
paramValidator(orgIdParamSchema),
resolveOrganizationContextMiddleware,
jsonValidator(createRoleSchema),
async (c) => {
const permission = ensureOwner(c)
if (!permission.ok) {
return c.json(permission.response, 403)
@@ -57,9 +77,28 @@ export function registerOrgRoleRoutes<T extends { Variables: OrgRouteVariables }
})
return c.json({ success: true }, 201)
})
},
)
app.patch("/v1/orgs/:orgId/roles/:roleId", requireUserMiddleware, paramValidator(orgRoleParamsSchema), resolveOrganizationContextMiddleware, jsonValidator(updateRoleSchema), async (c) => {
app.patch(
"/v1/orgs/:orgId/roles/:roleId",
describeRoute({
tags: ["Organizations", "Organization Roles"],
summary: "Update organization role",
description: "Updates a custom organization role and propagates role name changes to members and pending invitations.",
responses: {
200: jsonResponse("Organization role updated successfully.", successSchema),
400: jsonResponse("The role update request was invalid.", invalidRequestSchema),
401: jsonResponse("The caller must be signed in to update organization roles.", unauthorizedSchema),
403: jsonResponse("Only organization owners can update organization roles.", forbiddenSchema),
404: jsonResponse("The role or organization could not be found.", notFoundSchema),
},
}),
requireUserMiddleware,
paramValidator(orgRoleParamsSchema),
resolveOrganizationContextMiddleware,
jsonValidator(updateRoleSchema),
async (c) => {
const permission = ensureOwner(c)
if (!permission.ok) {
return c.json(permission.response, 403)
@@ -145,9 +184,27 @@ export function registerOrgRoleRoutes<T extends { Variables: OrgRouteVariables }
}
return c.json({ success: true })
})
},
)
app.delete("/v1/orgs/:orgId/roles/:roleId", requireUserMiddleware, paramValidator(orgRoleParamsSchema), resolveOrganizationContextMiddleware, async (c) => {
app.delete(
"/v1/orgs/:orgId/roles/:roleId",
describeRoute({
tags: ["Organizations", "Organization Roles"],
summary: "Delete organization role",
description: "Deletes a custom organization role after confirming that no members or pending invitations still depend on it.",
responses: {
204: emptyResponse("Organization role deleted successfully."),
400: jsonResponse("The role deletion request was invalid.", invalidRequestSchema),
401: jsonResponse("The caller must be signed in to delete organization roles.", unauthorizedSchema),
403: jsonResponse("Only organization owners can delete organization roles.", forbiddenSchema),
404: jsonResponse("The role or organization could not be found.", notFoundSchema),
},
}),
requireUserMiddleware,
paramValidator(orgRoleParamsSchema),
resolveOrganizationContextMiddleware,
async (c) => {
const permission = ensureOwner(c)
if (!permission.ok) {
return c.json(permission.response, 403)
@@ -196,5 +253,6 @@ export function registerOrgRoleRoutes<T extends { Variables: OrgRouteVariables }
await db.delete(OrganizationRoleTable).where(eq(OrganizationRoleTable.id, roleRow.id))
return c.body(null, 204)
})
},
)
}

View File

@@ -11,6 +11,7 @@ import {
import { hasSkillFrontmatterName, parseSkillMarkdown } from "@openwork-ee/utils"
import { createDenTypeId, normalizeDenTypeId } from "@openwork-ee/utils/typeid"
import type { Hono } from "hono"
import { describeRoute } from "hono-openapi"
import { z } from "zod"
import { db } from "../../db.js"
import {
@@ -21,6 +22,7 @@ import {
resolveOrganizationContextMiddleware,
} from "../../middleware/index.js"
import type { MemberTeamsContext } from "../../middleware/member-teams.js"
import { emptyResponse, forbiddenSchema, invalidRequestSchema, jsonResponse, notFoundSchema, successSchema, unauthorizedSchema } from "../../openapi.js"
import type { OrgRouteVariables } from "./shared.js"
import { idParamSchema, memberHasRole, orgIdParamSchema } from "./shared.js"
@@ -108,6 +110,31 @@ const orgSkillParamsSchema = orgIdParamSchema.extend(idParamSchema("skillId").sh
const orgSkillHubSkillParamsSchema = orgSkillHubParamsSchema.extend(idParamSchema("skillId").shape)
const orgSkillHubAccessParamsSchema = orgSkillHubParamsSchema.extend(idParamSchema("accessId").shape)
const skillResponseSchema = z.object({
skill: z.object({}).passthrough(),
}).meta({ ref: "SkillResponse" })
const skillListResponseSchema = z.object({
skills: z.array(z.object({}).passthrough()),
}).meta({ ref: "SkillListResponse" })
const skillHubResponseSchema = z.object({
skillHub: z.object({}).passthrough(),
}).meta({ ref: "SkillHubResponse" })
const skillHubListResponseSchema = z.object({
skillHubs: z.array(z.object({}).passthrough()),
}).meta({ ref: "SkillHubListResponse" })
const skillHubAccessResponseSchema = z.object({
access: z.object({}).passthrough(),
}).meta({ ref: "SkillHubAccessResponse" })
const conflictSchema = z.object({
error: z.string(),
message: z.string().optional(),
}).meta({ ref: "ConflictError" })
function parseSkillId(value: string) {
return normalizeDenTypeId("skill", value)
}
@@ -237,6 +264,16 @@ function canViewSkill(input: {
export function registerOrgSkillRoutes<T extends { Variables: OrgRouteVariables & Partial<MemberTeamsContext> }>(app: Hono<T>) {
app.post(
"/v1/orgs/:orgId/skills",
describeRoute({
tags: ["Organizations", "Organization Skills"],
summary: "Create skill",
description: "Creates a new skill in the organization from markdown content and optional sharing visibility.",
responses: {
201: jsonResponse("Skill created successfully.", skillResponseSchema),
400: jsonResponse("The skill creation request was invalid.", invalidRequestSchema),
401: jsonResponse("The caller must be signed in to create skills.", unauthorizedSchema),
},
}),
requireUserMiddleware,
paramValidator(orgIdParamSchema),
resolveOrganizationContextMiddleware,
@@ -278,6 +315,16 @@ export function registerOrgSkillRoutes<T extends { Variables: OrgRouteVariables
app.get(
"/v1/orgs/:orgId/skills",
describeRoute({
tags: ["Organizations", "Organization Skills"],
summary: "List skills",
description: "Lists the skills the current member can view, including owned skills, shared skills, and skills available through hub access.",
responses: {
200: jsonResponse("Accessible skills returned successfully.", skillListResponseSchema),
400: jsonResponse("The skill list path parameters were invalid.", invalidRequestSchema),
401: jsonResponse("The caller must be signed in to list skills.", unauthorizedSchema),
},
}),
requireUserMiddleware,
paramValidator(orgIdParamSchema),
resolveOrganizationContextMiddleware,
@@ -314,6 +361,18 @@ export function registerOrgSkillRoutes<T extends { Variables: OrgRouteVariables
app.delete(
"/v1/orgs/:orgId/skills/:skillId",
describeRoute({
tags: ["Organizations", "Organization Skills"],
summary: "Delete skill",
description: "Deletes one organization skill when the caller is allowed to manage it.",
responses: {
204: emptyResponse("Skill deleted successfully."),
400: jsonResponse("The skill deletion path parameters were invalid.", invalidRequestSchema),
401: jsonResponse("The caller must be signed in to delete skills.", unauthorizedSchema),
403: jsonResponse("The caller is not allowed to delete this skill.", forbiddenSchema),
404: jsonResponse("The skill could not be found.", notFoundSchema),
},
}),
requireUserMiddleware,
paramValidator(orgSkillParamsSchema),
resolveOrganizationContextMiddleware,
@@ -354,6 +413,18 @@ export function registerOrgSkillRoutes<T extends { Variables: OrgRouteVariables
app.patch(
"/v1/orgs/:orgId/skills/:skillId",
describeRoute({
tags: ["Organizations", "Organization Skills"],
summary: "Update skill",
description: "Updates a skill's markdown content and-or sharing visibility while keeping derived metadata in sync.",
responses: {
200: jsonResponse("Skill updated successfully.", skillResponseSchema),
400: jsonResponse("The skill update request was invalid.", invalidRequestSchema),
401: jsonResponse("The caller must be signed in to update skills.", unauthorizedSchema),
403: jsonResponse("The caller is not allowed to update this skill.", forbiddenSchema),
404: jsonResponse("The skill could not be found.", notFoundSchema),
},
}),
requireUserMiddleware,
paramValidator(orgSkillParamsSchema),
resolveOrganizationContextMiddleware,
@@ -416,6 +487,16 @@ export function registerOrgSkillRoutes<T extends { Variables: OrgRouteVariables
app.post(
"/v1/orgs/:orgId/skill-hubs",
describeRoute({
tags: ["Organizations", "Organization Skill Hubs"],
summary: "Create skill hub",
description: "Creates a skill hub that can group skills and assign access to specific members or teams.",
responses: {
201: jsonResponse("Skill hub created successfully.", skillHubResponseSchema),
400: jsonResponse("The skill hub creation request was invalid.", invalidRequestSchema),
401: jsonResponse("The caller must be signed in to create skill hubs.", unauthorizedSchema),
},
}),
requireUserMiddleware,
paramValidator(orgIdParamSchema),
resolveOrganizationContextMiddleware,
@@ -462,6 +543,16 @@ export function registerOrgSkillRoutes<T extends { Variables: OrgRouteVariables
app.get(
"/v1/orgs/:orgId/skill-hubs",
describeRoute({
tags: ["Organizations", "Organization Skill Hubs"],
summary: "List skill hubs",
description: "Lists the skill hubs the current member can access, along with linked skills and access metadata.",
responses: {
200: jsonResponse("Accessible skill hubs returned successfully.", skillHubListResponseSchema),
400: jsonResponse("The skill hub list path parameters were invalid.", invalidRequestSchema),
401: jsonResponse("The caller must be signed in to list skill hubs.", unauthorizedSchema),
},
}),
requireUserMiddleware,
paramValidator(orgIdParamSchema),
resolveOrganizationContextMiddleware,
@@ -608,6 +699,18 @@ export function registerOrgSkillRoutes<T extends { Variables: OrgRouteVariables
app.patch(
"/v1/orgs/:orgId/skill-hubs/:skillHubId",
describeRoute({
tags: ["Organizations", "Organization Skill Hubs"],
summary: "Update skill hub",
description: "Updates a skill hub's display name or description.",
responses: {
200: jsonResponse("Skill hub updated successfully.", skillHubResponseSchema),
400: jsonResponse("The skill hub update request was invalid.", invalidRequestSchema),
401: jsonResponse("The caller must be signed in to update skill hubs.", unauthorizedSchema),
403: jsonResponse("The caller is not allowed to update this skill hub.", forbiddenSchema),
404: jsonResponse("The skill hub could not be found.", notFoundSchema),
},
}),
requireUserMiddleware,
paramValidator(orgSkillHubParamsSchema),
resolveOrganizationContextMiddleware,
@@ -665,6 +768,18 @@ export function registerOrgSkillRoutes<T extends { Variables: OrgRouteVariables
app.delete(
"/v1/orgs/:orgId/skill-hubs/:skillHubId",
describeRoute({
tags: ["Organizations", "Organization Skill Hubs"],
summary: "Delete skill hub",
description: "Deletes a skill hub and removes its access links and skill links.",
responses: {
204: emptyResponse("Skill hub deleted successfully."),
400: jsonResponse("The skill hub deletion path parameters were invalid.", invalidRequestSchema),
401: jsonResponse("The caller must be signed in to delete skill hubs.", unauthorizedSchema),
403: jsonResponse("The caller is not allowed to delete this skill hub.", forbiddenSchema),
404: jsonResponse("The skill hub could not be found.", notFoundSchema),
},
}),
requireUserMiddleware,
paramValidator(orgSkillHubParamsSchema),
resolveOrganizationContextMiddleware,
@@ -706,6 +821,19 @@ export function registerOrgSkillRoutes<T extends { Variables: OrgRouteVariables
app.post(
"/v1/orgs/:orgId/skill-hubs/:skillHubId/skills",
describeRoute({
tags: ["Organizations", "Organization Skill Hubs"],
summary: "Add skill to skill hub",
description: "Adds an existing organization skill to a skill hub so hub members can discover and use it.",
responses: {
201: jsonResponse("Skill added to skill hub successfully.", successSchema),
400: jsonResponse("The add-skill-to-hub request was invalid.", invalidRequestSchema),
401: jsonResponse("The caller must be signed in to manage skill hub contents.", unauthorizedSchema),
403: jsonResponse("The caller is not allowed to add this skill to the skill hub.", forbiddenSchema),
404: jsonResponse("The skill hub or skill could not be found.", notFoundSchema),
409: jsonResponse("The skill is already attached to the skill hub.", conflictSchema),
},
}),
requireUserMiddleware,
paramValidator(orgSkillHubParamsSchema),
resolveOrganizationContextMiddleware,
@@ -781,6 +909,18 @@ export function registerOrgSkillRoutes<T extends { Variables: OrgRouteVariables
app.delete(
"/v1/orgs/:orgId/skill-hubs/:skillHubId/skills/:skillId",
describeRoute({
tags: ["Organizations", "Organization Skill Hubs"],
summary: "Remove skill from skill hub",
description: "Removes a skill from a skill hub without deleting the underlying skill itself.",
responses: {
204: emptyResponse("Skill removed from skill hub successfully."),
400: jsonResponse("The remove-skill-from-hub path parameters were invalid.", invalidRequestSchema),
401: jsonResponse("The caller must be signed in to manage skill hub contents.", unauthorizedSchema),
403: jsonResponse("The caller is not allowed to remove skills from this skill hub.", forbiddenSchema),
404: jsonResponse("The skill hub or hub-skill link could not be found.", notFoundSchema),
},
}),
requireUserMiddleware,
paramValidator(orgSkillHubSkillParamsSchema),
resolveOrganizationContextMiddleware,
@@ -832,6 +972,19 @@ export function registerOrgSkillRoutes<T extends { Variables: OrgRouteVariables
app.post(
"/v1/orgs/:orgId/skill-hubs/:skillHubId/access",
describeRoute({
tags: ["Organizations", "Organization Skill Hubs"],
summary: "Grant skill hub access",
description: "Grants a specific member or team access to a skill hub.",
responses: {
201: jsonResponse("Skill hub access granted successfully.", skillHubAccessResponseSchema),
400: jsonResponse("The skill hub access request was invalid.", invalidRequestSchema),
401: jsonResponse("The caller must be signed in to manage skill hub access.", unauthorizedSchema),
403: jsonResponse("The caller is not allowed to manage access for this skill hub.", forbiddenSchema),
404: jsonResponse("The skill hub or access target could not be found.", notFoundSchema),
409: jsonResponse("The requested access entry already exists.", conflictSchema),
},
}),
requireUserMiddleware,
paramValidator(orgSkillHubParamsSchema),
resolveOrganizationContextMiddleware,
@@ -930,6 +1083,18 @@ export function registerOrgSkillRoutes<T extends { Variables: OrgRouteVariables
app.delete(
"/v1/orgs/:orgId/skill-hubs/:skillHubId/access/:accessId",
describeRoute({
tags: ["Organizations", "Organization Skill Hubs"],
summary: "Revoke skill hub access",
description: "Revokes one member or team access entry from a skill hub.",
responses: {
204: emptyResponse("Skill hub access removed successfully."),
400: jsonResponse("The skill hub access deletion path parameters were invalid.", invalidRequestSchema),
401: jsonResponse("The caller must be signed in to manage skill hub access.", unauthorizedSchema),
403: jsonResponse("The caller is not allowed to manage access for this skill hub.", forbiddenSchema),
404: jsonResponse("The skill hub or access entry could not be found.", notFoundSchema),
},
}),
requireUserMiddleware,
paramValidator(orgSkillHubAccessParamsSchema),
resolveOrganizationContextMiddleware,

View File

@@ -7,6 +7,7 @@ import {
} from "@openwork-ee/den-db/schema"
import { createDenTypeId, normalizeDenTypeId } from "@openwork-ee/utils/typeid"
import type { Hono } from "hono"
import { describeRoute } from "hono-openapi"
import { z } from "zod"
import { db } from "../../db.js"
import {
@@ -15,6 +16,7 @@ import {
requireUserMiddleware,
resolveOrganizationContextMiddleware,
} from "../../middleware/index.js"
import { emptyResponse, forbiddenSchema, invalidRequestSchema, jsonResponse, notFoundSchema, unauthorizedSchema } from "../../openapi.js"
import type { OrgRouteVariables } from "./shared.js"
import {
ensureTeamManager,
@@ -45,6 +47,17 @@ type MemberId = typeof MemberTable.$inferSelect.id
const orgTeamParamsSchema = orgIdParamSchema.extend(idParamSchema("teamId").shape)
const teamResponseSchema = z.object({
team: z.object({
id: z.string(),
organizationId: z.string(),
name: z.string(),
createdAt: z.string().datetime(),
updatedAt: z.string().datetime(),
memberIds: z.array(z.string()),
}),
}).meta({ ref: "TeamResponse" })
function parseTeamId(value: string) {
return normalizeDenTypeId("team", value)
}
@@ -73,6 +86,18 @@ async function ensureMembersBelongToOrganization(input: {
export function registerOrgTeamRoutes<T extends { Variables: OrgRouteVariables }>(app: Hono<T>) {
app.post(
"/v1/orgs/:orgId/teams",
describeRoute({
tags: ["Organizations", "Organization Teams"],
summary: "Create team",
description: "Creates a team inside an organization and can optionally attach existing organization members to it.",
responses: {
201: jsonResponse("Team created successfully.", teamResponseSchema),
400: jsonResponse("The team creation request was invalid.", invalidRequestSchema),
401: jsonResponse("The caller must be signed in to create teams.", unauthorizedSchema),
403: jsonResponse("The caller is not allowed to manage teams for this organization.", forbiddenSchema),
404: jsonResponse("The organization or a referenced member could not be found.", notFoundSchema),
},
}),
requireUserMiddleware,
paramValidator(orgIdParamSchema),
resolveOrganizationContextMiddleware,
@@ -150,6 +175,18 @@ export function registerOrgTeamRoutes<T extends { Variables: OrgRouteVariables }
app.patch(
"/v1/orgs/:orgId/teams/:teamId",
describeRoute({
tags: ["Organizations", "Organization Teams"],
summary: "Update team",
description: "Updates a team's name and-or membership list within an organization.",
responses: {
200: jsonResponse("Team updated successfully.", teamResponseSchema),
400: jsonResponse("The team update request was invalid.", invalidRequestSchema),
401: jsonResponse("The caller must be signed in to update teams.", unauthorizedSchema),
403: jsonResponse("The caller is not allowed to manage teams for this organization.", forbiddenSchema),
404: jsonResponse("The team, organization, or a referenced member could not be found.", notFoundSchema),
},
}),
requireUserMiddleware,
paramValidator(orgTeamParamsSchema),
resolveOrganizationContextMiddleware,
@@ -242,6 +279,18 @@ export function registerOrgTeamRoutes<T extends { Variables: OrgRouteVariables }
app.delete(
"/v1/orgs/:orgId/teams/:teamId",
describeRoute({
tags: ["Organizations", "Organization Teams"],
summary: "Delete team",
description: "Deletes a team and removes its related hub-access and team-membership records.",
responses: {
204: emptyResponse("Team deleted successfully."),
400: jsonResponse("The team deletion path parameters were invalid.", invalidRequestSchema),
401: jsonResponse("The caller must be signed in to delete teams.", unauthorizedSchema),
403: jsonResponse("The caller is not allowed to manage teams for this organization.", forbiddenSchema),
404: jsonResponse("The team or organization could not be found.", notFoundSchema),
},
}),
requireUserMiddleware,
paramValidator(orgTeamParamsSchema),
resolveOrganizationContextMiddleware,

View File

@@ -2,9 +2,11 @@ import { and, desc, eq } from "@openwork-ee/den-db/drizzle"
import { AuthUserTable, MemberTable, TempTemplateSharingTable } from "@openwork-ee/den-db/schema"
import { createDenTypeId, normalizeDenTypeId } from "@openwork-ee/utils/typeid"
import type { Hono } from "hono"
import { describeRoute } from "hono-openapi"
import { z } from "zod"
import { db } from "../../db.js"
import { jsonValidator, paramValidator, requireUserMiddleware, resolveOrganizationContextMiddleware } from "../../middleware/index.js"
import { emptyResponse, forbiddenSchema, invalidRequestSchema, jsonResponse, notFoundSchema, unauthorizedSchema } from "../../openapi.js"
import type { OrgRouteVariables } from "./shared.js"
import { idParamSchema, orgIdParamSchema, parseTemplateJson } from "./shared.js"
@@ -13,11 +15,53 @@ const createTemplateSchema = z.object({
templateData: z.unknown(),
})
const templateSchema = z.object({
id: z.string(),
organizationId: z.string(),
name: z.string(),
templateData: z.unknown(),
createdAt: z.string().datetime(),
updatedAt: z.string().datetime(),
creator: z.object({
memberId: z.string(),
userId: z.string(),
role: z.string(),
name: z.string().nullable(),
email: z.string().email().nullable(),
image: z.string().nullable().optional(),
}).passthrough(),
}).meta({ ref: "Template" })
const templateResponseSchema = z.object({
template: templateSchema,
}).meta({ ref: "TemplateResponse" })
const templateListResponseSchema = z.object({
templates: z.array(templateSchema),
}).meta({ ref: "TemplateListResponse" })
type TemplateSharingId = typeof TempTemplateSharingTable.$inferSelect.id
const orgTemplateParamsSchema = orgIdParamSchema.extend(idParamSchema("templateId").shape)
export function registerOrgTemplateRoutes<T extends { Variables: OrgRouteVariables }>(app: Hono<T>) {
app.post("/v1/orgs/:orgId/templates", requireUserMiddleware, paramValidator(orgIdParamSchema), resolveOrganizationContextMiddleware, jsonValidator(createTemplateSchema), async (c) => {
app.post(
"/v1/orgs/:orgId/templates",
describeRoute({
tags: ["Organizations", "Organization Templates"],
summary: "Create shared template",
description: "Stores a reusable shared template snapshot inside an organization.",
responses: {
201: jsonResponse("Template created successfully.", templateResponseSchema),
400: jsonResponse("The template creation request was invalid.", invalidRequestSchema),
401: jsonResponse("The caller must be signed in to create templates.", unauthorizedSchema),
404: jsonResponse("The organization could not be found.", notFoundSchema),
},
}),
requireUserMiddleware,
paramValidator(orgIdParamSchema),
resolveOrganizationContextMiddleware,
jsonValidator(createTemplateSchema),
async (c) => {
const payload = c.get("organizationContext")
const user = c.get("user")
const input = c.req.valid("json")
@@ -53,9 +97,26 @@ export function registerOrgTemplateRoutes<T extends { Variables: OrgRouteVariabl
},
},
}, 201)
})
},
)
app.get("/v1/orgs/:orgId/templates", requireUserMiddleware, paramValidator(orgIdParamSchema), resolveOrganizationContextMiddleware, async (c) => {
app.get(
"/v1/orgs/:orgId/templates",
describeRoute({
tags: ["Organizations", "Organization Templates"],
summary: "List shared templates",
description: "Lists the shared templates that belong to an organization, including creator metadata.",
responses: {
200: jsonResponse("Templates returned successfully.", templateListResponseSchema),
400: jsonResponse("The template list path parameters were invalid.", invalidRequestSchema),
401: jsonResponse("The caller must be signed in to list templates.", unauthorizedSchema),
404: jsonResponse("The organization could not be found.", notFoundSchema),
},
}),
requireUserMiddleware,
paramValidator(orgIdParamSchema),
resolveOrganizationContextMiddleware,
async (c) => {
const payload = c.get("organizationContext")
const templates = await db
@@ -103,9 +164,27 @@ export function registerOrgTemplateRoutes<T extends { Variables: OrgRouteVariabl
},
})),
})
})
},
)
app.delete("/v1/orgs/:orgId/templates/:templateId", requireUserMiddleware, paramValidator(orgTemplateParamsSchema), resolveOrganizationContextMiddleware, async (c) => {
app.delete(
"/v1/orgs/:orgId/templates/:templateId",
describeRoute({
tags: ["Organizations", "Organization Templates"],
summary: "Delete shared template",
description: "Deletes a shared template when the caller is the template creator or an organization owner.",
responses: {
204: emptyResponse("Template deleted successfully."),
400: jsonResponse("The template deletion path parameters were invalid.", invalidRequestSchema),
401: jsonResponse("The caller must be signed in to delete templates.", unauthorizedSchema),
403: jsonResponse("The caller is not allowed to delete this template.", forbiddenSchema),
404: jsonResponse("The template or organization could not be found.", notFoundSchema),
},
}),
requireUserMiddleware,
paramValidator(orgTemplateParamsSchema),
resolveOrganizationContextMiddleware,
async (c) => {
const payload = c.get("organizationContext")
const params = c.req.valid("param")
@@ -138,5 +217,6 @@ export function registerOrgTemplateRoutes<T extends { Variables: OrgRouteVariabl
await db.delete(TempTemplateSharingTable).where(eq(TempTemplateSharingTable.id, template.id))
return c.body(null, 204)
})
},
)
}

View File

@@ -1,8 +1,11 @@
import { and, eq, isNull } from "@openwork-ee/den-db/drizzle"
import { WorkerTable, WorkerTokenTable } from "@openwork-ee/den-db/schema"
import type { Hono } from "hono"
import { describeRoute } from "hono-openapi"
import { z } from "zod"
import { db } from "../../db.js"
import { jsonValidator, paramValidator } from "../../middleware/index.js"
import { invalidRequestSchema, jsonResponse, notFoundSchema, unauthorizedSchema } from "../../openapi.js"
import {
activityHeartbeatSchema,
newerDate,
@@ -13,8 +16,32 @@ import {
type WorkerRouteVariables,
} from "./shared.js"
const workerHeartbeatResponseSchema = z.object({
ok: z.literal(true),
workerId: z.string(),
isActiveRecently: z.boolean(),
openSessionCount: z.number().int().nullable(),
lastHeartbeatAt: z.string().datetime(),
lastActiveAt: z.string().datetime().nullable(),
}).meta({ ref: "WorkerHeartbeatResponse" })
export function registerWorkerActivityRoutes<T extends { Variables: WorkerRouteVariables }>(app: Hono<T>) {
app.post("/v1/workers/:id/activity-heartbeat", paramValidator(workerIdParamSchema), jsonValidator(activityHeartbeatSchema), async (c) => {
app.post(
"/v1/workers/:id/activity-heartbeat",
describeRoute({
tags: ["Workers", "Worker Activity"],
summary: "Record worker heartbeat",
description: "Accepts signed heartbeat and recent-activity updates from a worker so Den can track worker health and recent usage.",
responses: {
200: jsonResponse("Worker heartbeat accepted successfully.", workerHeartbeatResponseSchema),
400: jsonResponse("The heartbeat payload or worker path parameters were invalid.", invalidRequestSchema),
401: jsonResponse("The worker heartbeat token was missing or invalid.", unauthorizedSchema),
404: jsonResponse("The worker could not be found.", notFoundSchema),
},
}),
paramValidator(workerIdParamSchema),
jsonValidator(activityHeartbeatSchema),
async (c) => {
const params = c.req.valid("param")
const body = c.req.valid("json")
@@ -86,5 +113,6 @@ export function registerWorkerActivityRoutes<T extends { Variables: WorkerRouteV
lastHeartbeatAt: nextHeartbeatAt,
lastActiveAt: nextActiveAt,
})
})
},
)
}

View File

@@ -1,12 +1,49 @@
import type { Hono } from "hono"
import { describeRoute } from "hono-openapi"
import { z } from "zod"
import { env } from "../../env.js"
import { jsonValidator, queryValidator, requireUserMiddleware } from "../../middleware/index.js"
import { invalidRequestSchema, jsonResponse, unauthorizedSchema } from "../../openapi.js"
import { getRequiredUserEmail } from "../../user.js"
import type { WorkerRouteVariables } from "./shared.js"
import { billingQuerySchema, billingSubscriptionSchema, getWorkerBilling, setWorkerBillingSubscription, queryIncludesFlag } from "./shared.js"
const workerBillingPayloadSchema = z.object({
status: z.string(),
featureGateEnabled: z.boolean(),
productId: z.string().nullable().optional(),
benefitId: z.string().nullable().optional(),
}).passthrough()
const workerBillingResponseSchema = z.object({
billing: workerBillingPayloadSchema,
}).meta({ ref: "WorkerBillingResponse" })
const workerBillingSubscriptionResponseSchema = z.object({
subscription: z.object({}).passthrough(),
billing: workerBillingPayloadSchema,
}).meta({ ref: "WorkerBillingSubscriptionResponse" })
const userEmailRequiredSchema = z.object({
error: z.literal("user_email_required"),
}).meta({ ref: "UserEmailRequiredError" })
export function registerWorkerBillingRoutes<T extends { Variables: WorkerRouteVariables }>(app: Hono<T>) {
app.get("/v1/workers/billing", requireUserMiddleware, queryValidator(billingQuerySchema), async (c) => {
app.get(
"/v1/workers/billing",
describeRoute({
tags: ["Workers", "Worker Billing"],
summary: "Get worker billing status",
description: "Returns billing and subscription status for the signed-in user's cloud worker access.",
responses: {
200: jsonResponse("Worker billing status returned successfully.", workerBillingResponseSchema),
400: jsonResponse("The billing query parameters were invalid or the user is missing an email.", z.union([invalidRequestSchema, userEmailRequiredSchema])),
401: jsonResponse("The caller must be signed in to read billing status.", unauthorizedSchema),
},
}),
requireUserMiddleware,
queryValidator(billingQuerySchema),
async (c) => {
const user = c.get("user")
const query = c.req.valid("query")
const email = getRequiredUserEmail(user)
@@ -31,9 +68,24 @@ export function registerWorkerBillingRoutes<T extends { Variables: WorkerRouteVa
benefitId: env.polar.benefitId,
},
})
})
},
)
app.post("/v1/workers/billing/subscription", requireUserMiddleware, jsonValidator(billingSubscriptionSchema), async (c) => {
app.post(
"/v1/workers/billing/subscription",
describeRoute({
tags: ["Workers", "Worker Billing"],
summary: "Update worker subscription settings",
description: "Updates whether the user's cloud worker subscription should cancel at the end of the current billing period.",
responses: {
200: jsonResponse("Worker subscription settings updated successfully.", workerBillingSubscriptionResponseSchema),
400: jsonResponse("The subscription update payload was invalid or the user is missing an email.", z.union([invalidRequestSchema, userEmailRequiredSchema])),
401: jsonResponse("The caller must be signed in to update billing settings.", unauthorizedSchema),
},
}),
requireUserMiddleware,
jsonValidator(billingSubscriptionSchema),
async (c) => {
const user = c.get("user")
const input = c.req.valid("json")
const email = getRequiredUserEmail(user)
@@ -67,5 +119,6 @@ export function registerWorkerBillingRoutes<T extends { Variables: WorkerRouteVa
benefitId: env.polar.benefitId,
},
})
})
},
)
}

View File

@@ -2,8 +2,11 @@ import { desc, eq } from "@openwork-ee/den-db/drizzle"
import { WorkerTable, WorkerTokenTable } from "@openwork-ee/den-db/schema"
import { createDenTypeId } from "@openwork-ee/utils/typeid"
import type { Hono } from "hono"
import { describeRoute } from "hono-openapi"
import { z } from "zod"
import { db } from "../../db.js"
import { jsonValidator, paramValidator, queryValidator, requireUserMiddleware, resolveUserOrganizationsMiddleware } from "../../middleware/index.js"
import { emptyResponse, forbiddenSchema, invalidRequestSchema, jsonResponse, notFoundSchema, unauthorizedSchema } from "../../openapi.js"
import { getOrganizationLimitStatus } from "../../organization-limits.js"
import type { WorkerRouteVariables } from "./shared.js"
import {
@@ -22,8 +25,111 @@ import {
workerIdParamSchema,
} from "./shared.js"
const workerInstanceSchema = z.object({
provider: z.string(),
region: z.string().nullable(),
url: z.string().nullable(),
status: z.string(),
createdAt: z.string().datetime(),
updatedAt: z.string().datetime(),
}).nullable().meta({ ref: "WorkerInstance" })
const workerSchema = z.object({
id: z.string(),
orgId: z.string(),
createdByUserId: z.string().nullable(),
isMine: z.boolean(),
name: z.string(),
description: z.string().nullable(),
destination: z.string(),
status: z.string(),
imageVersion: z.string().nullable(),
workspacePath: z.string().nullable(),
sandboxBackend: z.string().nullable(),
lastHeartbeatAt: z.string().datetime().nullable(),
lastActiveAt: z.string().datetime().nullable(),
createdAt: z.string().datetime(),
updatedAt: z.string().datetime(),
}).meta({ ref: "Worker" })
const workerListResponseSchema = z.object({
workers: z.array(z.object({
instance: workerInstanceSchema,
}).merge(workerSchema)),
}).meta({ ref: "WorkerListResponse" })
const workerResponseSchema = z.object({
worker: workerSchema,
instance: workerInstanceSchema,
}).meta({ ref: "WorkerResponse" })
const workerCreateResponseSchema = z.object({
worker: workerSchema,
tokens: z.object({
owner: z.string(),
host: z.string(),
client: z.string(),
}),
instance: workerInstanceSchema,
launch: z.object({
mode: z.string(),
pollAfterMs: z.number().int(),
}),
}).meta({ ref: "WorkerCreateResponse" })
const workerTokensResponseSchema = z.object({
tokens: z.object({
owner: z.string(),
host: z.string(),
client: z.string(),
}),
connect: z.object({
openworkUrl: z.string().nullable(),
workspaceId: z.string().nullable(),
}).nullable(),
}).meta({ ref: "WorkerTokensResponse" })
const organizationUnavailableSchema = z.object({
error: z.literal("organization_unavailable"),
}).meta({ ref: "OrganizationUnavailableError" })
const workspacePathRequiredSchema = z.object({
error: z.literal("workspace_path_required"),
}).meta({ ref: "WorkspacePathRequiredError" })
const orgLimitReachedSchema = z.object({
error: z.literal("org_limit_reached"),
limitType: z.literal("workers"),
limit: z.number().int(),
currentCount: z.number().int(),
message: z.string(),
}).meta({ ref: "WorkerOrgLimitReachedError" })
const workerRuntimeUnavailableSchema = z.object({
error: z.literal("worker_tokens_unavailable"),
message: z.string(),
}).or(z.object({
error: z.literal("worker_runtime_unavailable"),
message: z.string(),
})).meta({ ref: "WorkerConnectionError" })
export function registerWorkerCoreRoutes<T extends { Variables: WorkerRouteVariables }>(app: Hono<T>) {
app.get("/v1/workers", requireUserMiddleware, resolveUserOrganizationsMiddleware, queryValidator(listWorkersQuerySchema), async (c) => {
app.get(
"/v1/workers",
describeRoute({
tags: ["Workers"],
summary: "List workers",
description: "Lists the workers that belong to the caller's active organization, including each worker's latest known instance state.",
responses: {
200: jsonResponse("Workers returned successfully.", workerListResponseSchema),
400: jsonResponse("The worker list query parameters were invalid.", invalidRequestSchema),
401: jsonResponse("The caller must be signed in to list workers.", unauthorizedSchema),
},
}),
requireUserMiddleware,
resolveUserOrganizationsMiddleware,
queryValidator(listWorkersQuerySchema),
async (c) => {
const user = c.get("user")
const orgId = c.get("activeOrganizationId")
const query = c.req.valid("query")
@@ -50,9 +156,27 @@ export function registerWorkerCoreRoutes<T extends { Variables: WorkerRouteVaria
)
return c.json({ workers })
})
},
)
app.post("/v1/workers", requireUserMiddleware, resolveUserOrganizationsMiddleware, jsonValidator(createWorkerSchema), async (c) => {
app.post(
"/v1/workers",
describeRoute({
tags: ["Workers"],
summary: "Create worker",
description: "Creates a local or cloud worker for the active organization and returns the initial tokens needed to connect to it.",
responses: {
201: jsonResponse("Local worker created successfully.", workerCreateResponseSchema),
202: jsonResponse("Cloud worker creation started successfully.", workerCreateResponseSchema),
400: jsonResponse("The worker creation payload was invalid.", z.union([invalidRequestSchema, organizationUnavailableSchema, workspacePathRequiredSchema])),
401: jsonResponse("The caller must be signed in to create workers.", unauthorizedSchema),
409: jsonResponse("The organization has reached its worker limit.", orgLimitReachedSchema),
},
}),
requireUserMiddleware,
resolveUserOrganizationsMiddleware,
jsonValidator(createWorkerSchema),
async (c) => {
const user = c.get("user")
const orgId = c.get("activeOrganizationId")
const input = c.req.valid("json")
@@ -156,9 +280,26 @@ export function registerWorkerCoreRoutes<T extends { Variables: WorkerRouteVaria
instance: null,
launch: input.destination === "cloud" ? { mode: "async", pollAfterMs: 5000 } : { mode: "instant", pollAfterMs: 0 },
}, input.destination === "cloud" ? 202 : 201)
})
},
)
app.get("/v1/workers/:id", requireUserMiddleware, resolveUserOrganizationsMiddleware, paramValidator(workerIdParamSchema), async (c) => {
app.get(
"/v1/workers/:id",
describeRoute({
tags: ["Workers"],
summary: "Get worker",
description: "Returns one worker from the active organization together with its latest provisioned instance details.",
responses: {
200: jsonResponse("Worker returned successfully.", workerResponseSchema),
400: jsonResponse("The worker path parameters were invalid.", invalidRequestSchema),
401: jsonResponse("The caller must be signed in to read worker details.", unauthorizedSchema),
404: jsonResponse("The worker could not be found.", notFoundSchema),
},
}),
requireUserMiddleware,
resolveUserOrganizationsMiddleware,
paramValidator(workerIdParamSchema),
async (c) => {
const user = c.get("user")
const orgId = c.get("activeOrganizationId")
const params = c.req.valid("param")
@@ -185,9 +326,28 @@ export function registerWorkerCoreRoutes<T extends { Variables: WorkerRouteVaria
worker: toWorkerResponse(worker, user.id),
instance: toInstanceResponse(instance),
})
})
},
)
app.patch("/v1/workers/:id", requireUserMiddleware, resolveUserOrganizationsMiddleware, paramValidator(workerIdParamSchema), jsonValidator(updateWorkerSchema), async (c) => {
app.patch(
"/v1/workers/:id",
describeRoute({
tags: ["Workers"],
summary: "Update worker",
description: "Renames a worker, but only when the caller is the user who originally created that worker.",
responses: {
200: jsonResponse("Worker updated successfully.", z.object({ worker: workerSchema }).meta({ ref: "WorkerUpdateResponse" })),
400: jsonResponse("The worker update request was invalid.", invalidRequestSchema),
401: jsonResponse("The caller must be signed in to update workers.", unauthorizedSchema),
403: jsonResponse("Only the worker creator can rename this worker.", forbiddenSchema),
404: jsonResponse("The worker could not be found.", notFoundSchema),
},
}),
requireUserMiddleware,
resolveUserOrganizationsMiddleware,
paramValidator(workerIdParamSchema),
jsonValidator(updateWorkerSchema),
async (c) => {
const user = c.get("user")
const orgId = c.get("activeOrganizationId")
const params = c.req.valid("param")
@@ -228,9 +388,27 @@ export function registerWorkerCoreRoutes<T extends { Variables: WorkerRouteVaria
user.id,
),
})
})
},
)
app.post("/v1/workers/:id/tokens", requireUserMiddleware, resolveUserOrganizationsMiddleware, paramValidator(workerIdParamSchema), async (c) => {
app.post(
"/v1/workers/:id/tokens",
describeRoute({
tags: ["Workers"],
summary: "Get worker connection tokens",
description: "Returns connection tokens and the resolved OpenWork connect URL for an existing worker.",
responses: {
200: jsonResponse("Worker connection tokens returned successfully.", workerTokensResponseSchema),
400: jsonResponse("The worker token path parameters were invalid.", invalidRequestSchema),
401: jsonResponse("The caller must be signed in to request worker tokens.", unauthorizedSchema),
404: jsonResponse("The worker could not be found.", notFoundSchema),
409: jsonResponse("The worker is not ready to return connection tokens yet.", workerRuntimeUnavailableSchema),
},
}),
requireUserMiddleware,
resolveUserOrganizationsMiddleware,
paramValidator(workerIdParamSchema),
async (c) => {
const orgId = c.get("activeOrganizationId")
const params = c.req.valid("param")
@@ -261,9 +439,26 @@ export function registerWorkerCoreRoutes<T extends { Variables: WorkerRouteVaria
}
return c.json(resolved)
})
},
)
app.delete("/v1/workers/:id", requireUserMiddleware, resolveUserOrganizationsMiddleware, paramValidator(workerIdParamSchema), async (c) => {
app.delete(
"/v1/workers/:id",
describeRoute({
tags: ["Workers"],
summary: "Delete worker",
description: "Deletes a worker and cascades cleanup for its tokens, runtime records, and provider-specific resources.",
responses: {
204: emptyResponse("Worker deleted successfully."),
400: jsonResponse("The worker deletion path parameters were invalid.", invalidRequestSchema),
401: jsonResponse("The caller must be signed in to delete workers.", unauthorizedSchema),
404: jsonResponse("The worker could not be found.", notFoundSchema),
},
}),
requireUserMiddleware,
resolveUserOrganizationsMiddleware,
paramValidator(workerIdParamSchema),
async (c) => {
const orgId = c.get("activeOrganizationId")
const params = c.req.valid("param")
@@ -285,5 +480,6 @@ export function registerWorkerCoreRoutes<T extends { Variables: WorkerRouteVaria
await deleteWorkerCascade(worker)
return c.body(null, 204)
})
},
)
}

View File

@@ -1,11 +1,31 @@
import type { Hono } from "hono"
import { describeRoute } from "hono-openapi"
import { z } from "zod"
import { jsonValidator, paramValidator, requireUserMiddleware, resolveUserOrganizationsMiddleware } from "../../middleware/index.js"
import { invalidRequestSchema, jsonResponse, notFoundSchema, unauthorizedSchema } from "../../openapi.js"
import type { WorkerRouteVariables } from "./shared.js"
import { fetchWorkerRuntimeJson, getWorkerByIdForOrg, parseWorkerIdParam, workerIdParamSchema } from "./shared.js"
const workerRuntimeResponseSchema = z.object({}).passthrough().meta({ ref: "WorkerRuntimeResponse" })
export function registerWorkerRuntimeRoutes<T extends { Variables: WorkerRouteVariables }>(app: Hono<T>) {
app.get("/v1/workers/:id/runtime", requireUserMiddleware, resolveUserOrganizationsMiddleware, paramValidator(workerIdParamSchema), async (c) => {
app.get(
"/v1/workers/:id/runtime",
describeRoute({
tags: ["Workers", "Worker Runtime"],
summary: "Get worker runtime status",
description: "Fetches runtime version and status information from a specific worker's runtime endpoint.",
responses: {
200: jsonResponse("Worker runtime information returned successfully.", workerRuntimeResponseSchema),
400: jsonResponse("The worker runtime path parameters were invalid.", invalidRequestSchema),
401: jsonResponse("The caller must be signed in to read worker runtime information.", unauthorizedSchema),
404: jsonResponse("The worker could not be found.", notFoundSchema),
},
}),
requireUserMiddleware,
resolveUserOrganizationsMiddleware,
paramValidator(workerIdParamSchema),
async (c) => {
const orgId = c.get("activeOrganizationId")
const params = c.req.valid("param")
@@ -36,9 +56,27 @@ export function registerWorkerRuntimeRoutes<T extends { Variables: WorkerRouteVa
"Content-Type": "application/json",
},
})
})
},
)
app.post("/v1/workers/:id/runtime/upgrade", requireUserMiddleware, resolveUserOrganizationsMiddleware, paramValidator(workerIdParamSchema), jsonValidator(z.object({}).passthrough()), async (c) => {
app.post(
"/v1/workers/:id/runtime/upgrade",
describeRoute({
tags: ["Workers", "Worker Runtime"],
summary: "Upgrade worker runtime",
description: "Forwards a runtime upgrade request to a specific worker and returns the worker runtime's response.",
responses: {
200: jsonResponse("Worker runtime upgrade request completed successfully.", workerRuntimeResponseSchema),
400: jsonResponse("The runtime upgrade request was invalid.", invalidRequestSchema),
401: jsonResponse("The caller must be signed in to upgrade a worker runtime.", unauthorizedSchema),
404: jsonResponse("The worker could not be found.", notFoundSchema),
},
}),
requireUserMiddleware,
resolveUserOrganizationsMiddleware,
paramValidator(workerIdParamSchema),
jsonValidator(z.object({}).passthrough()),
async (c) => {
const orgId = c.get("activeOrganizationId")
const params = c.req.valid("param")
const body = c.req.valid("json")
@@ -72,5 +110,6 @@ export function registerWorkerRuntimeRoutes<T extends { Variables: WorkerRouteVa
"Content-Type": "application/json",
},
})
})
},
)
}

163
pnpm-lock.yaml generated
View File

@@ -434,15 +434,27 @@ importers:
'@hono/node-server':
specifier: ^1.13.8
version: 1.19.11(hono@4.12.8)
'@hono/zod-validator':
specifier: ^0.7.6
version: 0.7.6(hono@4.12.8)(zod@4.3.6)
'@hono/standard-validator':
specifier: ^0.2.2
version: 0.2.2(@standard-schema/spec@1.1.0)(hono@4.12.8)
'@hono/swagger-ui':
specifier: ^0.6.1
version: 0.6.1(hono@4.12.8)
'@openwork-ee/den-db':
specifier: workspace:*
version: link:../../packages/den-db
'@openwork-ee/utils':
specifier: workspace:*
version: link:../../packages/utils
'@standard-community/standard-json':
specifier: ^0.3.5
version: 0.3.5(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(quansync@0.2.11)(zod@4.3.6)
'@standard-community/standard-openapi':
specifier: ^0.2.9
version: 0.2.9(@standard-community/standard-json@0.3.5(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(quansync@0.2.11)(zod@4.3.6))(@standard-schema/spec@1.1.0)(openapi-types@12.1.3)(zod@4.3.6)
'@standard-schema/spec':
specifier: ^1.1.0
version: 1.1.0
better-auth:
specifier: ^1.5.6
version: 1.5.6(@opentelemetry/api@1.9.0)(drizzle-kit@0.31.9)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.6)(kysely@0.28.15)(mysql2@3.17.4))(mysql2@3.17.4)(next@16.2.1(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(solid-js@1.9.10)
@@ -455,10 +467,19 @@ importers:
hono:
specifier: ^4.7.2
version: 4.12.8
hono-openapi:
specifier: ^1.3.0
version: 1.3.0(@hono/standard-validator@0.2.2(@standard-schema/spec@1.1.0)(hono@4.12.8))(@standard-community/standard-json@0.3.5(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(quansync@0.2.11)(zod@4.3.6))(@standard-community/standard-openapi@0.2.9(@standard-community/standard-json@0.3.5(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(quansync@0.2.11)(zod@4.3.6))(@standard-schema/spec@1.1.0)(openapi-types@12.1.3)(zod@4.3.6))(@types/json-schema@7.0.15)(hono@4.12.8)(openapi-types@12.1.3)
openapi-types:
specifier: ^12.1.3
version: 12.1.3
zod:
specifier: ^4.3.6
version: 4.3.6
devDependencies:
'@types/json-schema':
specifier: ^7.0.15
version: 7.0.15
'@types/node':
specifier: ^20.11.30
version: 20.12.12
@@ -1663,11 +1684,16 @@ packages:
peerDependencies:
hono: ^4
'@hono/zod-validator@0.7.6':
resolution: {integrity: sha512-Io1B6d011Gj1KknV4rXYz4le5+5EubcWEU/speUjuw9XMMIaP3n78yXLhjd2A3PXaXaUwEAluOiAyLqhBEJgsw==}
'@hono/standard-validator@0.2.2':
resolution: {integrity: sha512-mJ7W84Bt/rSvoIl63Ynew+UZOHAzzRAoAXb3JaWuxAkM/Lzg+ZHTCUiz77KOtn2e623WNN8LkD57Dk0szqUrIw==}
peerDependencies:
'@standard-schema/spec': ^1.0.0
hono: '>=3.9.0'
zod: ^3.25.0 || ^4.0.0
'@hono/swagger-ui@0.6.1':
resolution: {integrity: sha512-sJTvldu1GPeEPfyeLG7gRj+W4vEuD+JDi+JjJ3TJs/DvMUtBLs0KJO5yokGegWWdy5qrbdnQGekbhgNRmPmYKQ==}
peerDependencies:
hono: '>=4.0.0'
'@iarna/toml@2.2.5':
resolution: {integrity: sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg==}
@@ -2965,6 +2991,67 @@ packages:
peerDependencies:
solid-js: ^1.8.6
'@standard-community/standard-json@0.3.5':
resolution: {integrity: sha512-4+ZPorwDRt47i+O7RjyuaxHRK/37QY/LmgxlGrRrSTLYoFatEOzvqIc85GTlM18SFZ5E91C+v0o/M37wZPpUHA==}
peerDependencies:
'@standard-schema/spec': ^1.0.0
'@types/json-schema': ^7.0.15
'@valibot/to-json-schema': ^1.3.0
arktype: ^2.1.20
effect: ^3.16.8
quansync: ^0.2.11
sury: ^10.0.0
typebox: ^1.0.17
valibot: ^1.1.0
zod: ^3.25.0 || ^4.0.0
zod-to-json-schema: ^3.24.5
peerDependenciesMeta:
'@valibot/to-json-schema':
optional: true
arktype:
optional: true
effect:
optional: true
sury:
optional: true
typebox:
optional: true
valibot:
optional: true
zod:
optional: true
zod-to-json-schema:
optional: true
'@standard-community/standard-openapi@0.2.9':
resolution: {integrity: sha512-htj+yldvN1XncyZi4rehbf9kLbu8os2Ke/rfqoZHCMHuw34kiF3LP/yQPdA0tQ940y8nDq3Iou8R3wG+AGGyvg==}
peerDependencies:
'@standard-community/standard-json': ^0.3.5
'@standard-schema/spec': ^1.0.0
arktype: ^2.1.20
effect: ^3.17.14
openapi-types: ^12.1.3
sury: ^10.0.0
typebox: ^1.0.0
valibot: ^1.1.0
zod: ^3.25.0 || ^4.0.0
zod-openapi: ^4
peerDependenciesMeta:
arktype:
optional: true
effect:
optional: true
sury:
optional: true
typebox:
optional: true
valibot:
optional: true
zod:
optional: true
zod-openapi:
optional: true
'@standard-schema/spec@1.1.0':
resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
@@ -3328,6 +3415,9 @@ packages:
'@types/hast@3.0.4':
resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==}
'@types/json-schema@7.0.15':
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
'@types/mdast@4.0.4':
resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==}
@@ -4373,6 +4463,21 @@ packages:
resolution: {integrity: sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==}
engines: {node: '>=0.10.0'}
hono-openapi@1.3.0:
resolution: {integrity: sha512-xDvCWpWEIv0weEmnl3EjRQzqbHIO8LnfzMuYOCmbuyE5aes6aXxLg4vM3ybnoZD5TiTUkA6PuRQPJs3R7WRBig==}
peerDependencies:
'@hono/standard-validator': ^0.2.0
'@standard-community/standard-json': ^0.3.5
'@standard-community/standard-openapi': ^0.2.9
'@types/json-schema': ^7.0.15
hono: ^4.8.3
openapi-types: ^12.1.3
peerDependenciesMeta:
'@hono/standard-validator':
optional: true
hono:
optional: true
hono@4.12.8:
resolution: {integrity: sha512-VJCEvtrezO1IAR+kqEYnxUOoStaQPGrCmX3j4wDTNOcD1uRPFpGlwQUIW8niPuvHXaTUxeOUl5MMDGrl+tmO9A==}
engines: {node: '>=16.9.0'}
@@ -4999,6 +5104,9 @@ packages:
resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==}
engines: {node: '>=14.0.0'}
openapi-types@12.1.3:
resolution: {integrity: sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==}
p-finally@1.0.0:
resolution: {integrity: sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==}
engines: {node: '>=4'}
@@ -5231,6 +5339,9 @@ packages:
proxy-from-env@1.1.0:
resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
quansync@0.2.11:
resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==}
queue-microtask@1.2.3:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
@@ -7221,10 +7332,14 @@ snapshots:
dependencies:
hono: 4.12.8
'@hono/zod-validator@0.7.6(hono@4.12.8)(zod@4.3.6)':
'@hono/standard-validator@0.2.2(@standard-schema/spec@1.1.0)(hono@4.12.8)':
dependencies:
'@standard-schema/spec': 1.1.0
hono: 4.12.8
'@hono/swagger-ui@0.6.1(hono@4.12.8)':
dependencies:
hono: 4.12.8
zod: 4.3.6
'@iarna/toml@2.2.5': {}
@@ -8692,6 +8807,22 @@ snapshots:
dependencies:
solid-js: 1.9.9
'@standard-community/standard-json@0.3.5(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(quansync@0.2.11)(zod@4.3.6)':
dependencies:
'@standard-schema/spec': 1.1.0
'@types/json-schema': 7.0.15
quansync: 0.2.11
optionalDependencies:
zod: 4.3.6
'@standard-community/standard-openapi@0.2.9(@standard-community/standard-json@0.3.5(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(quansync@0.2.11)(zod@4.3.6))(@standard-schema/spec@1.1.0)(openapi-types@12.1.3)(zod@4.3.6)':
dependencies:
'@standard-community/standard-json': 0.3.5(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(quansync@0.2.11)(zod@4.3.6)
'@standard-schema/spec': 1.1.0
openapi-types: 12.1.3
optionalDependencies:
zod: 4.3.6
'@standard-schema/spec@1.1.0': {}
'@swc/counter@0.1.3': {}
@@ -9034,6 +9165,8 @@ snapshots:
dependencies:
'@types/unist': 3.0.3
'@types/json-schema@7.0.15': {}
'@types/mdast@4.0.4':
dependencies:
'@types/unist': 3.0.3
@@ -10107,6 +10240,16 @@ snapshots:
dependencies:
parse-passwd: 1.0.0
hono-openapi@1.3.0(@hono/standard-validator@0.2.2(@standard-schema/spec@1.1.0)(hono@4.12.8))(@standard-community/standard-json@0.3.5(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(quansync@0.2.11)(zod@4.3.6))(@standard-community/standard-openapi@0.2.9(@standard-community/standard-json@0.3.5(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(quansync@0.2.11)(zod@4.3.6))(@standard-schema/spec@1.1.0)(openapi-types@12.1.3)(zod@4.3.6))(@types/json-schema@7.0.15)(hono@4.12.8)(openapi-types@12.1.3):
dependencies:
'@standard-community/standard-json': 0.3.5(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(quansync@0.2.11)(zod@4.3.6)
'@standard-community/standard-openapi': 0.2.9(@standard-community/standard-json@0.3.5(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(quansync@0.2.11)(zod@4.3.6))(@standard-schema/spec@1.1.0)(openapi-types@12.1.3)(zod@4.3.6)
'@types/json-schema': 7.0.15
openapi-types: 12.1.3
optionalDependencies:
'@hono/standard-validator': 0.2.2(@standard-schema/spec@1.1.0)(hono@4.12.8)
hono: 4.12.8
hono@4.12.8: {}
html-entities@2.3.3: {}
@@ -10911,6 +11054,8 @@ snapshots:
on-exit-leak-free@2.1.2: {}
openapi-types@12.1.3: {}
p-finally@1.0.0: {}
p-limit@2.3.0:
@@ -11132,6 +11277,8 @@ snapshots:
proxy-from-env@1.1.0: {}
quansync@0.2.11: {}
queue-microtask@1.2.3: {}
quick-format-unescaped@4.0.4: {}