File size: 4,517 Bytes
518343a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
// 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<Response>;

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<T>(path: string): Promise<T> {
    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<T>(path: string, body: unknown): Promise<T> {
    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<Health> {
    return this.getJson<Health>("/healthz");
  }

  /** Route 1 backing call: readiness (ledger readable). */
  readiness(): Promise<Readiness> {
    return this.getJson<Readiness>("/readyz");
  }

  /** Route 2 (Proof Ledger) backing call. */
  ledger(limit = 20): Promise<LedgerPage> {
    return this.getJson<LedgerPage>(`/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<VerifyResult> {
    return this.postJson<VerifyResult>("/v1/verify", { ledger });
  }

  /** Route 5 (Policy) backing call. */
  evaluatePolicy(action: PolicyActionInput): Promise<PolicyDecision> {
    return this.postJson<PolicyDecision>("/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;