Skip to content

aidbox-health-card-issue: spec-conformant SMART Health Cards + QR/file/viewer#39

Merged
spicyfalafel merged 13 commits into
mainfrom
health-card-conformance-delivery
Jul 21, 2026
Merged

aidbox-health-card-issue: spec-conformant SMART Health Cards + QR/file/viewer#39
spicyfalafel merged 13 commits into
mainfrom
health-card-conformance-delivery

Conversation

@spicyfalafel

Copy link
Copy Markdown
Contributor

Upgrades the aidbox-health-card-issue example so issued cards pass strict SMART Health Cards verification, and adds the delivery surfaces that were missing (QR, .smart-health-card file) plus an in-browser verifier.

Conformance fixes

The previous verifiableCredential did not verify. Fixed (per spec.smarthealth.cards + HL7 IG v1.0.0):

  • Real DEFLATE before signingSignJWT set zip:"DEF" but never compressed the payload; now deflateRaw + CompactSign.
  • kid = base64url SHA-256 JWK thumbprint (RFC 7638) — read from the JWK so the signing kid always matches the published JWKS.
  • resource:N refsBundle.entry.fullUrl + all Reference.reference rewritten to short resource:0/1….
  • iss matches the JWKS location; JWKS op made public (allow policy) + CORS (Aidbox owns its own /.well-known/jwks.json, so ours stays namespaced under the app path).
  • includeIdentityClaim treated as a repeating string of claim paths (not boolean).

Features

  • Accept both https://smarthealth.cards#covid19 (VCI) and generic Immunization/Observation credentialType (uri/dateTime datatypes).
  • Delivery: shc:/ QR (numeric-mode encoder, versions ≤27), .smart-health-card download, /demo/issue + /demo/patients, and an Issue/Verify viewer (WebCrypto ES256 + deflate-raw).
  • README with sequence diagrams, adr/005-delivery-and-conformance.md; key-utils retired.

Verification

  • Live E2E through Aidbox: $health-cards-issueverifiableCredential; public JWKS; verified as a real verifier (read iss → fetch its JWKS → ES256 signature valid).
  • Bundle audit: 0 id / 0 text / 0 meta / 0 Coding.display; collection + resource:0/1; fhirVersion 4.0.1.
  • tsc build + lint clean; offline smoke (24 checks) pass.

Notes / scope

  • Local iss is http://localhost:8080 (spec wants https + no trailing slash) — documented local-dev deviation.
  • credentialValueSet accepted but not filtered; resourceLink OUT param and formal VCI $validate left out of scope.

🤖 Generated with Claude Code

spicyfalafel and others added 13 commits July 20, 2026 18:00
…e/viewer

Make issued cards pass strict SMART Health Cards verification and add the
missing delivery surfaces (QR, .smart-health-card file) plus an in-browser
verifier.

Conformance fixes:
- Actually DEFLATE the JWS payload before signing (SignJWT declared zip:DEF but
  never compressed); now deflateRaw + CompactSign.
- kid = base64url SHA-256 JWK thumbprint (RFC 7638), read from the JWK so the
  signing kid always matches the published JWKS.
- Minify bundle refs to short resource:N URIs (fullUrl + Reference.reference).
- iss now matches the JWKS location; JWKS op made public (allow policy) + CORS.
- includeIdentityClaim treated as repeating string claim paths (not boolean).

Features:
- Accept both #covid19 (VCI) and generic Immunization/Observation credentialType
  (uri/dateTime datatypes).
- Delivery: shc:/ QR (numeric-mode encoder), .smart-health-card download,
  /demo/issue + /demo/patients, and an Issue/Verify viewer (WebCrypto ES256).
- README with sequence diagrams + ADR-005; retire key-utils.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… drop dangling refs

Follow-up conformance hardening after comparing against the official
health-cards-dev-tools validator and reference issuers (dvci, kill-the-clipboard):

- QR: switch the encoder to error-correction level L (EC-M forced version 24;
  the validator rejects QR > version 22). A real card now fits ~v17.
- Compress the JWS payload at DEFLATE level 9 (card shrinks ~1037 -> ~708 chars).
- Minification: drop empty elements (""/[]/{}) and dangling references (a card
  reference must resolve to a resource:N URI); keep Reference.display.
- README: shorten diagram labels, JWKS note as a list, drop Layout + ADR link.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…-link example

The smart-health-link example uses aidbox 8080 + app 3000; this example now
uses aidbox 8081 + app 3001 (app internal port + Aidbox App endpoint + iss all
updated) so both can run at the same time.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ssue tab

The Issue tab called the unauth /demo/issue stand-in without showing the real
FHIR operation. Add a panel rendering the exact POST
/fhir/Patient/{id}/$health-cards-issue request (auth header, Parameters body)
and its Parameters { verifiableCredential } response.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ADME cleanup

- resourceLink (OUT): map each minified resource:N entry to its live FHIR URL
  (bundledResource -> hostedResource, vcIndex 0); reuses the bundle-builder
  refMap; hostedResource base from FHIR_PUBLIC_BASE_URL (default: iss origin + /fhir).
- Viewer: expose _since on the Issue tab (forwarded through /demo/issue), and
  show resourceLink in the $health-cards-issue response panel.
- README/ADR: drop resourceLink from out-of-scope; document credentialType
  (#covid19 -> CVX) mapping; restore features frontmatter; link the
  smart-health-link example.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
resourceLink.hostedResource is always derived from the issuer origin + /fhir
(one less knob). Revert the README H1 to 'SMART Health Cards Issue Operation'.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…package

- credentialValueSet (IN): filter resources by content — keep only those whose
  code is a member of the ValueSet, via Aidbox ValueSet/$validate-code.
- Load the real hl7.fhir.uv.shc-vaccination#1.0.0 package via
  BOX_BOOTSTRAP_FHIR_PACKAGES (':'-separated) for its immunization-all-cvx
  ValueSet — no hand-authored ValueSet.
- Viewer: add a credentialValueSet dropdown (forwarded through /demo/issue,
  shown in the operation request panel).
- Drop the filterByValueSet stub; remove credentialValueSet from out-of-scope.

Verified: covid19 + the CVX ValueSet issues a card (CVX 208 is a member);
Observation + the same ValueSet filters the LOINC lab out.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…zation for the credentialValueSet demo

- Viewer Issue form: credentialType, credentialValueSet, includeIdentityClaim
  are now repeatable (+ buttons); credentialValueSet + includeIdentityClaim live
  under an Advanced section. Arrays are forwarded through /demo/issue and shown
  in the operation request panel.
- credentialValueSet is now string[] end to end (multiple = logical AND).
- init-bundle: add a SNOMED-coded Immunization so credentialValueSet filtering
  between two immunizations is demonstrable (CVX kept, SNOMED filtered by the
  CVX ValueSet — validated locally).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Verify result now explains what happened (6-step check with pass/fail marks)
and shows what's inside the JWS: decoded header (alg/zip/kid), payload claims
(iss, nbf as a date, exp, vc.type, fhirVersion), and the FHIR bundle — instead of
a single opaque 'signed by …' line.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…option

SnomedVaccineCodes is a SNOMED descendent-of filter that the configured external\ntx server doesn't resolve here (returns false for every code), so it only ever\nproduced empty results and confused the AND semantics. Keep immunization-all-cvx.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…uct code

The second immunization used SNOMED 86198006 (Influenza vaccine), which is NOT a\ndescendant of 787859002 (Vaccine product), so SnomedVaccineCodes never matched it\nand that credentialValueSet option always returned empty. Use 1119349007\n(COVID-19 mRNA vaccine), a real Vaccine-product descendant the external tx\nresolves. Re-add SnomedVaccineCodes to the viewer — now Immunization + that\nValueSet keeps only the SNOMED shot, Immunization + immunization-all-cvx keeps\nonly the CVX shot.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… $fhir-package-install

Use the expected value sets: install cards.smarthealth.terminology (the official\nSHC terminology, not on any FHIR registry) via the $fhir-package-install operation\nin the init bundle, passing the package URL directly\n(https://terminology.smarthealth.cards/package.tgz) — no tarball committed or\nmounted. Drop hl7.fhir.uv.shc-vaccination from BOX_BOOTSTRAP.

The viewer's credentialValueSet now offers immunization-covid-cvx (the COVID-\nspecific, enumerated CVX set — validated locally). SNOMED value sets are dropped:\nthey enumerate SNOMED extension codes the configured external tx doesn't resolve.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A card that exceeds single-QR capacity (> version 22) is not an issuance
error — the card is valid and still delivered via file/JWS. Show that note
in the QR caption instead of the bottom error box; real issuance failures
stay in the error box.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@spicyfalafel
spicyfalafel merged commit 5b0bbd4 into main Jul 21, 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.

1 participant