Skip to content

feat(verify): /verify trust inspection surface (Wave 9 partial)#345

Open
ctol3r wants to merge 1 commit into
wave/trust-primitives-lane-bfrom
wave/verify-runtime-w9
Open

feat(verify): /verify trust inspection surface (Wave 9 partial)#345
ctol3r wants to merge 1 commit into
wave/trust-primitives-lane-bfrom
wave/verify-runtime-w9

Conversation

@ctol3r
Copy link
Copy Markdown
Owner

@ctol3r ctol3r commented May 12, 2026

Summary

Wave 9 partial — a new verifier-readable page at /verify?npi=:NPI that exposes every trust-bearing field the existing backend already returns, in canonical institutional order, using only the Lane B primitives (#341).

Stacked on #341. No new backend endpoint, no schema changes, no auth changes. Pure inspector surface over the existing GET /api/passport/npi/:npi contract.

Composition

Section Primitive used
Header <TrustHeader variant="SNAPSHOT">
Band <TrustStateBand>
Credentials list <TierBadge> + <CheckedAtStamp> per row
Source coverage list <CheckedAtStamp> per row
Degraded continuity <DegradedStateBanner> per non-checked source, mapping unavailable → A, gated/access-required/pending/review-required/notDecisionGrade → B, issuer-* → E
Issuer attribution <IssuerAttribution> when trustContainer is present
Replay continuity <ReplayLineage> with current snapshot as one entry
Footer <RunIdentity> + lineage chip

Wave 10 (#343) coordination

PassportData.replay lands on #343. This page uses an optional type cast to read .replay if present and falls back to entityId for both runId and lineageKey otherwise. Footer shows scheme · pending-w10 until threaded; switches to scheme · v1 automatically once #343 merges and the upstream contract carries the field.

Truth rules

  • Banned-strings scan: CLEAN
  • All trust labels come from existing canonical primitives; no new copy authored
  • Malformed upstream payload renders as <DegradedStateBanner code="C"> (infrastructure outage) instead of leaking errors

What this PR does NOT do (explicit deferred scope)

  • No new backend endpoint (/api/receipt/[id] is Wave 13)
  • No /replay/[id] or /receipt/[id] pages (Wave 13)
  • No .well-known/* mounts (jwks.json, did.json, openid-credential-issuer, trust.json) — needs backend route work
  • No prior-check history on replay timeline (Wave 6 — backend history query doesn't exist)

Validation

  • Build: pnpm turbo run build --filter @vitalcv/web13/13 tasks, 67s
  • No source files modified — only apps/web/app/verify/page.tsx added

Stacked on #341 (Lane B primitives). A new public verifier-readable
page at /verify?npi=:NPI that exposes every trust-bearing field the
existing backend already knows about, in canonical institutional
order (OBJECT → OWNERSHIP → CHECKED_AT → CHANNEL → REPLAY → RUN_ID),
using only the Lane B primitives.

Composition:
  <TrustHeader>           institutional reading order with PREVIEW /
                          SNAPSHOT / SIGNED variants (SNAPSHOT here)
  <TrustStateBand>        decision-readiness band
  <TierBadge>             per-credential T1-T4 derived from
                          verificationLevel
  <CheckedAtStamp>        per-source + per-credential freshness
  <DegradedStateBanner>   A/B/C/E for any source not in 'checked'
                          state (D = no-adverse-findings rendered
                          inline via TierBadge for OIG clean rows)
  <ReplayLineage>         current snapshot as one entry; prior-check
                          enumeration is Wave 6 scope
  <IssuerAttribution>     when trustContainer manifest present;
                          DID / signer / kid / receiptId surfaced
  <RunIdentity>           visible run id footer chip
  <FormElement>           verifier-facing NPI input

Data source: existing GET /api/passport/npi/:npi (no new backend
endpoint introduced). Server component — fetches at request time
against BACKEND_URL, validates with assertPassportData(), so a
malformed upstream payload renders as a degraded-state banner
instead of leaking error stacks.

Wave-10 dependency note: passport.replay.runId / passport.replay
.lineageKey become available when #343 lands and this branch
rebases. Until then the page falls back to entityId as a visible
stable stand-in. Footer chip shows scheme · pending-w10 until #343
threads through; switches to scheme · v1 automatically when present.

What this PR does NOT do (explicit deferred scope):
  - No new backend endpoint. /api/receipt/[id] and the JWKS validation
    pieces are Wave 13.
  - No /replay/[id] or /receipt/[id] pages — Wave 13.
  - No .well-known/* mounts — separate Wave 9 work.
  - No prior-check history rendering on the replay timeline — Wave 6
    (requires backend history query that does not yet exist).

Validation: pnpm turbo run build --filter @vitalcv/web → 13/13 tasks.
No banned strings. No tests added (page is pure render over the
existing canonical PassportData contract, which is already
test-pinned by passport-contract tests and passport-proxy-routes
tests).
@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 2:15pm
vitalcv Ready Ready Preview, Comment May 12, 2026 2:15pm

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: 8a9f3b9cf7

ℹ️ 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".

id="verify-npi"
name="npi"
defaultValue={npi}
pattern="\d{10}"
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Escape the NPI regex in the input pattern

The input uses pattern="\d{10}", but in a JavaScript string literal \d is parsed as d, so the browser receives d{10} instead of a 10-digit validator. This silently disables the intended client-side guard and allows invalid NPIs through the form UX (server-side validation still catches it later). Use pattern="\\d{10}" or [0-9]{10} so the browser enforces the correct format.

Useful? React with 👍 / 👎.


export default async function VerifyPage({ searchParams }: VerifyPageProps) {
const params = await searchParams;
const npi = (params.npi ?? '').trim();
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 Normalize repeated npi query params before trimming

This assumes searchParams.npi is always a string, but app-router query params can be arrays when the URL repeats a key (for example /verify?npi=1&npi=2). In that case .trim() throws at runtime and the page fails with a 500. Please coerce arrays to a single value (or reject them) before calling string methods.

Useful? React with 👍 / 👎.

label: passport.identity.displayName,
kind: passport.identity.entityType.toLowerCase(),
}}
ownership={{ state: 'UNCLAIMED', claimant: passport.identity.displayName }}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Remove claimant text when rendering UNCLAIMED ownership

The header always sets ownership state to UNCLAIMED while also passing claimant: passport.identity.displayName. That produces contradictory trust output on the same chip (unclaimed plus a claimant identity), which can mislead verifiers about ownership status. For UNCLAIMED, omit claimant unless real claim metadata exists.

Useful? React with 👍 / 👎.

ctol3r pushed a commit that referenced this pull request May 12, 2026
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 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
Two new forensics docs answering the operator's deployment-topology
questions in one place:

APEX_DEPLOYMENT_FORENSICS.md
  - Proves apex (vitalcv.com) deploys apps/web (not apps/marketing)
    via live probe of /api/health returning service: "web"
  - Identifies the two Vercel projects (apex web vs separate
    marketing) and their per-app vercel.json overrides
  - Critical operational finding: apex /api/health reports
    clerk.enabled: false / mode: "none" — the production Vercel
    project is missing NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY +
    CLERK_SECRET_KEY env vars. /sign-in 500s as a result.
  - Independent of the 20-PR merge queue: this Clerk env gap is
    operator-side configuration, not code
  - 10 sections + recommendations

ROUTE_OWNERSHIP_MAP.md
  - Proves every named verifier route lives in apps/web (not
    apps/marketing) by filesystem inspection
  - /verify exists in both apps but they own different concepts:
    apps/web/app/verify/page.tsx = institutional inspector (#345),
    apps/marketing/app/verify/[shareId]/page.tsx = share-link
    viewer (different domain, different concern)
  - Eight of nine institutional routes are exclusive to apps/web
  - Confirms: when the merge train lands, every new route deploys
    automatically to apex — no project-rebinding, no domain
    reconfig, no migration required
  - 9 sections + summary table

Together with the existing BUILD_ARTIFACT_VERIFICATION.md
(physical build output) and MERGE_READINESS.md (#356, merge-train
sequencing), this PR is now the complete operator-facing answer
to: "what does apex serve, what's the build output, what's the
merge order, and what's the deployment risk?"

No code changes. Three doc files at repo root.
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
…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