mirror of
https://github.com/different-ai/openwork
synced 2026-04-25 17:15:34 +02:00
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:
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
102
ee/apps/den-api/src/openapi.ts
Normal file
102
ee/apps/den-api/src/openapi.ts
Normal 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 }
|
||||
}
|
||||
@@ -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(),
|
||||
})
|
||||
})
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
})
|
||||
})
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
})
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 })
|
||||
})
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
})
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
})
|
||||
})
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
})
|
||||
})
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user