docs(deploy): build artifacts + apex forensics + route ownership map#357
docs(deploy): build artifacts + apex forensics + route ownership map#357ctol3r wants to merge 8 commits 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).
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).
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
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.
…eceipt-by-lineage Stacked on #349 (well-known surfaces + /api/receipt/[npi] + wellKnownIdentity helper). Closes the three remaining items from the "finish verifier continuity" brief that were genuinely net-new relative to the existing PR queue. Mapping the brief's seven items to the queue: 1. /.well-known/jwks.json ✅ already in #349 2. /.well-known/did.json ✅ already in #349 3. /.well-known/openid-credential-issuer ✅ already in #349 4. /.well-known/openid-configuration ✅ THIS PR 5. /trust ✅ THIS PR 6. /verify ✅ already in #345 7. /api/receipt/[lineageKey] ✅ THIS PR (at /by-lineage/<id> because Next.js doesn't allow two slug names at the same path level) New surfaces: apps/web/app/.well-known/openid-configuration/route.ts OIDC Discovery 1.0 courtesy endpoint. VitalCV is a Verifiable Credential issuer (OID4VCI), not a full OIDC Provider. The handler emits honest values for the fields we CAN populate (issuer, jwks_uri, ES256 algorithm, public subject type) and POINTS authorization_endpoint + token_endpoint at the credential-issuer metadata endpoint rather than fabricating OAuth flow URLs. Non-standard `vitalcv:role` extension signals `credential_issuer_only`; `vitalcv:notes` explains the relationship to /.well-known/openid-credential-issuer. "no placeholder values" rule honored: response_types_supported is the empty array (we don't run an authorization flow), not a fictional list. apps/web/app/trust/page.tsx Server-rendered institutional overview. Consumes the same trust-register payload the /.well-known/trust-register handler emits (via in-process dynamic import — no HTTP round-trip). Eight sections in canonical institutional reading order: issuer identity → signing → runtime → launch-spine sources → supported credentials → discovery endpoints → operational doctrine → disclaimers. Monochrome, mono identifiers, left-aligned, no decorative iconography. Each row carries the appropriate data-* attribute for automated inspection. apps/web/app/api/receipt/by-lineage/[lineageKey]/route.ts Receipt retrieval keyed by lineageKey. Requires an NPI as a verifier-supplied claim (?npi=) and cross-checks computeLineageKey(npi) === path.lineageKey before delegating to the existing /api/receipt/[npi] handler. The cross-check means callers cannot probe arbitrary lineageKeys without proof of the underlying subject. 501 (Not Implemented) when no ?npi= is supplied with a structured pointer body explaining that a backend-side lineageKey→NPI index would be required for direct lookup. This is honest, not a placeholder. Path is /by-lineage/<lineageKey> rather than /<lineageKey> because Next.js cannot have two different dynamic-slug names at the same path level ([npi] is already in #349). Tests (apps/web/__tests__/verifier-continuity-completion.test.ts, 14 vitest cases): - openid-configuration: required OIDC fields; empty response_types_supported; vitalcv:role extension; pointer URLs - /trust: server-renders 8 sections with the real trust-register payload; all 4 launch-spine sources visible - receipt-by-lineage: 400 / 501 / 401 / 200 paths all exercised; end-to-end JWT issuance with the cross-check passing - cross-surface coherence: openid-configuration jwks_uri matches JWKS surface; issuer matches did.json id + trust-register - doctrine: banned-strings scan CLEAN across all new surfaces Validation: 14/14 vitest passing; pnpm turbo run build --filter @vitalcv/web → 13/13 tasks; truth-strings CLEAN. After this PR + the existing queue, the brief's success condition ("external verifier can independently discover trust continuity") is met by the seven surfaces enumerated above, all reachable from a single landing point (/trust) plus the four .well-known discovery surfaces.
…e/build-artifact-verification
…inuity routes Integration branch combining #345 (/verify), #349 (well-known + receipt), and #355 (openid-configuration, /trust, receipt-by-lineage). Ran `pnpm turbo run build --filter @vitalcv/web` (13/13 tasks, 33.5s) and inspected `.next/server/app/` to prove every route enumerated in the brief has physical artifacts in the deployable Next.js build output. Doc: BUILD_ARTIFACT_VERIFICATION.md (12 sections, 6KB) §1 source-file presence (pre-build sanity) §2 .next/server/app/.well-known/ tree (5 new + 2 pre-existing) §3 /trust + /verify page artifacts §4 /api/receipt/* route artifacts (npi + by-lineage) §5 pre-rendered .body content samples (proves real institutional data, not placeholders) §6 .meta headers (status + content-type + cache-control per static surface) §7 functions-config-manifest.json entries (9 routes registered) §8 per-route SHA256 + size fingerprints §9 static vs dynamic rendering breakdown §10 deployment readiness checklist (post-merge curl probes) §11 build environment context §12 what this proves + what it does NOT prove Verdict: 9/9 routes built. All artifacts present. Pre-rendered payloads are real (ES256 EC keys, did:web URI, OID4VCI metadata, trust-register schema v1). Content-types RFC-correct. SHAs reproducible. Does NOT prove production deploys this branch — that requires merge + Vercel promotion. See queued DEPLOYMENT_TARGET_TRUTH.md task for that next layer.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: bb99b940d6
ℹ️ 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.
Publish a reachable credential endpoint in metadata
This metadata advertises credential_endpoint as https://<origin>/api/credentials/issue, but this app does not define a corresponding Next route (apps/web/app/api/credentials/issue/route.ts is absent). Any OID4VCI client that follows discovery will hit a 404 on issuance, so the published well-known document currently points verifiers to a non-functional flow.
Useful? React with 👍 / 👎.
|
|
||
| // Delegate to the NPI handler — same JWT signing path, same caching | ||
| // headers, same deterministic jti. | ||
| return receiptByNpi(req, { params: Promise.resolve({ npi }) }); |
There was a problem hiding this comment.
Enforce returned receipt lineage matches requested lineage
After the path/query cross-check passes, this handler delegates directly to the NPI route and returns its JWT unchanged, but that JWT lineage comes from backend passport data (vcv.replay.lineageKey) rather than the validated path lineage. If backend replay data is stale or divergent, callers can request lineage A and receive a signed receipt claiming lineage B, which breaks the route’s own continuity contract for lineage-keyed retrieval.
Useful? React with 👍 / 👎.
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.
Summary
Three deployment-forensics docs at repo root. Doc-only PR; zero code changes.
BUILD_ARTIFACT_VERIFICATION.mdAPEX_DEPLOYMENT_FORENSICS.mdvitalcv.com, and what's the current production state?ROUTE_OWNERSHIP_MAP.mdapps/webvsapps/marketing) owns each of the named routes?Critical findings
.next/server/app/(verified by file inspection + manifest registration + per-route SHA256)vitalcv.com) deploysapps/web— verified by/api/healthreturningservice: "web"NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY/CLERK_SECRET_KEY—/api/healthreportsclerk.enabled: false / mode: "none"and/sign-in500sapps/marketing) is a separate Vercel project on a different domain — never touches apexapps/webexclusively;/verifyexists in both apps but owns different concepts (institutional inspector vs share-link viewer)What this PR proves
apps/web)What this PR does NOT prove
origin/main9eb5cdee, andMERGE_READINESS.md(docs(merge): Tier-1 merge readiness audit (17 PRs, #338-#355) #356) is the runbook for landing the 20-PR queuedpl_*for apex — requires Vercel dashboard access/api/healthoutputTruth rules
Validation
pnpm turbo run build --filter @vitalcv/web→ 13/13 tasks (for the build-artifact doc)https://vitalcv.com/api/healthand/(for the apex forensics doc)apps/web/app/andapps/marketing/app/(for the route ownership doc)Diff scope