Files
ThothII/backend/src/routes/workspaces.ts
T

260 lines
10 KiB
TypeScript

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 } 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";
export type WorkspaceDiagnoser = (
workspace: WorkspaceDescriptor,
bindings: RuntimeBindings,
options: { writeProbe: boolean },
) => Promise<ConnectorDiagnostics>;
interface WorkspaceRoutesDeps {
registry: WorkspaceRegistry;
config: WorkspaceRegistryConfig;
diagnose: WorkspaceDiagnoser;
authDiagnoser: AuthDiagnoser;
secretStore: WorkspaceSecretStore;
}
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;
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, inspectedAuthentication] = await Promise.all([
deps.diagnose(operational, lease.bindings, { writeProbe: false }),
deps.authDiagnoser.inspect({ live: true }),
]);
const authentication = authenticationReport(inspectedAuthentication);
return {
...workspaceDiagnostics,
activatable: workspaceDiagnostics.activatable && authentication.ready,
authentication,
};
} finally {
lease.release();
}
} catch (error) {
return errorReply(reply, error);
}
});
}