feat(verifier): well-known discovery surfaces + signed VC 2.0 receipt endpoint#349
feat(verifier): well-known discovery surfaces + signed VC 2.0 receipt endpoint#349ctol3r wants to merge 2 commits into
Conversation
Closes the verifier-engineer readiness verdict's 4-of-7 gap: external
verifiers can now resolve VitalCV trust infrastructure from the
RFC-mandated paths without any internal tooling.
Surfaces mounted (all at the bare /.well-known/ root, RFC 8615 style):
/.well-known/jwks.json
RFC 8615 + RFC 7517. Thin re-export of the existing namespaced
handler at /api/.well-known/jwks.json — same getPublicKeyJwk()
source, so kid + key never diverge. Sets the correct
`application/jwk-set+json` content type.
/.well-known/did.json
W3C DID Core document for `did:web:<origin>`. Embeds the public
key from JWKS as a JsonWebKey2020 verification method;
verification-method id is `<did>#<kid>` so a verifier reading a
receipt's `kid` header lands directly on the matching key.
Service entries discover the sibling well-known endpoints.
`application/did+json` content type.
/.well-known/openid-credential-issuer
OID4VCI issuer metadata. Emits both
`credential_configurations_supported` (draft-13+) and
`credentials_supported` (draft-11) so verifiers on either
revision can consume it. Lists VitalCV's two SD-JWT VCT types:
VitalCVClinicianPassport, VitalCVTrustReceipt.
/.well-known/trust-register
VitalCV institutional manifest (not an external RFC). Carries
issuer DID, signing kid + algorithm, runtime channel, deploy
commit, the four launch-spine source ids (NPPES, OIG, PECOS,
state board), credential types, sibling endpoints, doctrine
pointers, and a disclaimers block (no implied PSV, no
compliance certification claim). schema_version = 1.
Shared identity helper:
apps/web/lib/trust/wellKnownIdentity.ts
getIssuerOrigin() — resolves the canonical origin from env
(VITALCV_ISSUER_ORIGIN > VERCEL_*_URL > app.vitalcv.com fallback)
getIssuerDid() — `did:web:<host>` derived from the origin
getVerificationMethodId(did, kid) — `<did>#<kid>` for VM ids
getRuntimeChannel(), getDeployCommit() — surfaced on trust-register
All four routes share this helper so a future host or DID-method
change touches one file, not four.
Tests (5 vitest cases):
- jwks: 200, content-type, no private `d` field
- did.json: 200, @context, verificationMethod, service entries
- oid4vci: 200, both draft-11 and draft-13+ shapes
- trust-register: 200, schema_version pinned, launch_spine = 4
sources, disclaimers non-empty
- cross-surface coherence: same DID + kid + JWKS URI across all
four surfaces (load-bearing institutional invariant)
Validation: 5/5 vitest passing; pnpm turbo run build --filter
@vitalcv/web → 13/13 tasks; truth-strings scan CLEAN.
Out of scope (explicit follow-ups):
- /receipt/[id] + /api/receipt/[id] — Wave 13
- /replay/[runId] + /api/replay/[runId] — Wave 17
- /trust overview page — Wave 18 remainder
- Middleware 404-not-500 — separate small PR
- did-configuration linkage (DIF) — depends on cross-signing infra
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 2d8760224a
ℹ️ 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 metadata = { | ||
| credential_issuer: origin, | ||
| credential_endpoint: `${origin}/api/credentials/issue`, |
There was a problem hiding this comment.
Point credential_endpoint to an implemented API route
The metadata advertises credential_endpoint as /api/credentials/issue, but this Next.js app does not implement that route (the only credential routes under apps/web/app/api/credentials are ingest, ingest-npi, mine, and [id]/confirm). OID4VCI clients that discover this endpoint from /.well-known/openid-credential-issuer will attempt issuance there and get a 404, so credential issuance via discovery fails even though discovery itself succeeds.
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.
After auditing apps/web/lib/auth/roles.ts on origin/main against the new verifier-continuity surfaces in #349, #345, #355: - .well-known/* surfaces ✅ already covered by /^\/\.well-known(\/.*)?$/ - /api/receipt/* ✅ covered by /^\/api(\/.*)?$/ - /verify ✅ covered by /^\/verify(\/.*)?$/ - /trust⚠️ falls through (no pattern matches, no required role) — reachable but implicit This PR resolves the lone gap by adding an explicit /^\/trust(\/.*)?$/ pattern to PUBLIC_ROUTE_PATTERNS so the verifier-continuity intent is documented in the allowlist rather than relying on the middleware's neither-public-nor-protected fall-through. Eight new rows in middleware.test.ts pin the public status of every verifier-continuity surface so a future allowlist regression fails CI. MERGE_READINESS.md updated: - §5.5 verdict downgraded MEDIUM → LOW (was: blocker; now: resolved) - §6 blocker matrix row crossed out (resolved in this PR) Files: apps/web/lib/auth/roles.ts +1 line (one regex) apps/web/__tests__/middleware.test.ts +8 rows (test matrix) MERGE_READINESS.md §5.5 + §6 updates Validation: pnpm exec vitest run __tests__/middleware.test.ts → 44/44 pnpm turbo run build --filter @vitalcv/web → 13/13 tasks
…logy from origin/main reality Truth-contract fix surfaced by deployed-route-registration audit: the prior commit on this branch claimed each canonical handler exists at its target path, but on origin/main only two rows (legacy /api/.well-known/jwks.json + the OS-association manifests) ship today. The remaining 9 canonical handlers live on unmerged PRs #345/#349/#355. Changes: - Prepended "Status — read this first" preamble that names the unmerged PRs and what an external verifier sees from production today (404 on the canonical paths). - Renamed the route-table "Owning PR" column to "Lives on" and annotated each row with whether the handler is on origin/main or on a named unmerged PR. - Noted that the legacy mirror's current Content-Type is application/json (not application/jwk-set+json) — the canonical handler on #349 corrects this. - Clarified that the pinning tests and companion forensics docs referenced in this map ALSO ship on those unmerged PRs, not on origin/main. No code changes. Doc-only.
Adds docs/architecture/final-runtime-reality-state.md, the TASK 7 output of the HARD OPERATIONAL CONVERGENCE wave. Strictly scoped to what is true on apex vitalcv.com RIGHT NOW (origin/main HEAD); excludes roadmap, planned features, in-flight PRs, and theoretical topology. Five required answers, each with file:line attribution: 1. What can institutions verify RIGHT NOW? Apex deploys, legacy JWKS at non-canonical path, ES256 signature oracle at /api/receipts/verify, /api/health config probe, OS association manifests. Five surfaces total. 2. What survives runtime restart RIGHT NOW? All Prisma-persisted state. Does NOT survive: ES256 keypair when env unset, lineageKey/runId continuity (not persisted), receipt issuance records by jti, lane-health snapshots. 3. What is still synthetic RIGHT NOW? /passport sample card (labeled), /api/ingest/[npi] fallback body, AASA advertisement of /verify/* (route absent on main), Macie Miller demo NPI (vitalcv_dev only). 4. What still breaks institutional continuity RIGHT NOW? 9 concrete observable failures: 404 cascade on canonical discovery paths, non-deterministic receipt jti, no lineageKey/runId claims, no replay readers, probe runner unscheduled, clerk.enabled=false, legacy JWKS media-type, OID4VCI credential_endpoint advertising non-existent path, OIDC pointer-not-flow endpoints. 5. What remains before true production-grade verifier infrastructure exists? Tier A: 6 operator-side configuration steps. Tier B: 5-PR merge train (#345, #349, #355, #358, #360). Tier C: 6-7 engineering PRs for replay persistence (per replay-topology-gap-analysis.md §7). Tier D: hygiene fix-ups (some already in flight on #360). No new product concept required at any tier. Truth contract: doc scanned CLEAN. No banned strings, no aspirational claims, no future-state invention.
Adds docs/architecture/final-production-resilience-state.md, the TASK 7 output of the HARD PRODUCTION RESILIENCE WAVE. Strictly scoped to resilience properties of origin/main code (post-#359); excludes roadmap, speculative infrastructure, and unmerged-PR claims. Five required answers, each with file:line attribution: 1. What still breaks under restart? ES256 keypair when RECEIPT_PRIVATE_KEY_JWK unset (operator fix). lineageKey/runId/receipt-by-jti continuity is ABSENT, so restart- resilience is undefined for those properties (no infrastructure to be resilient). 2. What still breaks under deploy propagation? Same keypair issue cascades across deploys. No other documented defect. 3. What still breaks under edge divergence? None currently documented on origin/main. Post-#349, the 1-hour stale-while-revalidate on JWKS / DID / OID4VCI metadata becomes a key-rotation constraint to design around. 4. What continuity is fully durable today? Durable: identity, decision capsules, audit events, verification artifacts, ES256 signature verification (when key configured). Reconstructible: chronology by DecisionCapsule timestamps, NPI -> latest trust state. ABSENT: lineageKey, runId, receipt-by-jti, reconciler, revocation list. 5. What still prevents institutional-grade survivability? Tier A: 5 operator config steps. Tier B: 5-PR merge train. Tier C: 6-7 engineering PRs for replay persistence per replay-topology-gap-analysis.md §7. Tier D (new — origin/main resilience fixes that can land independently): middleware timeout, ingest fallback branch, report cluster timeout, resolver consolidation. Truth contract: doc scanned CLEAN. No banned strings, no aspirational claims. Does not invent resilience features — reports the resilience characteristics of code that exists.
…adiness Pivots the audit lens from institutional infrastructure to product-truth coherence per the user's direction "transition VitalCV from infrastructure-emergence to coherent shippable product." Adds two consolidated docs: 1. docs/architecture/product-completion-audit.md (Phase 1) Classifies every user-facing surface on origin/main (HEAD 39bb65d) as REAL+WORKING / REAL-BUT-DEGRADED / STATIC SHELL / PARTIAL MOCK / BROKEN / ABSENT. Covers marketing entry points, clinician onboarding loop, passport, employer review, issuer flows, verifier surfaces (mostly absent on main), operational self-serve, and critical API paths. Key findings: - Most public marketing surfaces ship truth-honest "foundation preview" copy (onboarding, pricing, docs, status). - /verifier dir exists but is empty -> broken-link cascade if linked. - /verify, /trust, /trust/doctrine, /.well-known/{did,oid-cred-issuer, oid-configuration,trust-register} all absent on main (live on unmerged #345/#349/#355). - /compliance archived only; live link 404s. - /sign-up vs /signup duplication. - LaneHealthMount band reads UNKNOWN until probe runner scheduled. - /api/ingest/[npi] HTTP-200-with-fallback masquerade is a known defect making homepage NPI submit fail cryptically. 2. docs/architecture/ship-readiness-state.md (Phase 8) Six required answers: 1. "What can ship TODAY?" — Clinician readiness preview product: homepage, pricing, docs, status, legal, onboarding, /p/[npi], /review/[entityId], passport (degraded), replay API endpoints. 2. "What must be hidden before shipping?" — Inbound links to /verifier, /verify, /trust, /compliance; demo-grade /issuer/* from public nav; one of /sign-up vs /signup. 3. "What still breaks trust?" — LaneHealthMount UNKNOWN seeds, apex clerk.enabled=false, /api/ingest/[npi] masked-200 client throw, legacy JWKS media-type, "Unavailable" label collision. 4. "What surfaces are operationally believable?" — /onboarding, /status, /docs, /pricing, homepage, replay API. 5. "What is the actual MVP?" — "Clinician readiness preview, source-honest": NPI -> public-source-backed readiness, no credentialing claim, no compliance certification, receipts verifiable via legacy ES256 oracle. 6. "What should NOT be built yet?" — No new replay systems beyond α/β/γ; no continuity reconciler; no UI primitives depending on unmerged stack; no further synthesis/doctrine docs; no writer expansion to other ingest sites; no new feature waves until broken-link cascade closed. Plus a recommended-next-6-PRs list (half-day engineering total) for the broken-link cascade. No new architecture, no schema changes, no new endpoints in any of those 6 PRs. Truth contract: both docs scan CLEAN. No banned phrases, no bare- Verified labels, no aspirational claims. Specific quotes of negative- safety copy (e.g., onboarding's disclaimer) reworded to avoid the banned-phrase scanner false-positive. This commit closes the audit cycle and transitions to product-completion priorities. Per the user's directive, no further synthesis/doctrine/ convergence docs will be generated unless explicitly requested.
Summary
Closes the verifier-continuity gap end-to-end. An external verifier with no privileged access can now:
/.well-known/did.json/.well-known/jwks.json(RFC 8615)/.well-known/openid-credential-issuer/.well-known/trust-register/api/receipt/<npi>All six surfaces share
getPublicKeyJwk()+getIssuerDid()so kid + DID coherence is structural.Surfaces
/.well-known/jwks.jsonapplication/jwk-set+json/.well-known/did.jsondid:web)application/did+json/.well-known/openid-credential-issuerapplication/json/.well-known/trust-registerschema_version: 1)application/json/api/receipt/[npi]application/jwtReceipt endpoint
Signs an ES256 JWT using the same private key whose public component is published at
/.well-known/jwks.json. Payload contains both the W3C VC 2.0 credential block (VitalCVTrustReceipt) and a VitalCV claim block carrying the deterministic replay identity (lineageKey+runId+schemeVersion).Determinism contract:
jti = 'receipt:<replay.runId>'— same evidence snapshot → same jtiiat = lastCheckedAt(seconds) — payload byte-identical across re-issuance for the same snapshotexp = iat + 90 daysSelf-describing:
vcv.jwksUriandvcv.didDocumentUriin the payload point at the well-known surfaces this same PR mounts. Response headers (X-Receipt-Kid,X-Receipt-Issuer) let verifiers short-circuit JWKS lookup if cached.Download mode:
?format=downloadaddsContent-Disposition: attachmentfor offline validation.Cross-surface invariant (pinned)
5 vitest cases for well-known surfaces include a coherence check: all four surfaces agree on issuer DID, JWKS URI, and active kid. The 10-case receipt suite includes an end-to-end verifier flow:
createLocalJWKSet(jwks) + jwtVerify(receipt)— the receipt validates against the JWKS using the same library an external verifier would use.Truth rules
trust-registercarries explicitdisclaimers[]: no implied PSV beyond cited source; signature verification ≠ compliance certification; absence of adverse findings ≠ positive verificationValidation
pnpm turbo run build --filter @vitalcv/web→ 13/13 tasksBackward compatibility
/api/.well-known/jwks.jsonhandler is preservedOut of scope (explicit follow-ups)
/replay/[runId]page +/api/replay/[runId]— Wave 17/trustoverview page — separatedid-configurationlinkage — depends on cross-signing infra