fabro/apps/arc-web/app/api-client.ts
Bryan Helmkamp e938d3395a Throw Response from apiJson so API 404s render as 404 pages
apiJson was throwing a plain Error on non-ok responses, which React
Router always treated as a 500. Now throws a Response with the actual
status code so API 404s surface as proper 404 error pages.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-02 11:05:34 -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 Response(await res.text(), { status: res.status });
return res.json() as Promise<T>;
}