Skip to content

feat(verifier): well-known discovery surfaces + signed VC 2.0 receipt endpoint#349

Open
ctol3r wants to merge 2 commits into
mainfrom
wave/well-known-w9
Open

feat(verifier): well-known discovery surfaces + signed VC 2.0 receipt endpoint#349
ctol3r wants to merge 2 commits into
mainfrom
wave/well-known-w9

Conversation

@ctol3r
Copy link
Copy Markdown
Owner

@ctol3r ctol3r commented May 12, 2026

Summary

Closes the verifier-continuity gap end-to-end. An external verifier with no privileged access can now:

  1. Discover the issuer at /.well-known/did.json
  2. Resolve key material at /.well-known/jwks.json (RFC 8615)
  3. Read OID4VCI metadata at /.well-known/openid-credential-issuer
  4. Inspect the institutional manifest at /.well-known/trust-register
  5. Fetch a signed receipt at /api/receipt/<npi>
  6. Cryptographically validate the receipt against the JWKS

All six surfaces share getPublicKeyJwk() + getIssuerDid() so kid + DID coherence is structural.

Surfaces

Surface Spec Content-Type
/.well-known/jwks.json RFC 8615 + RFC 7517 application/jwk-set+json
/.well-known/did.json W3C DID Core (did:web) application/did+json
/.well-known/openid-credential-issuer OID4VCI (drafts 11 + 13+) application/json
/.well-known/trust-register VitalCV institutional manifest (schema_version: 1) application/json
/api/receipt/[npi] ES256 JWT, W3C VC 2.0 application/jwt

Receipt 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 jti
  • iat = lastCheckedAt (seconds) — payload byte-identical across re-issuance for the same snapshot
  • exp = iat + 90 days

Self-describing: vcv.jwksUri and vcv.didDocumentUri in 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=download adds Content-Disposition: attachment for 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

  • Banned-strings scan: CLEAN
  • trust-register carries explicit disclaimers[]: no implied PSV beyond cited source; signature verification ≠ compliance certification; absence of adverse findings ≠ positive verification
  • No fake compliance claims

Validation

  • Targeted vitest: 15/15 passing (5 well-known + 10 receipt)
  • Full build: pnpm turbo run build --filter @vitalcv/web13/13 tasks
  • Diff scope: 6 new route files + 1 helper module + 2 test files; zero existing files modified

Backward compatibility

  • Pre-existing /api/.well-known/jwks.json handler is preserved
  • All new routes are purely additive

Out of scope (explicit follow-ups)

  • /replay/[runId] page + /api/replay/[runId] — Wave 17
  • /trust overview page — separate
  • Middleware 404-not-500 doctrine fix — separate
  • DIF did-configuration linkage — depends on cross-signing infra

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
@vercel
Copy link
Copy Markdown

vercel Bot commented May 12, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
vcv-web Ready Ready Preview, Comment May 12, 2026 10:13pm
vitalcv Ready Ready Preview, Comment May 12, 2026 10:13pm

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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`,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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.
@ctol3r ctol3r changed the title feat(verifier): RFC-correct well-known discovery surfaces (Wave 9) feat(verifier): well-known discovery surfaces + signed VC 2.0 receipt endpoint May 12, 2026
ctol3r pushed a commit that referenced this pull request May 13, 2026
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
ctol3r pushed a commit that referenced this pull request May 13, 2026
…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.
ctol3r pushed a commit that referenced this pull request May 13, 2026
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.
ctol3r pushed a commit that referenced this pull request May 13, 2026
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.
ctol3r pushed a commit that referenced this pull request May 13, 2026
…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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants