Track B Phase 3 (CR-1) — chain-verify badge over a shared verify service
2026-06-21
LOCKSTEP TRACEABILITY MATRIX --- api_endpoints: ["GET /auth/governance/audit/chain/verify"] sdk_methods_updated: ["none — dashboard JWT mirror of /v1/governance/audit/chain/verify"] mcp_tools_updated: ["none"] ---
What We Built
The dashboard now shows the tamper-evident state of the audit chain as a badge, backed by a JWT-human route GET /auth/governance/audit/chain/verify. The badge resolves to one of: verifying… → chain verified ✓ (N) (the reserved chain-verified green) / CHAIN BROKEN AT #n (red, server-attested) / unavailable (gray, transient only). It mounts on the #grants surface — the page where every break-glass revoke and scope denial is committed to that very chain.
Crucially, the new /auth route and the existing apiKey /v1/governance/audit/chain/verify route now call one shared implementation — verifyTenantAuditChain(), extracted into src/security/audit-chain-verify-service.ts. This directly answers the design's R10 risk: a serialization drift between two copies of the verify logic rendering a false BROKEN (a max-severity UI lie). One implementation cannot drift from itself.
The service also closes a CR-7 loop: when a verify detects a break, it emits the chain_break governance event, so the live feed surfaces tamper evidence the instant a verification catches it.
Why It Matters
"Tamper-evident" is only believable if an operator can _see_ it holding. The badge turns the hash chain — the spine of the whole evidence-plane thesis — into a glanceable, server-attested verdict over the recent window (per-row v1/v2 payload dispatch, the lower-edge suppression probe, and the v2 provenance recompute all included). BROKEN AT #n is reserved for a real, server-computed break; the badge never cries wolf on a client-side serialization quirk (R10).
How It Works
verifyTenantAuditChain(tenantId, { since, limit }) reads the newest limit rows in monotonic insert order, rebuilds each row's exact hashed payload by its own chain-schema version, runs verifyChain, closes the window's lower edge against MAX(tenant_seq) below it, and adds the per-row provenance recompute — returning a single verdict object both routes serialize verbatim. The dashboard chainVerifyBadge() fetches it over the shared JWT authFetch, falling back to an honest "unavailable" only on a real transient error (an absent/gray-forever badge would be wallpaper — R9).
The Numbers
- 1 new JWT route; 1 shared service (eliminating a 145-line duplication risk); 1 new badge component + service test. The DB-backed path stays covered by the existing
/v1chain-verify suite. - 9,030 backend tests + 389 dashboard tests pass; build +
pnpm checkgreen. The two Phase-1 R9 assertions that asserted the badge was _absent_ from source were flipped to assert it now ships.
Competitive Edge
The hash chain was always there; CR-1 makes it _legible_. A reviewer glancing at the dashboard sees "chain verified ✓ (200)" — and if they don't, they see exactly which row broke and why. No competitor in this set renders cryptographic audit integrity as a first-class, server-attested UI state.
Lockstep Checklist
- [x] API Routes:
GET /auth/governance/audit/chain/verify(JWT mirror), sharedverifyTenantAuditChainservice. - [x] TS/Python SDK: none — dashboard JWT mirror of the apiKey
/v1route. - [x] MCP Schemas: none.
- [x] Generated contract:
docs/openapi.yaml+ static-assets regenerated. - [x] Tests: service no-Postgres contract test; dashboard R9 assertions flipped; full suites green.
Remaining (Phase 3 + 4)
The chain-verify badge and CR-7 governance events ship the live, honest evidence spine. Still ahead: migrate /v1 chain-verify onto the shared service (DRY follow-up — no behavior change), the would-deny preview UI (CR-3), effective-state badges (CR-2/CR-5), the scope-check composer (CR-4 refactor), and the live-feed wiring of the governance events into the Ledger. Phase 4 (Plans) after.