Skip to content

docs(deploy): build artifacts + apex forensics + route ownership map#357

Open
ctol3r wants to merge 8 commits into
mainfrom
wave/build-artifact-verification
Open

docs(deploy): build artifacts + apex forensics + route ownership map#357
ctol3r wants to merge 8 commits into
mainfrom
wave/build-artifact-verification

Conversation

@ctol3r
Copy link
Copy Markdown
Owner

@ctol3r ctol3r commented May 13, 2026

Summary

Three deployment-forensics docs at repo root. Doc-only PR; zero code changes.

Doc Question it answers
BUILD_ARTIFACT_VERIFICATION.md Are the verifier-continuity routes physically present in the Next.js build output?
APEX_DEPLOYMENT_FORENSICS.md Which Next app deploys to vitalcv.com, and what's the current production state?
ROUTE_OWNERSHIP_MAP.md Which app (apps/web vs apps/marketing) owns each of the named routes?

Critical findings

Finding Severity
All 9 verifier-continuity routes built into .next/server/app/ (verified by file inspection + manifest registration + per-route SHA256) OK
Apex (vitalcv.com) deploys apps/web — verified by /api/health returning service: "web" OK
Apex Vercel project missing NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY / CLERK_SECRET_KEY/api/health reports clerk.enabled: false / mode: "none" and /sign-in 500s HIGH — operator-side env config needed
Marketing app (apps/marketing) is a separate Vercel project on a different domain — never touches apex OK
Every named institutional route lives in apps/web exclusively; /verify exists in both apps but owns different concepts (institutional inspector vs share-link viewer) OK

What this PR proves

  • The institutional convergence code is physically present in the Next build when the relevant PRs are integrated
  • The institutional convergence code targets exactly the runtime that apex serves (apps/web)
  • When the merge train lands, the new routes will deploy to apex automatically with no project-rebinding
  • One operator-side gap exists independent of the merge queue: Clerk env vars on the apex Vercel project

What this PR does NOT prove

  • That production currently serves the merged code — production is still on origin/main 9eb5cdee, and MERGE_READINESS.md (docs(merge): Tier-1 merge readiness audit (17 PRs, #338-#355) #356) is the runbook for landing the 20-PR queue
  • The exact Vercel project ID / dpl_* for apex — requires Vercel dashboard access
  • Which env vars ARE set on the apex Vercel project — only inferred from /api/health output

Truth rules

  • Banned-strings scan: CLEAN across all three docs
  • No new product claims; pure deployment forensics

Validation

  • Build: pnpm turbo run build --filter @vitalcv/web → 13/13 tasks (for the build-artifact doc)
  • Live probes against https://vitalcv.com/api/health and / (for the apex forensics doc)
  • Filesystem inspection of apps/web/app/ and apps/marketing/app/ (for the route ownership doc)

Diff scope

  • 3 new docs at repo root (369 + 314 + 244 = 927 lines total)
  • 0 code modifications

tol3r added 7 commits May 12, 2026 05:44
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.
…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.
@vercel
Copy link
Copy Markdown

vercel Bot commented May 13, 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 13, 2026 3:28am
vitalcv Ready Ready Preview, Comment May 13, 2026 3:28am

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: 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`,
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 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 }) });
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 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.
@ctol3r ctol3r changed the title docs(build): physical build-artifact verification for 9 verifier-continuity routes docs(deploy): build artifacts + apex forensics + route ownership map May 13, 2026
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