// Copyright 2026 SZL Holdings // SPDX-License-Identifier: Apache-2.0 // // a11oyClient.ts — browser/runtime client for a11oy's HTTP server (the routes // added by `a11oy serve`). This is the data layer behind the 5 operator-console // routes. It is deliberately dependency-free (uses fetch) so it runs unchanged // in the browser SPA and under node:test against a real loopback server. // // Endpoints (a11oy serve — see packages/receipt-substrate/src/serve.ts): // GET /healthz // GET /readyz // GET /v1/ledger?limit=N // GET /v1/ledger/{hash} // POST /v1/verify {ledger:[...]} -> {valid, broken_at, errors} // POST /v1/policy/evaluate {action} -> {decision, gate, receipt_hash, ...} // // No mocked data path: every method issues a real fetch and returns the parsed // real response. // // Authored for SZL Holdings. Signed-off per repository DCO. export interface OperationalReceipt { readonly receipt_id?: string; readonly merkle_root?: string; readonly [key: string]: unknown; } export interface Health { readonly status: string; readonly sha: string; readonly ts: string; } export interface Readiness { readonly status: string; readonly ledger: string; readonly reason?: string; } export interface LedgerPage { readonly count: number; readonly total: number; readonly receipts: OperationalReceipt[]; } export interface VerifyResult { readonly valid: boolean; readonly broken_at: number | null; readonly errors: string[]; } export interface PolicyDecision { readonly decision: "allow" | "deny"; readonly gate: string; readonly receipt_hash: string; readonly rationale?: string; readonly lambda_score?: number; } export interface PolicyActionInput { readonly actionId?: string; readonly severity?: "low" | "medium" | "high" | "critical"; readonly decisionClass?: string; readonly confidence?: number; readonly witnesses?: unknown[]; } type FetchLike = (input: string, init?: RequestInit) => Promise; export class A11oyClient { private readonly base: string; private readonly fetchImpl: FetchLike; constructor(baseUrl: string, fetchImpl?: FetchLike) { this.base = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl; // Allow injection for tests; default to global fetch (browser + Node 18+). this.fetchImpl = fetchImpl ?? ((i, init) => fetch(i, init)); } private async getJson(path: string): Promise { const res = await this.fetchImpl(this.base + path, { method: "GET" }); if (!res.ok) throw new Error(`GET ${path} -> ${res.status}`); return (await res.json()) as T; } private async postJson(path: string, body: unknown): Promise { const res = await this.fetchImpl(this.base + path, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body), }); if (!res.ok) throw new Error(`POST ${path} -> ${res.status}`); return (await res.json()) as T; } /** Route 1 backing call: liveness. */ health(): Promise { return this.getJson("/healthz"); } /** Route 1 backing call: readiness (ledger readable). */ readiness(): Promise { return this.getJson("/readyz"); } /** Route 2 (Proof Ledger) backing call. */ ledger(limit = 20): Promise { return this.getJson(`/v1/ledger?limit=${encodeURIComponent(String(limit))}`); } /** Route 3 (Receipt detail) backing call. */ receipt(hash: string): Promise<{ receipt: OperationalReceipt }> { return this.getJson<{ receipt: OperationalReceipt }>(`/v1/ledger/${encodeURIComponent(hash)}`); } /** Route 4 (Verify) backing call. */ verify(ledger: OperationalReceipt[]): Promise { return this.postJson("/v1/verify", { ledger }); } /** Route 5 (Policy) backing call. */ evaluatePolicy(action: PolicyActionInput): Promise { return this.postJson("/v1/policy/evaluate", { action }); } } /** The 5 operator-console routes, derived from the mined a11oy MCP route surface. */ export const CONSOLE_ROUTES = [ { path: "/", label: "Health", source: "GET /healthz + GET /readyz" }, { path: "/ledger", label: "Proof Ledger", source: "GET /v1/ledger" }, { path: "/receipt/:hash", label: "Receipt", source: "GET /v1/ledger/{hash}" }, { path: "/verify", label: "Verify", source: "POST /v1/verify" }, { path: "/policy", label: "Policy", source: "POST /v1/policy/evaluate" }, ] as const;