import { createHash } from "node:crypto"; import { Buffer } from "node:buffer"; import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify"; import multipart from "@fastify/multipart"; import yauzl from "yauzl"; import yazl from "yazl"; import { z } from "zod"; import type { WorkspaceRegistryConfig } from "../workspaces/types.js"; import { WorkspaceRegistryError } from "../workspaces/git-repository.js"; import { WorkspaceConflictError, type PublishWorkspaceRequest, type WorkspaceRegistry, } from "../workspaces/registry.js"; import { resolveRuntimeBindings } from "../workspaces/bindings.js"; import { buildInstallationContract, renderWorkspaceDocs } from "../workspaces/contracts.js"; import { parseWorkspaceYaml, serializeWorkspaceYaml, validateOperationalWorkspace, validateWorkspaceDescriptor, type CanonicalWorkspace, type WorkspaceDescriptor, } from "../workspaces/schema.js"; import type { RuntimeBindings } from "../workspaces/runtime-renderer.js"; import type { WorkspaceDiagnostics } from "../workspaces/diagnostics.js"; export type WorkspaceDiagnoser = ( workspace: WorkspaceDescriptor, bindings: RuntimeBindings, options: { writeProbe: boolean }, ) => Promise; interface WorkspaceRoutesDeps { registry: WorkspaceRegistry; config: WorkspaceRegistryConfig; diagnose: WorkspaceDiagnoser; } const workspaceId = z.string().regex(/^[a-z][a-z0-9-]{2,62}$/); const commit = z.string().regex(/^[0-9a-f]{40}$/); const workspacePayload = z.object({ workspace: z.unknown() }).strict(); const publishPayload = z.discriminatedUnion("action", [ z.object({ action: z.literal("create"), workspace: z.unknown(), baseCommit: commit }).strict(), z.object({ action: z.literal("update"), workspace: z.unknown(), baseCommit: commit, baseBlob: commit }).strict(), z.object({ action: z.literal("delete"), id: workspaceId, baseCommit: commit, baseBlob: commit }).strict(), ]); const bundleManifest = z.object({ schema_version: z.literal(1), workspace_id: workspaceId, files: z.object({ "workspace.yaml": z.string().regex(/^[0-9a-f]{64}$/), "contract.env.example": z.string().regex(/^[0-9a-f]{64}$/), "README.md": z.string().regex(/^[0-9a-f]{64}$/), }).strict(), }).strict(); // Public P1 bundles contain only the descriptor and derived docs. Evidence file bytes remain // revision-owned Git content for the later P6 materialization boundary. const BUNDLE_FILES = ["manifest.json", "workspace.yaml", "contract.env.example", "README.md"] as const; type BundleFile = (typeof BUNDLE_FILES)[number]; const SAFE_MESSAGES = { workspace_invalid: "Workspace request or bundle is invalid.", binding_missing: "Installation binding is missing or invalid.", workspace_not_activatable: "Workspace cannot be activated on this installation.", workspace_stale: "Workspace revision is stale.", workspace_conflict: "Workspace changed in the registry.", workspace_curator_owned: "Workspace descriptor is owned by the curator and must be changed through Git.", git_unavailable: "Workspace Git service is unavailable.", git_auth_failed: "Workspace Git authentication failed.", git_non_fast_forward: "Workspace Git branch has changed.", git_push_rejected: "Workspace Git publication was rejected.", connector_unavailable: "Workspace connector is unavailable.", semantic_index_incompatible: "Semantic index is incompatible with this workspace.", } as const; function sha256(value: string | Buffer): string { return createHash("sha256").update(value).digest("hex"); } function invalidBundle(): WorkspaceRegistryError { return new WorkspaceRegistryError("workspace_invalid", "Workspace bundle is invalid"); } function isBundleFile(value: string): value is BundleFile { return (BUNDLE_FILES as readonly string[]).includes(value); } function unsafeArchiveEntry(entry: yauzl.Entry): boolean { const name = entry.fileName; const unixType = (entry.externalFileAttributes >>> 16) & 0o170000; return name.length === 0 || name.startsWith("/") || name.startsWith("\\") || name.includes("\\") || name.split("/").includes("..") || name.endsWith("/") || unixType === 0o120000 || !isBundleFile(name); } async function readZipBundle(source: Buffer, config: WorkspaceRegistryConfig): Promise> { if (source.length === 0 || source.length > config.maxImportBytes) throw invalidBundle(); return await new Promise>((resolve, reject) => { yauzl.fromBuffer(source, { lazyEntries: true, strictFileNames: true, validateEntrySizes: true, decodeStrings: true, }, (error, archive) => { if (error || !archive) return reject(invalidBundle()); const files = new Map(); let entries = 0; let settled = false; const fail = () => { if (settled) return; settled = true; archive.close(); reject(invalidBundle()); }; archive.on("error", fail); archive.on("entry", (entry) => { entries += 1; if (entries > config.maxImportEntries || unsafeArchiveEntry(entry) || files.has(entry.fileName as BundleFile)) { fail(); return; } if (entry.uncompressedSize > config.maxImportBytes) { fail(); return; } archive.openReadStream(entry, (streamError, stream) => { if (streamError || !stream) return fail(); const chunks: Buffer[] = []; let size = 0; stream.on("data", (chunk: Buffer) => { size += chunk.length; if (size > config.maxImportBytes) return fail(); chunks.push(chunk); }); stream.on("error", fail); stream.on("end", () => { if (settled || size !== entry.uncompressedSize) return fail(); files.set(entry.fileName as BundleFile, Buffer.concat(chunks)); archive.readEntry(); }); }); }); archive.on("end", () => { if (settled) return; settled = true; if (entries !== BUNDLE_FILES.length || BUNDLE_FILES.some((name) => !files.has(name))) return reject(invalidBundle()); resolve(Object.fromEntries(files) as Record); }); archive.readEntry(); }); }); } function utf8(buffer: Buffer): string { const text = buffer.toString("utf8"); if (!Buffer.from(text, "utf8").equals(buffer) || text.includes("\0")) throw invalidBundle(); return text; } async function importDraft(source: Buffer, config: WorkspaceRegistryConfig): Promise { const files = await readZipBundle(source, config); let manifest: z.infer; try { manifest = bundleManifest.parse(JSON.parse(utf8(files["manifest.json"]))); } catch { throw invalidBundle(); } for (const name of ["workspace.yaml", "contract.env.example", "README.md"] as const) { if (sha256(files[name]) !== manifest.files[name]) throw invalidBundle(); } try { const descriptor = parseWorkspaceYaml(utf8(files["workspace.yaml"])); const workspace = validateWorkspaceDescriptor(descriptor); const docs = renderWorkspaceDocs(workspace); if ( workspace.workspace.id !== manifest.workspace_id || serializeWorkspaceYaml(workspace) !== utf8(files["workspace.yaml"]) || docs.envExample !== utf8(files["contract.env.example"]) || docs.markdown !== utf8(files["README.md"]) ) throw invalidBundle(); return workspace; } catch (error) { if (error instanceof WorkspaceRegistryError) throw error; throw invalidBundle(); } } async function exportBundle(workspace: CanonicalWorkspace): Promise { const yaml = serializeWorkspaceYaml(workspace); const docs = renderWorkspaceDocs(workspace); const files: Record = { "manifest.json": JSON.stringify({ schema_version: 1, workspace_id: workspace.workspace.id, files: { "workspace.yaml": sha256(yaml), "contract.env.example": sha256(docs.envExample), "README.md": sha256(docs.markdown), }, }), "workspace.yaml": yaml, "contract.env.example": docs.envExample, "README.md": docs.markdown, }; const archive = new yazl.ZipFile(); const chunks: Buffer[] = []; archive.outputStream.on("data", (chunk: Buffer) => chunks.push(chunk)); for (const name of BUNDLE_FILES) archive.addBuffer(Buffer.from(files[name]), name); archive.end(); await new Promise((resolve, reject) => { archive.outputStream.once("end", resolve); archive.outputStream.once("error", reject); }); return Buffer.concat(chunks); } 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_conflict" || code === "workspace_curator_owned" || code === "workspace_stale" || code === "git_non_fast_forward") return 409; if (code === "git_unavailable" || code === "git_auth_failed" || code === "git_push_rejected") return 503; return 400; } function validatedWorkspace(value: unknown): WorkspaceDescriptor | undefined { try { return validateWorkspaceDescriptor(value); } catch { return undefined; } } function errorReply(reply: FastifyReply, error: unknown) { const code = workspaceErrorCode(error); const body: Record = { code, message: SAFE_MESSAGES[code] }; if (error instanceof WorkspaceConflictError) { body.fields = error.fields; body.expected = error.expected; body.actual = error.actual; if (error.base) body.base = error.base; if (error.local) body.local = error.local; if (error.remote) body.remote = error.remote; } else if (code === "workspace_conflict" && error && typeof error === "object") { const conflict = error as Partial; if (Array.isArray(conflict.fields) && conflict.fields.every((field) => typeof field === "string")) body.fields = conflict.fields; for (const key of ["expected", "actual"] as const) { const revision = conflict[key]; if ( revision && typeof revision.commit === "string" && /^[0-9a-f]{40}$/.test(revision.commit) && (revision.blob === undefined || (typeof revision.blob === "string" && /^[0-9a-f]{40}$/.test(revision.blob))) ) body[key] = revision; } for (const key of ["base", "local", "remote"] as const) { const workspace = validatedWorkspace(conflict[key]); if (workspace) body[key] = workspace; } } return reply.code(workspaceErrorStatus(code)).send(body); } function publishRequest(value: unknown): PublishWorkspaceRequest { const parsed = publishPayload.parse(value); if (parsed.action === "delete") return parsed; return { ...parsed, workspace: validateWorkspaceDescriptor(parsed.workspace) }; } export function workspaceRoutes(app: FastifyInstance, deps: WorkspaceRoutesDeps): void { app.register(multipart, { limits: { fileSize: deps.config.maxImportBytes, files: 1, fields: 0, parts: 1 }, throwFileSizeLimit: true, }); app.get("/workspace-registry/status", async (_request, reply) => { try { return await deps.registry.bootstrap(); } catch (error) { return errorReply(reply, error); } }); app.post("/workspace-registry/pull", async (_request, reply) => { try { return await deps.registry.pull(); } catch (error) { return errorReply(reply, error); } }); app.get("/workspaces", async (_request, reply) => { try { const records = await deps.registry.listCatalog(); return records.map((record) => ({ 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: record.configurationState, ...(record.revision ? { revision: record.revision } : {}), })); } catch (error) { return errorReply(reply, error); } }); app.get("/workspaces/:id", async (request, 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) => { try { const { workspace } = workspacePayload.parse(request.body); const canonical = validateWorkspaceDescriptor(workspace); return { workspace: canonical, contract: buildInstallationContract(canonical) }; } catch (error) { return errorReply(reply, error); } }); app.post("/workspaces/:id/test", async (request, 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 bindings = resolveRuntimeBindings(operational, process.env, deps.config.secretRoots); return await deps.diagnose(operational, bindings, { writeProbe: false }); } catch (error) { return errorReply(reply, error); } }); app.post("/workspaces/publish", async (request, reply) => { try { const result = await deps.registry.publish(publishRequest(request.body)); return result ? { revision: result } : reply.code(204).send(); } catch (error) { return errorReply(reply, error); } }); app.get("/workspaces/:id/export", async (request, reply) => { try { const { id } = z.object({ id: workspaceId }).parse(request.params); const { workspace } = await deps.registry.read(id); const canonical = validateWorkspaceDescriptor(workspace); const bundle = await exportBundle(canonical); return reply .type("application/zip") .header("content-disposition", `attachment; filename=\"${id}.zip\"`) .send(bundle); } catch (error) { return errorReply(reply, error); } }); app.post("/workspaces/import", async (request: FastifyRequest, reply) => { try { const file = await request.file(); if (!file || file.fieldname !== "bundle" || file.mimetype !== "application/zip") throw invalidBundle(); const draft = await importDraft(await file.toBuffer(), deps.config); return { draft: { workspace: draft, contract: buildInstallationContract(draft) } }; } catch (error) { return errorReply(reply, error); } }); }