fabro/apps/arc-web/app/api-client.ts
Bryan Helmkamp 49364b0843 Add demo API server and wire React app to fetch from API
Expand the OpenAPI spec from 11 to 39 endpoints covering Runs, Workflows,
Verifications, Retros, Sessions, Insights, Settings, and Projects with ~45
schemas. Add `--demo` flag to `arc serve` that serves static demo data for
all endpoints (auth disabled, read-only). Non-demo mode returns 501 for new
endpoints while existing run handlers continue working.

Regenerate the TypeScript API client and add `apiJson` helper. Wire all 19
React route files with server-side loaders that fetch from the API and map
snake_case responses to camelCase UI types. Mock data kept as fallback.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-02 09:39:53 -05:00

56 lines
1.5 KiB
TypeScript

import { importPKCS8, SignJWT } from "jose";
const ARC_API_BASE_URL = process.env.ARC_API_BASE_URL;
const ARC_JWT_PRIVATE_KEY = process.env.ARC_JWT_PRIVATE_KEY;
let cachedKey: CryptoKey | null = null;
async function getSigningKey(): Promise<CryptoKey> {
if (cachedKey) return cachedKey;
if (!ARC_JWT_PRIVATE_KEY) {
throw new Error("ARC_JWT_PRIVATE_KEY environment variable is not set");
}
cachedKey = await importPKCS8(ARC_JWT_PRIVATE_KEY, "EdDSA");
return cachedKey;
}
async function signToken(): Promise<string> {
const key = await getSigningKey();
return new SignJWT({ iss: "arc-web" })
.setProtectedHeader({ alg: "EdDSA" })
.setIssuedAt()
.setExpirationTime("30s")
.sign(key);
}
/**
* Fetch wrapper that signs requests with a JWT for service-to-service auth.
*/
export async function apiFetch(
path: string,
init?: RequestInit
): Promise<Response> {
if (!ARC_API_BASE_URL) {
throw new Error("ARC_API_BASE_URL environment variable is not set");
}
const headers = new Headers(init?.headers);
if (ARC_JWT_PRIVATE_KEY) {
const token = await signToken();
headers.set("Authorization", `Bearer ${token}`);
}
return fetch(`${ARC_API_BASE_URL}${path}`, {
...init,
headers,
});
}
/**
* Typed JSON fetch helper. Calls apiFetch and parses the JSON response.
*/
export async function apiJson<T>(path: string, init?: RequestInit): Promise<T> {
const res = await apiFetch(path, init);
if (!res.ok) throw new Error(`API ${res.status}: ${await res.text()}`);
return res.json() as Promise<T>;
}