import type { FastifyInstance, FastifyReply } from "fastify"; import { z } from "zod"; import type { WorkspaceRegistryConfig } from "../workspaces/types.js"; import { WorkspaceRegistryError } from "../workspaces/git-repository.js"; import type { WorkspaceRegistry } from "../workspaces/registry.js"; import { buildInstallationContract } from "../workspaces/contracts.js"; import { discoverWorkspaceSecretRequirements, resolveRuntimeBindingsWithWorkspaceSecrets, } from "../workspaces/secret-requirements.js"; import type { WorkspaceSecretStore } from "../workspaces/secret-store.js"; import { validateOperationalWorkspace, validateWorkspaceDescriptor, type CanonicalWorkspace, type WorkspaceDescriptor, } from "../workspaces/schema.js"; import type { RuntimeBindings } from "../workspaces/runtime-renderer.js"; import type { ConnectorDiagnostics, Diagnostic, WorkspaceDiagnosticOptions, } from "../workspaces/diagnostics.js"; import { isPrincipalContext, requirePermission } from "../auth/authorization.js"; import type { AuthDiagnoser } from "../auth/diagnostics.js"; import { decodeAuthDiagnostics, type AuthDiagnostics } from "../auth/group-catalog.js"; import type { WorkspaceDatabase } from "../catalog/types.js"; export type WorkspaceDiagnoser = ( workspace: WorkspaceDescriptor, bindings: RuntimeBindings, options: WorkspaceDiagnosticOptions, ) => Promise; export type WorkspaceDatabaseTester = ( workspaceId: string, ) => Promise; interface WorkspaceRoutesDeps { registry: WorkspaceRegistry; config: WorkspaceRegistryConfig; diagnose: WorkspaceDiagnoser; authDiagnoser: AuthDiagnoser; secretStore: WorkspaceSecretStore; testDatabaseConnection: WorkspaceDatabaseTester; } const workspaceId = z.string().regex(/^[a-z][a-z0-9-]{2,62}$/); const workspacePayload = z.object({ workspace: z.unknown() }).strict(); const secretRequirementId = z.string().regex(/^[a-z0-9][a-z0-9._-]{1,127}$/); const secretValuesPayload = z.object({ values: z.record(secretRequirementId, z.string()).refine( (values) => Object.keys(values).length > 0 && Object.keys(values).length <= 16, ), }).strict(); const SAFE_MESSAGES = { workspace_invalid: "Workspace request is invalid.", binding_missing: "Installation binding is missing or invalid.", workspace_not_activatable: "Workspace cannot be activated on this installation.", workspace_stale: "Workspace repository state is stale.", git_unavailable: "Workspace Git service is unavailable.", git_auth_failed: "Workspace Git authentication failed.", git_non_fast_forward: "Workspace Git branch has changed.", connector_unavailable: "Workspace connector is unavailable.", semantic_index_incompatible: "Semantic index is incompatible with this workspace.", } as const; const catalogConnectionUnavailable = (): Diagnostic => ({ level: "error", code: "connector_unavailable", field: "dwh", message: "The configured database could not be reached or authenticated.", }); const catalogConnectionMissing = (): Diagnostic => ({ level: "error", code: "binding_missing", field: "dwh", message: "Configure this workspace in Database Management before testing connections.", }); function authenticationReport(value: unknown): AuthDiagnostics { const report = decodeAuthDiagnostics(value); if (!report) throw new Error("invalid authentication diagnostic report"); return report; } function workspaceErrorCode(error: unknown): keyof typeof SAFE_MESSAGES { return error instanceof WorkspaceRegistryError ? error.code : "workspace_invalid"; } function workspaceErrorStatus(code: keyof typeof SAFE_MESSAGES): number { if (code === "workspace_stale" || code === "git_non_fast_forward") return 409; if (code === "git_unavailable" || code === "git_auth_failed") return 503; return 400; } function errorReply(reply: FastifyReply, error: unknown) { const code = workspaceErrorCode(error); return reply.code(workspaceErrorStatus(code)).send({ code, message: SAFE_MESSAGES[code] }); } export function workspaceRoutes(app: FastifyInstance, deps: WorkspaceRoutesDeps): void { const runtimeConfiguration = async (id: string) => { const { workspace, revision } = await deps.registry.read(id); const operational = validateOperationalWorkspace(workspace); const requirements = discoverWorkspaceSecretRequirements(operational, process.env) .map((requirement) => ({ id: requirement.id, connector: requirement.connector, label: requirement.label, description: requirement.description, input: requirement.input, required: requirement.required, configured: deps.secretStore.has(id, requirement.id), })); return { workspaceId: id, revision, configurationState: requirements.some(({ required, configured }) => required && !configured) ? "configuration_required" as const : "ready" as const, requirements, }; }; app.get("/workspace-registry/status", async (request, reply) => { if (!isPrincipalContext(requirePermission(request, reply, "workspace.manage"))) return reply; try { return await deps.registry.bootstrap(); } catch (error) { return errorReply(reply, error); } }); app.post("/workspace-registry/pull", async (request, reply) => { if (!isPrincipalContext(requirePermission(request, reply, "workspace.manage"))) return reply; try { return await deps.registry.pull(); } catch (error) { return errorReply(reply, error); } }); app.get("/workspaces", async (request, reply) => { if (!isPrincipalContext(requirePermission(request, reply, "session.use"))) return reply; try { const records = await deps.registry.listCatalog(); return await Promise.all(records.map(async (record) => { const configuration = await runtimeConfiguration(record.id); return { id: record.id, // Retain the metadata endpoint's selector field while adding catalog metadata. name: record.id, file: `${record.id}/workspace.yaml`, displayName: record.name, description: record.description, configurationState: configuration.configurationState, ...(record.revision ? { revision: record.revision } : {}), }; })); } catch (error) { return errorReply(reply, error); } }); app.get("/workspaces/:id", async (request, reply) => { if (!isPrincipalContext(requirePermission(request, reply, "session.use"))) return reply; try { const { id } = z.object({ id: workspaceId }).parse(request.params); return await deps.registry.read(id); } catch (error) { return errorReply(reply, error); } }); app.post("/workspaces/validate", async (request, reply) => { if (!isPrincipalContext(requirePermission(request, reply, "workspace.manage"))) return reply; try { const { workspace } = workspacePayload.parse(request.body); const canonical = validateWorkspaceDescriptor(workspace); const authentication = authenticationReport(await deps.authDiagnoser.inspect({ live: false })); return { workspace: canonical, contract: buildInstallationContract(canonical), activatable: authentication.ready, diagnostics: [], authentication, }; } catch (error) { return errorReply(reply, error); } }); app.get("/workspaces/:id/runtime-configuration", async (request, reply) => { if (!isPrincipalContext(requirePermission(request, reply, "session.use"))) return reply; try { const { id } = z.object({ id: workspaceId }).parse(request.params); return await runtimeConfiguration(id); } catch (error) { return errorReply(reply, error); } }); app.put("/workspaces/:id/secrets", async (request, reply) => { if (!isPrincipalContext(requirePermission(request, reply, "workspace.secrets.manage"))) return reply; try { const { id } = z.object({ id: workspaceId }).parse(request.params); const { values } = secretValuesPayload.parse(request.body); const { workspace } = await deps.registry.read(id); const declared = new Set( discoverWorkspaceSecretRequirements(validateOperationalWorkspace(workspace), process.env) .map(({ id: requirementId }) => requirementId), ); if (Object.keys(values).some((requirementId) => !declared.has(requirementId))) { throw new Error("undeclared workspace secret"); } deps.secretStore.putMany(id, values); return await runtimeConfiguration(id); } catch (error) { return errorReply(reply, error); } }); app.delete("/workspaces/:id/secrets/:requirementId", async (request, reply) => { if (!isPrincipalContext(requirePermission(request, reply, "workspace.secrets.manage"))) return reply; try { const { id, requirementId } = z.object({ id: workspaceId, requirementId: secretRequirementId, }).parse(request.params); const { workspace } = await deps.registry.read(id); const declared = discoverWorkspaceSecretRequirements( validateOperationalWorkspace(workspace), process.env, ).some(({ id: candidate }) => candidate === requirementId); if (!declared) throw new Error("undeclared workspace secret"); deps.secretStore.forget(id, requirementId); return await runtimeConfiguration(id); } catch (error) { return errorReply(reply, error); } }); app.post("/workspaces/:id/test", async (request, reply) => { if (!isPrincipalContext(requirePermission(request, reply, "workspace.manage"))) return reply; try { const { id } = z.object({ id: workspaceId }).parse(request.params); const { workspace } = await deps.registry.read(id); let operational: CanonicalWorkspace; try { operational = validateOperationalWorkspace(workspace); } catch { throw new WorkspaceRegistryError( "workspace_not_activatable", "Workspace requires explicit migration", ); } const lease = resolveRuntimeBindingsWithWorkspaceSecrets( operational, process.env, deps.config.secretRoots, deps.secretStore, ); try { const [workspaceDiagnostics, testedDatabase, inspectedAuthentication] = await Promise.all([ deps.diagnose(operational, lease.bindings, { writeProbe: false, skipDwh: true, }), deps.testDatabaseConnection(id), deps.authDiagnoser.inspect({ live: true }), ]); const authentication = authenticationReport(inspectedAuthentication); const catalogConnectionReady = testedDatabase?.connectionStatus === "reachable"; const catalogConnectionDiagnostic = !testedDatabase ? catalogConnectionMissing() : catalogConnectionReady ? undefined : catalogConnectionUnavailable(); const diagnostics = catalogConnectionDiagnostic ? [ ...workspaceDiagnostics.diagnostics.filter(({ code }) => code !== "binding_ok"), catalogConnectionDiagnostic, ] : workspaceDiagnostics.diagnostics; return { ...workspaceDiagnostics, activatable: workspaceDiagnostics.activatable && catalogConnectionReady && authentication.ready, diagnostics, authentication, }; } finally { lease.release(); } } catch (error) { return errorReply(reply, error); } }); }