Files
pikasTech-HWLAB/internal/workbench/http.ts
T
2026-07-17 09:16:47 +02:00

99 lines
8.3 KiB
TypeScript

import type { WorkbenchCommand } from "./contracts.ts";
export function createWorkbenchHttpApp(options: { dispatch: (command: WorkbenchCommand) => Promise<any>; snapshot?: () => Promise<{ sessions: Record<string, Record<string, unknown>>; turns: Record<string, Record<string, unknown>> }>; authorization?: string; close?: () => Promise<void> }) {
return {
async fetch(request: Request): Promise<Response> {
try {
const url = new URL(request.url);
if (url.pathname === "/health/live") return json(200, { ok: true, serviceId: "hwlab-workbench-api", status: "live" });
if (url.pathname === "/health/ready") return resultResponse(await options.dispatch({ operation: "health" }));
if (options.snapshot) {
if (url.pathname === "/auth/session" || url.pathname === "/auth/bootstrap") return json(200, { ok: true, authenticated: true, actor: { id: "usr_native", role: "user" }, mode: "native-test" });
if (url.pathname === "/auth/login" && request.method === "POST") return Response.json({ ok: true, authenticated: true, actor: { id: "usr_native", role: "user" }, mode: "native-test" }, { status: 200, headers: { "set-cookie": "hwlab_session=native-test; Path=/; HttpOnly; SameSite=Lax", "cache-control": "no-store" } });
if (url.pathname === "/v1/users/me") return json(200, { ok: true, actor: { id: "usr_native", role: "user" }, authMethod: "native-test" });
if (url.pathname === "/v1/access/status") return json(200, { ok: true, status: "active", actor: { id: "usr_native", role: "user" }, nav: ["workbench.code"] });
if (url.pathname === "/v1/provider-profiles") return json(200, { ok: true, items: [{ profile: "native-test", backendProfile: "native-test", configured: true }], count: 1, mode: "native-test" });
if (url.pathname === "/v1/dashboard/summary") return json(200, { ok: true, status: "ready", mode: "native-test" });
if (url.pathname === "/v1/workbench/events" && request.method === "GET") return nativeEventStream();
if (url.pathname === "/v1/workbench/sessions" && request.method === "GET") return nativeSessionList(options, url);
const sessionMatch = /^\/v1\/workbench\/sessions\/([^/]+)(?:\/(messages))?$/u.exec(url.pathname);
if (sessionMatch && request.method === "GET") return nativeSessionDetail(options, decodeURIComponent(sessionMatch[1]), sessionMatch[2] === "messages");
const turnMatch = /^\/v1\/workbench\/turns\/([^/]+)$/u.exec(url.pathname);
if (turnMatch && request.method === "GET") return nativeTurn(options, decodeURIComponent(turnMatch[1]));
}
if (url.pathname === "/v1/workbench/commands" && request.method === "POST") {
requireAuthorization(request, options);
const command = await bodyObject(request) as WorkbenchCommand;
return resultResponse(await options.dispatch(command), command.operation === "health" ? 200 : 202);
}
const actor = actorFrom(request, Boolean(options.snapshot));
if (url.pathname === "/v1/agent/sessions" && request.method === "POST") { requireAuthorization(request, options); return legacyResponse(await options.dispatch({ operation: "session.create", actor, params: await bodyObject(request) }), 201); }
if (url.pathname === "/v1/agent/chat" && request.method === "POST") {
requireAuthorization(request, options);
const params = await bodyObject(request);
return legacyResponse(await options.dispatch({ operation: "turn.submit", actor, params, traceId: text(params.traceId) || undefined }), 202);
}
if (url.pathname === "/v1/agent/chat/cancel" && request.method === "POST") {
requireAuthorization(request, options);
const params = await bodyObject(request);
return legacyResponse(await options.dispatch({ operation: "turn.cancel", actor, traceId: text(params.traceId), params }));
}
return json(404, { ok: false, error: { code: "not_found", message: "Workbench API route was not found" } });
} catch (error: any) {
return json(400, { ok: false, error: { code: error?.code ?? "workbench_http_error", message: error?.message ?? String(error) } });
}
},
close: options.close ?? (async () => {})
};
}
function requireAuthorization(request: Request, options: { snapshot?: unknown; authorization?: string }) {
if (options.snapshot) return;
const expected = text(options.authorization);
const actual = text(request.headers.get("authorization"));
if (!expected || actual !== expected) throw Object.assign(new Error("Workbench API internal authentication failed"), { code: "auth_required" });
}
async function nativeSessionList(options: { snapshot?: () => Promise<any> }, url: URL) {
const state = await requiredSnapshot(options);
const owner = text(url.searchParams.get("ownerUserId"));
const sessions = Object.values(state.sessions).filter((session: any) => !owner || session.ownerUserId === owner).sort((left: any, right: any) => String(right.updatedAt).localeCompare(String(left.updatedAt)));
return json(200, { ok: true, status: "ready", sessions, count: sessions.length, mode: "native-test" });
}
async function nativeSessionDetail(options: { snapshot?: () => Promise<any> }, sessionId: string, messagesOnly: boolean) {
const state = await requiredSnapshot(options);
const session = state.sessions[sessionId];
if (!session) return json(404, { ok: false, error: { code: "session_not_found", message: "native session was not found" } });
return json(200, messagesOnly ? { ok: true, sessionId, messages: session.messages ?? [], count: Array.isArray(session.messages) ? session.messages.length : 0, mode: "native-test" } : { ok: true, session, mode: "native-test" });
}
async function nativeTurn(options: { snapshot?: () => Promise<any> }, traceId: string) {
const state = await requiredSnapshot(options);
const turn = state.turns[traceId];
return turn ? json(200, { ok: true, ...turn, mode: "native-test" }) : json(404, { ok: false, error: { code: "turn_not_found", message: "native turn was not found" } });
}
async function requiredSnapshot(options: { snapshot?: () => Promise<any> }) { if (!options.snapshot) throw Object.assign(new Error("native projection is unavailable"), { code: "native_projection_unavailable" }); return options.snapshot(); }
function nativeEventStream() {
const encoder = new TextEncoder();
let heartbeat: ReturnType<typeof setInterval> | undefined;
return new Response(new ReadableStream({
start(controller) {
controller.enqueue(encoder.encode(`event: capability\ndata: ${JSON.stringify({ mode: "native-test", terminalAuthority: "fixture" })}\n\n`));
heartbeat = setInterval(() => controller.enqueue(encoder.encode(`: native-test-heartbeat ${Date.now()}\n\n`)), 10_000);
},
cancel() { if (heartbeat) clearInterval(heartbeat); }
}), { status: 200, headers: { "content-type": "text/event-stream", "cache-control": "no-store", connection: "keep-alive" } });
}
function actorFrom(request: Request, nativeTest: boolean) {
const id = text(request.headers.get("x-hwlab-actor-id"));
if (!id && nativeTest) return { id: "usr_native", role: "user" };
if (!id) throw Object.assign(new Error("x-hwlab-actor-id is required"), { code: "auth_required" });
return { id, role: text(request.headers.get("x-hwlab-actor-role")) || "user" };
}
function resultResponse(result: any, successStatus = 200) { return json(result?.ok === true ? successStatus : statusFor(result?.error?.code), result); }
function legacyResponse(result: any, successStatus = 200) { return result?.ok === true ? json(successStatus, { ok: true, ...result.data, orchestrationMode: result.mode }) : resultResponse(result, successStatus); }
function statusFor(code: unknown) { const value = String(code ?? ""); if (value === "auth_required") return 401; if (value.includes("not_found")) return 404; if (value.includes("required") || value === "invalid_input") return 400; return 503; }
async function bodyObject(request: Request) { const body = await request.json().catch(() => null); if (!body || typeof body !== "object" || Array.isArray(body)) throw Object.assign(new Error("JSON object body is required"), { code: "invalid_json" }); return body as Record<string, unknown>; }
function text(value: unknown) { return String(value ?? "").trim(); }
function json(status: number, body: unknown) { return Response.json(body, { status, headers: { "cache-control": "no-store" } }); }