feat(trust): canonical institutional trust primitives (Lane B)#341
feat(trust): canonical institutional trust primitives (Lane B)#341ctol3r wants to merge 1 commit into
Conversation
Creates the shared foundation that Lane C/D will adopt across passport,
verifier, replay, receipt, and trust surfaces. No surface adoption in
this PR — that is Lane C's scope.
New module: apps/web/components/trust/
CheckedAtStamp — visible ISO timestamp; never aria-only / tooltip-only
RunIdentity — visible run_id; truncates middle, full id on data-run-id
TierBadge — T1/T2/T3/T4 + DEGRADED / UNAVAILABLE / UNSIGNED /
ISSUER_UNREACHABLE special states
TrustStateBand — GREEN / YELLOW / RED / UNKNOWN; decision-readiness
band (distinct from TierBadge claim-confidence)
OwnershipState — CLAIMED / PENDING / UNCLAIMED / DISPUTED chip;
fills the audit-identified UI gap where
/api/ownership endpoints exist with no surface
DegradedStateBanner— A/B/C/D/E taxonomy; D ("no adverse findings")
renders as SUCCESS, C ("infrastructure outage")
as critical with role=alert
IssuerAttribution — signer / DID / key id / receipt / continuity
(ACTIVE / STALE / BROKEN); surfaces backend
identity data that previously never reached the UI
ReplayLineage — evidence continuity chain (not analytics):
lineage key, prior checks, gaps, replay count
TrustHeader — composite enforcing the mandatory render order:
OBJECT → OWNERSHIP → CHECKED_AT → CHANNEL → REPLAY
→ RUN_ID; PREVIEW / SNAPSHOT / SIGNED variants
Tests (51 cases, vitest):
- A/B/C/D/E taxonomy completeness; D-is-success / C-is-critical invariants
- Replay continuity detection (zero checks, no gaps, with gaps)
- TrustHeader section count + canonical order assertion
- CheckedAtStamp + RunIdentity visibility (rendered text, not just data-*)
- OwnershipState + TierBadge full-vocabulary coverage
- Doctrine: banned-strings scan across all primitives
- "Verified" bare-label scan
Validation: 51/51 vitest passing; pnpm turbo run build --filter @vitalcv/web
green (13/13 tasks).
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d491e4e3fc
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const sortedChecks = [...priorChecks].sort((a, b) => a.checkedAt.localeCompare(b.checkedAt)); | ||
| const sortedGaps = [...gaps].sort((a, b) => a.from.localeCompare(b.from)); |
There was a problem hiding this comment.
Sort replay timestamps by epoch time
checkedAt/from are documented as ISO timestamps, but these arrays are currently sorted with localeCompare, which is not chronological when offsets differ (e.g. 2026-01-01T00:30:00+01:00 sorts after 2026-01-01T00:00:00Z even though it is earlier). In those inputs, the lineage chain and gap list render in the wrong order, which can misstate evidence continuity. Parse to numeric time (new Date(...).getTime()) for ordering instead of lexical string compare.
Useful? React with 👍 / 👎.
Adds the cryptographically-signed receipt surface that an external
verifier needs to validate a clinician passport snapshot end-to-end:
1. fetch the receipt at /api/receipt/<npi>
2. fetch the JWKS at /.well-known/jwks.json (from Wave 9)
3. validate the JWT signature against the JWKS
The new route signs an ES256 JWT using the same getOrInitKeypair()
that produces the public key published at /.well-known/jwks.json
(Wave 9), so kid + key never diverge between issuance and
discoverability.
Receipt shape (ES256 JWT):
header { alg: 'ES256', kid: <active-kid>, typ: 'vc+jwt' }
payload { iss: <did:web issuer>, sub: 'npi:<npi>',
jti: 'receipt:<runId>' (deterministic per snapshot),
iat: <lastCheckedAt seconds>, nbf, exp = iat + 90d,
vc: W3C VC 2.0 credential block (VitalCVTrustReceipt),
vcv: VitalCV claim block incl. replay identity }
Determinism contract (test-pinned):
- jti = 'receipt:<replay.runId>' so the same evidence snapshot
yields the same receipt jti; the JWT signature is non-
deterministic per ES256 spec but the payload is byte-identical.
- iat is pinned to lastCheckedAt seconds so two receipts for the
same snapshot share iat. A verifier joining receipts by
(lineageKey, runId) gets unambiguous snapshot identity.
Verifier readability:
- vcv.jwksUri and vcv.didDocumentUri point at the well-known
surfaces #349 published, so a receipt is self-describing.
- X-Receipt-Kid + X-Receipt-Issuer response headers let a verifier
short-circuit JWKS resolution if it already has the key cached.
- ?format=download adds Content-Disposition: attachment so a
verifier engineer can save the JWT for offline validation.
Tests (10 vitest cases, all passing):
- 400 on non-10-digit NPI, 502 on malformed upstream, propagated
backend status on upstream non-ok
- 200 + application/jwt + ES256 + kid header + 'vc+jwt' typ
- W3C VC 2.0 + VitalCV claim block shape (credentialSubject,
replay.{lineageKey,runId,schemeVersion}, jwksUri, didDocumentUri)
- Determinism: same fetch → same jti, same iat
- End-to-end verifier flow: receipt JWT validates against the
JWKS the well-known route exposes (createLocalJWKSet + jwtVerify)
- ?format=download adds Content-Disposition: attachment
- Graceful no-replay fallback (jti from npi when replay absent)
Validation: 10/10 vitest passing; pnpm turbo run build --filter
@vitalcv/web → 13/13 tasks; truth-strings scan CLEAN.
Stacked on the rest of this branch (#349's well-known surfaces +
wellKnownIdentity helper). Brief 1 of this turn (replay-lineage
rendering) is structurally already shipped across #341 + #342 +
#343 + #345 — no rendering changes needed here.
Summary
Lane B of the trust-convergence migration. Creates the shared foundation of canonical primitives under
apps/web/components/trust/that Lane C/D will adopt. No surface adoption in this PR — pure additive primitive layer.Primitives created
CheckedAtStampRunIdentitydata-run-id)TierBadgeTrustStateBandOwnershipStateDegradedStateBannerIssuerAttributionReplayLineageTrustHeaderMandatory render order (enforced by
<TrustHeader>)OBJECT → OWNERSHIP → CHECKED_AT → CHANNEL → REPLAY → RUN_IDTest asserts the six sections appear in this order in the DOM. Variant (
PREVIEW/SNAPSHOT/SIGNED) changes visual register but never the order.Degraded-state taxonomy (canonical)
D-is-successis pinned with a test that explicitly rejectsdata-degraded-tone="warning"and"critical"on code D, ensuring "absence of evidence" is not conflated with "presence of risk".What this PR does NOT do
@vitalcv/trust-statepackage.Truth rules
Verifiedas a status label).Validation
components/trust/__tests__/trust-primitives.test.tsx)pnpm turbo run build --filter @vitalcv/web→ 13/13 tasks, 37sapps/web/components/trust/only (11 new files, no existing files modified)