diff --git a/ee/apps/den-api/package.json b/ee/apps/den-api/package.json index 11e4c8944..7f1a0fe8e 100644 --- a/ee/apps/den-api/package.json +++ b/ee/apps/den-api/package.json @@ -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" diff --git a/ee/apps/den-api/src/app.ts b/ee/apps/den-api/src/app.ts index b72dcd20d..200b3ac23 100644 --- a/ee/apps/den-api/src/app.ts +++ b/ee/apps/den-api/src/app.ts @@ -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 & Partial & Partial +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) }) diff --git a/ee/apps/den-api/src/middleware/validation.ts b/ee/apps/den-api/src/middleware/validation.ts index 8f419029a..4c3e53119 100644 --- a/ee/apps/den-api/src/middleware/validation.ts +++ b/ee/apps/den-api/src/middleware/validation.ts @@ -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, ) diff --git a/ee/apps/den-api/src/openapi.ts b/ee/apps/den-api/src/openapi.ts new file mode 100644 index 000000000..b5330d554 --- /dev/null +++ b/ee/apps/den-api/src/openapi.ts @@ -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 } +} diff --git a/ee/apps/den-api/src/routes/admin/index.ts b/ee/apps/den-api/src/routes/admin/index.ts index 6181e1bf4..f58ceb028 100644 --- a/ee/apps/den-api/src/routes/admin/index.ts +++ b/ee/apps/den-api/src/routes/admin/index.ts @@ -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(items: T[], limit: number, mapper: (item } export function registerAdminRoutes(app: Hono) { - 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(app: Hono) { - 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 { + }, + ) + + 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(app: Hono) { - 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) } diff --git a/ee/apps/den-api/src/routes/me/index.ts b/ee/apps/den-api/src/routes/me/index.ts index 58947ecee..314990c7d 100644 --- a/ee/apps/den-api/src/routes/me/index.ts +++ b/ee/apps/den-api/src/routes/me/index.ts @@ -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 }>(app: Hono) { - 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 return c.json({ @@ -21,5 +62,6 @@ export function registerMeRoutes process.env.NODE_ENV === "production" export function registerOrgApiKeyRoutes(app: Hono) { 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(app: Hono) { - 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 { + 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 { + 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(app: Hono) { - 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 { + }, + ) + + 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 }>(app: Hono) { 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(app: Hono) { - 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 { + }, + ) + + 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(app: Hono) { - 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 { + }, + ) + + 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 { + }, + ) + + 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 }>(app: Hono) { 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(app: Hono) { 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(app: Hono) { - 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 { + 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 { + 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(app: Hono) { - 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(app: Hono) { - 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 { + 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(app: Hono) { - 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 { + }, + ) + + 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 { + }, + ) + + 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 { + }, + ) + + 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 { + }, + ) + + 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 { + }, + ) + + 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(app: Hono) { - 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 { + 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=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: {}