import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs"; import { homedir } from "node:os"; import { join, relative, resolve } from "node:path"; import { artifactCacheDirFromEnv, type OrchestratorConfig } from "./config"; import { RELAY_TOKEN_HEADER } from "agent-relay-sdk"; const SAFE_ARTIFACT_ID = /^[a-zA-Z0-9._-]{1,160}$/; const CONTENT_ROUTE = /^\/api\/artifacts\/([^/]+)\/content$/; const CACHEABLE_RESPONSE_HEADERS = [ "content-type", "content-disposition", "x-artifact-digest", "etag", "last-modified", "cache-control", ]; const FORWARDED_REQUEST_HEADERS = [ "content-type", "x-artifact-filename", "x-artifact-digest", "x-artifact-kind", "x-artifact-sensitivity", "x-artifact-expires-at", ]; const RESPONSE_HEADERS = [ "content-type", "content-length", "content-disposition", "x-artifact-digest", "etag", "last-modified", "cache-control", ]; interface CacheMeta { headers: Record; cachedAt: number; } export function artifactProxyBaseUrl(config: Pick): string { return `http://127.0.0.1:${config.apiPort}/api/artifacts`; } function artifactCacheRoot(): string { const configured = artifactCacheDirFromEnv(); if (configured === "~") return homedir(); if (configured.startsWith("~/")) return join(homedir(), configured.slice(2)); return configured; } function cachePaths(id: string, root = artifactCacheRoot()): { dir: string; content: string; meta: string } { if (!SAFE_ARTIFACT_ID.test(id)) throw new Error("invalid artifact id"); const base = resolve(root); const dir = resolve(base, id); const rel = relative(base, dir); if (rel.startsWith("..") || rel.startsWith("/") || rel === "") throw new Error("artifact cache path escapes root"); return { dir, content: join(dir, "content"), meta: join(dir, "meta.json"), }; } function responseHeaders(source: Headers, names = RESPONSE_HEADERS): Headers { const headers = new Headers(); for (const name of names) { const value = source.get(name); if (value) headers.set(name, value); } headers.set("Access-Control-Allow-Origin", "*"); return headers; } function forwardHeaders(req: Request, config: OrchestratorConfig): Headers { const headers = new Headers(); for (const name of FORWARDED_REQUEST_HEADERS) { const value = req.headers.get(name); if (value) headers.set(name, value); } const token = config.token || req.headers.get("x-agent-relay-token"); if (token) headers.set(RELAY_TOKEN_HEADER, token); return headers; } function relayUrl(req: Request, config: OrchestratorConfig): URL { const url = new URL(req.url); return new URL(url.pathname + url.search, config.relayUrl); } async function relayFetch(req: Request, config: OrchestratorConfig): Promise { return fetch(relayUrl(req, config), { method: req.method, headers: forwardHeaders(req, config), body: req.method === "GET" || req.method === "HEAD" ? undefined : req.body, }); } function cachedContentResponse(id: string): Response | null { let paths; try { paths = cachePaths(id); } catch { return null; } if (!existsSync(paths.content) || !existsSync(paths.meta)) return null; try { const meta = JSON.parse(readFileSync(paths.meta, "utf8")) as CacheMeta; const stat = statSync(paths.content); const headers = new Headers(meta.headers); headers.set("Content-Length", String(stat.size)); return new Response(readFileSync(paths.content), { status: 200, headers }); } catch { return null; } } function writeCachedContent(id: string, bytes: ArrayBuffer, headers: Headers): void { const paths = cachePaths(id); mkdirSync(paths.dir, { recursive: true, mode: 0o700 }); const cachedHeaders: Record = {}; for (const name of CACHEABLE_RESPONSE_HEADERS) { const value = headers.get(name); if (value) cachedHeaders[name] = value; } cachedHeaders["content-length"] = String(bytes.byteLength); writeFileSync(paths.content, Buffer.from(bytes)); writeFileSync(paths.meta, JSON.stringify({ headers: cachedHeaders, cachedAt: Date.now() } satisfies CacheMeta, null, 2) + "\n"); } async function proxyContent(req: Request, config: OrchestratorConfig, id: string): Promise { if (req.method === "GET") { const cached = cachedContentResponse(id); if (cached) return cached; } const relay = await relayFetch(req, config); const headers = responseHeaders(relay.headers); if (req.method === "HEAD" || !relay.ok) { return new Response(req.method === "HEAD" ? null : relay.body, { status: relay.status, headers }); } const bytes = await relay.arrayBuffer(); try { writeCachedContent(id, bytes, headers); } catch (e) { console.error(`[orchestrator] artifact cache write failed for ${id}: ${(e as Error).message}`); } return new Response(bytes, { status: relay.status, headers }); } export async function proxyArtifactRequest(req: Request, config: OrchestratorConfig): Promise { const url = new URL(req.url); if (url.pathname === "/api/artifacts" && (req.method === "GET" || req.method === "POST")) { const relay = await relayFetch(req, config); return new Response(relay.body, { status: relay.status, headers: responseHeaders(relay.headers) }); } const contentMatch = url.pathname.match(CONTENT_ROUTE); if (contentMatch && (req.method === "GET" || req.method === "HEAD")) { const id = decodeURIComponent(contentMatch[1]!); if (!SAFE_ARTIFACT_ID.test(id)) return new Response(JSON.stringify({ error: "invalid artifact id" }), { status: 400, headers: { "Content-Type": "application/json" } }); return proxyContent(req, config, id); } const artifactMatch = url.pathname.match(/^\/api\/artifacts\/([^/]+)$/); if (artifactMatch && (req.method === "GET" || req.method === "DELETE")) { const id = decodeURIComponent(artifactMatch[1]!); if (!SAFE_ARTIFACT_ID.test(id)) return new Response(JSON.stringify({ error: "invalid artifact id" }), { status: 400, headers: { "Content-Type": "application/json" } }); const relay = await relayFetch(req, config); return new Response(relay.body, { status: relay.status, headers: responseHeaders(relay.headers) }); } return new Response(JSON.stringify({ error: "Not found" }), { status: 404, headers: { "Content-Type": "application/json" } }); }