mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-09-06 08:18:58 +00:00
- Rename 20 crate directories lib/crates/arc-* → fabro-* - Update all Cargo.toml: crate names, dep paths, feature flags, bin name - Rename arc_server module → fabro_server in fabro-llm - ArcError → FabroError across 30+ files - ARC_VERSION/ARC_GIT_SHA/ARC_BUILD_DATE → FABRO_* constants - All use/qualified paths: arc_agent:: → fabro_agent::, etc. (~1500 occurrences) - Env vars ARC_* → FABRO_* in string literals and shell scripts - String literals: X-Arc-Demo, arc-bot, arc@local, arc-web, arc-mcp, etc. - Path strings: .arc/ → .fabro/, arc.toml → fabro.toml, refs/arc/ → refs/fabro/ - arc-api.yaml → fabro-api.yaml (OpenAPI spec) - skills/arc-create-workflow → fabro-create-workflow - trycmd fixtures: $ arc → $ fabro - Inline snapshots (insta) updated - CI, Docker, install.sh, scripts, CLAUDE.md, AGENTS.md - TypeScript app: env vars, headers, JWT issuer - Docs: page slugs, git refs, config paths, sandbox names, repo URLs - Repo references: brynary/arc → fabro-sh/fabro Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
80 lines
2.3 KiB
TypeScript
80 lines
2.3 KiB
TypeScript
import { importPKCS8, SignJWT } from "jose";
|
|
import { getAppConfig } from "./lib/config.server";
|
|
import { isDemoMode } from "./lib/demo-mode.server";
|
|
import { getUser } from "./lib/session.server";
|
|
|
|
const FABRO_JWT_PRIVATE_KEY = process.env.FABRO_JWT_PRIVATE_KEY;
|
|
|
|
function decodePemEnv(value: string): string {
|
|
if (value.startsWith("-----")) return value;
|
|
return Buffer.from(value, "base64").toString("utf-8");
|
|
}
|
|
|
|
let cachedKey: CryptoKey | null = null;
|
|
|
|
async function getSigningKey(): Promise<CryptoKey> {
|
|
if (cachedKey) return cachedKey;
|
|
if (!FABRO_JWT_PRIVATE_KEY) {
|
|
throw new Error("FABRO_JWT_PRIVATE_KEY environment variable is not set");
|
|
}
|
|
cachedKey = await importPKCS8(decodePemEnv(FABRO_JWT_PRIVATE_KEY), "EdDSA");
|
|
return cachedKey;
|
|
}
|
|
|
|
async function signToken(sub?: string): Promise<string> {
|
|
const key = await getSigningKey();
|
|
return new SignJWT({ iss: "fabro-web", ...(sub ? { sub } : {}) })
|
|
.setProtectedHeader({ alg: "EdDSA" })
|
|
.setIssuedAt()
|
|
.setExpirationTime("30s")
|
|
.sign(key);
|
|
}
|
|
|
|
interface ApiOptions {
|
|
init?: RequestInit;
|
|
request?: Request;
|
|
}
|
|
|
|
/**
|
|
* Fetch wrapper that signs requests with a JWT for service-to-service auth.
|
|
* When a request is provided, the authenticated user's URL is included as
|
|
* the JWT `sub` claim.
|
|
*/
|
|
export async function apiFetch(
|
|
path: string,
|
|
options?: ApiOptions
|
|
): Promise<Response> {
|
|
const { base_url } = getAppConfig().api;
|
|
const { init, request } = options ?? {};
|
|
|
|
let sub: string | undefined;
|
|
if (request) {
|
|
const user = await getUser(request);
|
|
sub = user?.userUrl;
|
|
}
|
|
|
|
const headers = new Headers(init?.headers);
|
|
if (FABRO_JWT_PRIVATE_KEY) {
|
|
const token = await signToken(sub);
|
|
headers.set("Authorization", `Bearer ${token}`);
|
|
}
|
|
if (request && isDemoMode(request)) {
|
|
headers.set("X-Fabro-Demo", "1");
|
|
}
|
|
|
|
const url = `${base_url}${path}`;
|
|
try {
|
|
return await fetch(url, { ...init, headers });
|
|
} catch (cause) {
|
|
throw new Error(`API request to ${url} failed`, { cause });
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Typed JSON fetch helper. Calls apiFetch and parses the JSON response.
|
|
*/
|
|
export async function apiJson<T>(path: string, options?: ApiOptions): Promise<T> {
|
|
const res = await apiFetch(path, options);
|
|
if (!res.ok) throw new Response(null, { status: res.status });
|
|
return res.json() as Promise<T>;
|
|
}
|