From 31625d94488441158f79ea2c32444ecd782d0cf1 Mon Sep 17 00:00:00 2001 From: spicyfalafel <58147555+spicyfalafel@users.noreply.github.com> Date: Fri, 24 Jul 2026 12:48:17 +0200 Subject: [PATCH 01/22] [docs] Add SMART Health Cards ($health-cards-issue) page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Document the native $health-cards-issue operation: issuer key configuration, the input parameters (credentialType, credentialValueSet card-level AND, includeIdentityClaim, _since), the JWKS verification endpoint, access control, and the error responses. Link it from SUMMARY under REST API → Other. --- SUMMARY.md | 1 + docs/api/rest-api/other/smart-health-cards.md | 253 ++++++++++++++++++ 2 files changed, 254 insertions(+) create mode 100644 docs/api/rest-api/other/smart-health-cards.md diff --git a/SUMMARY.md b/SUMMARY.md index 8a4d9ccb1..2420cde62 100644 --- a/SUMMARY.md +++ b/SUMMARY.md @@ -178,6 +178,7 @@ * [Aidbox and FHIR formats](api/rest-api/other/aidbox-and-fhir-formats.md) * [Capability Statement](api/rest-api/other/capability-statement.md) * [$document](api/rest-api/other/document.md) + * [SMART Health Cards](api/rest-api/other/smart-health-cards.md) * [Observation/$lastn](api/rest-api/other/observation-lastn.md) * [$validate](api/rest-api/other/validate.md) * [SQL endpoints](api/rest-api/other/sql-endpoints.md) diff --git a/docs/api/rest-api/other/smart-health-cards.md b/docs/api/rest-api/other/smart-health-cards.md new file mode 100644 index 000000000..a37afad56 --- /dev/null +++ b/docs/api/rest-api/other/smart-health-cards.md @@ -0,0 +1,253 @@ +--- +description: >- + Issue signed SMART Health Cards from a patient's clinical data with the + $health-cards-issue operation. +--- + +# SMART Health Cards + +Since the 2607 version, Aidbox can issue [SMART Health Cards](https://smarthealth.cards/): verifiable health credentials packaged as a signed, compact JSON Web Signature (JWS) that a patient presents as a QR code or file. The [`$health-cards-issue`](https://spec.smarthealth.cards/#via-fhir-health-cards-issue-operation) operation gathers the patient's clinical resources, minifies them into a `verifiableCredential` Bundle, and signs it with the issuer's ES256 key. + +Aidbox publishes the verification public key at a JWKS endpoint, so any SMART Health Cards verifier can validate the cards. + +## Configuration + +The operation signs cards with an issuer **EC P-256 private key**, set as a PEM (PKCS#8) in `module.health-cards-links.issuer-private-key` (env `BOX_MODULE_HEALTH_CARDS_LINKS_ISSUER_PRIVATE_KEY`). You configure only the private key; Aidbox derives the public key, its `kid` ([RFC 7638](https://www.rfc-editor.org/rfc/rfc7638) JWK thumbprint) and the JWKS from it. Until it is set the operation returns `422` and the JWKS endpoint `404`. + +Generate a key with OpenSSL: + +```bash +openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out issuer.pem +``` + +{% hint style="warning" %} +Keep the private key secret and back it up. Rotating it changes the `kid`, so cards signed with the previous key stop verifying against the published JWKS. +{% endhint %} + +## Issuing a card + +```http +POST /fhir/Patient//$health-cards-issue +``` + +The body is a `Parameters` resource. `credentialType` is required; the other parameters are optional. + +| Parameter | Type | Description | +| ---------------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------- | +| `credentialType` | uri / string | Resource types the card should carry. Required, repeatable. See [credentialType](smart-health-cards.md#credentialtype). | +| `credentialValueSet` | uri / string | Restrict included resources to members of a ValueSet. Repeatable. See [credentialValueSet](smart-health-cards.md#credentialvalueset). | +| `includeIdentityClaim` | string / boolean | Which `Patient` identity fields to include. See [includeIdentityClaim](smart-health-cards.md#includeidentityclaim). | +| `_since` | dateTime / instant | Only include resources modified at or after this instant (`_lastUpdated ge`). | + +### Example request + +```http +POST /fhir/Patient/pt-1/$health-cards-issue +``` + +```json +{ + "resourceType": "Parameters", + "parameter": [ + { + "name": "credentialType", + "valueUri": "Immunization" + } + ] +} +``` + +### Example response + +```json +{ + "resourceType": "Parameters", + "parameter": [ + { + "name": "verifiableCredential", + "valueString": "eyJ6aXAiOiJERUYiLCJhbGciOiJFUzI1NiIsImtpZCI6Ii4uLiJ9.." + }, + { + "name": "resourceLink", + "part": [ + { "name": "vcIndex", "valueInteger": 0 }, + { "name": "bundledResource", "valueUri": "resource:0" }, + { + "name": "hostedResource", + "valueUri": "https:///fhir/Patient/pt-1" + } + ] + }, + { + "name": "resourceLink", + "part": [ + { "name": "vcIndex", "valueInteger": 0 }, + { "name": "bundledResource", "valueUri": "resource:1" }, + { + "name": "hostedResource", + "valueUri": "https:///fhir/Immunization/imm-1" + } + ] + } + ] +} +``` + +`verifiableCredential.valueString` is the signed SMART Health Card JWS: a raw-DEFLATE-compressed (`zip:"DEF"`) payload signed with ES256, whose protected header carries the issuer `kid`. Each `resourceLink` maps a bundled resource (`resource:N`) to its source Aidbox resource (`hostedResource`). + +### Repeatable parameters + +`credentialType`, `credentialValueSet`, and `includeIdentityClaim` take one `parameter` entry per value. This request asks for a card carrying both immunizations and observations (`credentialType` as a logical AND), limits the identity claim to the name, and skips resources modified before a cutoff: + +```json +{ + "resourceType": "Parameters", + "parameter": [ + { "name": "credentialType", "valueUri": "Immunization" }, + { "name": "credentialType", "valueUri": "Observation" }, + { "name": "includeIdentityClaim", "valueString": "Patient.name" }, + { "name": "_since", "valueInstant": "2024-01-01T00:00:00Z" } + ] +} +``` + +Add a `credentialValueSet` entry the same way to filter the kept resources, for example `https://terminology.smarthealth.cards/ValueSet/immunization-covid-cvx`. + +## Input parameters + +### credentialType + +The operation accepts any FHIR resource type in the Patient compartment, such as `Immunization`, `Observation`, or `Condition`. It searches the patient's compartment for that type and adds the matching resources to the card. + +Per the [spec](https://spec.smarthealth.cards/), `credentialType` is required (at least one). Aidbox combines multiple values with logical AND: the card carries every requested type the patient has. Aidbox rejects a missing or unsupported `credentialType` with `400`. + +Every issued card's `vc.type` array includes `https://smarthealth.cards#health-card`. The deprecated type URIs `#covid19`, `#immunization`, and `#laboratory` still work; see [Legacy credential types](smart-health-cards.md#legacy-credential-types). + +### credentialValueSet + +Restricts the card's resources to members of the given ValueSet(s). Aidbox checks each resource's clinical code (`Immunization.vaccineCode`, `Observation.code`, `MedicationRequest.medicationCodeableConcept`, `*.type`) with [`$validate-code`](validate.md). + +Multiple `credentialValueSet` parameters apply as a **card-level logical AND**: Aidbox keeps a resource when it belongs to at least one supplied ValueSet, and issues the card only when the kept resources cover **every** supplied ValueSet. If some ValueSet has no matching resource, Aidbox cannot satisfy the request and returns `404`. Use this to request, for example, a card that holds both a COVID-19 vaccine **and** an mpox vaccine. + +{% hint style="info" %} +Aidbox's terminology must resolve the ValueSet. A non-member code is filtered out; a ValueSet that cannot be resolved fails the request with `400` rather than a misleading empty result. +{% endhint %} + +### includeIdentityClaim + +Controls which `Patient` identity fields the card carries: + +| Value | Effect | +| ----------------------------------------------------------- | ------------------------------------------------------------------------------- | +| omitted or `true` | Default claims: `Patient.name` (family and given only) and `Patient.birthDate`. | +| a list of strings, e.g. `Patient.name`, `Patient.birthDate` | Only the named fields. | +| `false` | Omit the `Patient` from the card entirely. | + +### \_since + +The operation includes only resources with `_lastUpdated` at or after the given instant, for example `2024-01-01T00:00:00Z`. + +## Verifying cards (JWKS) + +Aidbox publishes the issuer's public key as a JWK Set at a public endpoint: + +```http +GET /health-cards/.well-known/jwks.json +``` + +```json +{ + "keys": [ + { + "kty": "EC", + "crv": "P-256", + "x": "vbo4liL4JMgrGcYgAhVWreZPFMisG65EhM0p3Vdgzi8", + "y": "U3_QRvRc12uGpFiwBlX43vXbqYoVwGQ2obGZgF2gCYc", + "kid": "jDfy60_p2Gyvcl_sXayw0FcSECzZtrC7WUKrVaigVYI", + "use": "sig", + "alg": "ES256" + } + ] +} +``` + +The `kid` matches the `kid` in the JWS protected header, so a verifier can select the right key. The card issuer identifier (the `iss` claim) is `/health-cards`, and per the SMART Health Cards spec its key set lives at `/.well-known/jwks.json`. The endpoint returns `404` while the issuer key is not configured. + +## Access control + +`$health-cards-issue` reads the patient's data by re-dispatching internal FHIR requests through the regular request pipeline, so Aidbox enforces the caller's [Access Policies](../../../access-control/authorization/access-policies.md). It never issues a card for data the caller may not read, and propagates a denied internal read verbatim (for example `403`). + +For a single call, the operation issues these internal requests, each authorized against the caller's policies: + +- `GET /fhir/Patient/{id}?_elements=name,birthDate` reads the identity claims as `FhirRead` (skipped when `includeIdentityClaim` is `false`; `_elements` lists the requested claim fields). +- `GET /fhir/{type}?{compartment-param}=Patient/{id}` searches each requested `credentialType` as `FhirSearch`, using the type's Patient-compartment search parameter (`patient` for `Immunization`, `subject` for `Observation` and `Condition`, and so on) and following `next` links to page through every match. Adds `&_lastUpdated=ge{_since}` when `_since` is set. + +Grant the caller a policy for each. For the type searches, link an [AccessPolicy](../../../access-control/authorization/access-policies.md) to `Operation/FhirSearch` and restrict it inside `matcho`. For example, to let one client build cards from `Immunization` and `Observation`: + +```json +{ + "resourceType": "AccessPolicy", + "id": "health-cards-issuer-search", + "engine": "matcho", + "link": [{ "reference": "Operation/FhirSearch" }], + "matcho": { + "client": { "id": "my-client-id" }, + "params": { + "resource/type": { + "$enum": ["Immunization", "Observation"] + } + } + } +} +``` + +Add each resource type you request to the `$enum`. For the `Patient` read, add a policy linked to `Operation/FhirRead`: + +```json +{ + "resourceType": "AccessPolicy", + "id": "health-cards-issuer-read-patient", + "engine": "matcho", + "link": [{ "reference": "Operation/FhirRead" }], + "matcho": { + "client": { "id": "my-client-id" }, + "params": { "resource/type": "Patient" } + } +} +``` + +See [more AccessPolicy examples](../../../tutorials/security-access-control-tutorials/accesspolicy-examples.md). + +### Organization scope (OrgBAC) + +Under [organization-based access control](../../../access-control/authorization/scoped-api/organization-based-hierarchical-access-control.md), the operation is also served at: + +```http +POST /Organization//fhir/Patient//$health-cards-issue +``` + +It issues a card only from data in that organization. The target `Patient` and every internal read run inside the organization's compartment, so a `Patient` that belongs to another organization is not visible and the request returns `403`. The `resourceLink.hostedResource` URLs are scoped to the same `/Organization//fhir` base. + +## Errors + +The operation returns every error as an `OperationOutcome`. + +| Status | When | +| ------ | --------------------------------------------------------------- | +| `422` | The issuer private key is not configured. | +| `400` | `credentialType` is missing or unsupported. | +| `400` | A requested `credentialValueSet` could not be resolved. | +| `404` | No resources match the requested credential type or value sets. | +| `403` | An Access Policy denies reading the patient's data. | + +## Legacy credential types + +Before v1.4.0, SMART Health Cards classified a card by a top-level type URI. The framework deprecated these in favor of classifying a card by its contents, but Aidbox accepts them as `credentialType` values: + +| Value | Meaning | +| ---------------------------------------- | ---------------------------------------------------------------------- | +| `https://smarthealth.cards#covid19` | COVID-19 `Immunization`s only, filtered to COVID-19 CVX vaccine codes. | +| `https://smarthealth.cards#immunization` | All `Immunization`s. | +| `https://smarthealth.cards#laboratory` | `Observation`s. | + +Aidbox also stamps these URIs into the card's `vc.type` from its contents: `#immunization` when the card carries immunizations, `#covid19` for COVID-19 immunizations, and `#laboratory` when it carries observations. A card built only from other resource types carries just `#health-card`. From 99a8f7966e9684b04e4d6f4036618a11a55bbd9f Mon Sep 17 00:00:00 2001 From: spicyfalafel <58147555+spicyfalafel@users.noreply.github.com> Date: Mon, 3 Aug 2026 10:56:24 +0200 Subject: [PATCH 02/22] [docs] OrgBAC: document $health-cards-issue org-scoped route --- .../organization-based-hierarchical-access-control.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/access-control/authorization/scoped-api/organization-based-hierarchical-access-control.md b/docs/access-control/authorization/scoped-api/organization-based-hierarchical-access-control.md index ed75bd506..5d85a6cb6 100644 --- a/docs/access-control/authorization/scoped-api/organization-based-hierarchical-access-control.md +++ b/docs/access-control/authorization/scoped-api/organization-based-hierarchical-access-control.md @@ -291,6 +291,16 @@ GET /Organization//fhir/Composition/$document See also [$document endpoint](../../../api/rest-api/other/document.md) +### $health-cards-issue + +``` +POST /Organization//fhir/Patient//$health-cards-issue +``` + +Issues a SMART Health Card scoped to the organization: the target `Patient` and every internal read run inside the organization's compartment. + +See also [SMART Health Cards](../../../api/rest-api/other/smart-health-cards.md) + ### History Resource full history From e3d5787e7311076483e5a1125356a9758a339038 Mon Sep 17 00:00:00 2001 From: spicyfalafel <58147555+spicyfalafel@users.noreply.github.com> Date: Mon, 3 Aug 2026 10:58:43 +0200 Subject: [PATCH 03/22] [docs] SMART Health Cards: available since 2608 --- docs/api/rest-api/other/smart-health-cards.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api/rest-api/other/smart-health-cards.md b/docs/api/rest-api/other/smart-health-cards.md index a37afad56..94dea7389 100644 --- a/docs/api/rest-api/other/smart-health-cards.md +++ b/docs/api/rest-api/other/smart-health-cards.md @@ -6,7 +6,7 @@ description: >- # SMART Health Cards -Since the 2607 version, Aidbox can issue [SMART Health Cards](https://smarthealth.cards/): verifiable health credentials packaged as a signed, compact JSON Web Signature (JWS) that a patient presents as a QR code or file. The [`$health-cards-issue`](https://spec.smarthealth.cards/#via-fhir-health-cards-issue-operation) operation gathers the patient's clinical resources, minifies them into a `verifiableCredential` Bundle, and signs it with the issuer's ES256 key. +Since the 2608 version, Aidbox can issue [SMART Health Cards](https://smarthealth.cards/): verifiable health credentials packaged as a signed, compact JSON Web Signature (JWS) that a patient presents as a QR code or file. The [`$health-cards-issue`](https://spec.smarthealth.cards/#via-fhir-health-cards-issue-operation) operation gathers the patient's clinical resources, minifies them into a `verifiableCredential` Bundle, and signs it with the issuer's ES256 key. Aidbox publishes the verification public key at a JWKS endpoint, so any SMART Health Cards verifier can validate the cards. From 01c954712e02344afa44d42817150a75112ca810 Mon Sep 17 00:00:00 2001 From: spicyfalafel <58147555+spicyfalafel@users.noreply.github.com> Date: Mon, 3 Aug 2026 11:02:37 +0200 Subject: [PATCH 04/22] [docs] OrgBAC $health-cards-issue: available since 2608 --- .../organization-based-hierarchical-access-control.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/access-control/authorization/scoped-api/organization-based-hierarchical-access-control.md b/docs/access-control/authorization/scoped-api/organization-based-hierarchical-access-control.md index 5d85a6cb6..10e317079 100644 --- a/docs/access-control/authorization/scoped-api/organization-based-hierarchical-access-control.md +++ b/docs/access-control/authorization/scoped-api/organization-based-hierarchical-access-control.md @@ -297,7 +297,7 @@ See also [$document endpoint](../../../api/rest-api/other/document.md) POST /Organization//fhir/Patient//$health-cards-issue ``` -Issues a SMART Health Card scoped to the organization: the target `Patient` and every internal read run inside the organization's compartment. +Issues a SMART Health Card scoped to the organization: the target `Patient` and every internal read run inside the organization's compartment. Available starting from version 2608. See also [SMART Health Cards](../../../api/rest-api/other/smart-health-cards.md) From 91375cb31d8d7f15b50ca8b5c39b5fcce502d844 Mon Sep 17 00:00:00 2001 From: spicyfalafel <58147555+spicyfalafel@users.noreply.github.com> Date: Mon, 3 Aug 2026 11:05:25 +0200 Subject: [PATCH 05/22] [docs] Move SMART Health Cards to the end of the Other list --- SUMMARY.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SUMMARY.md b/SUMMARY.md index 2420cde62..41d160b00 100644 --- a/SUMMARY.md +++ b/SUMMARY.md @@ -178,7 +178,6 @@ * [Aidbox and FHIR formats](api/rest-api/other/aidbox-and-fhir-formats.md) * [Capability Statement](api/rest-api/other/capability-statement.md) * [$document](api/rest-api/other/document.md) - * [SMART Health Cards](api/rest-api/other/smart-health-cards.md) * [Observation/$lastn](api/rest-api/other/observation-lastn.md) * [$validate](api/rest-api/other/validate.md) * [SQL endpoints](api/rest-api/other/sql-endpoints.md) @@ -189,6 +188,7 @@ * [$meta](api/rest-api/other/meta.md) * [$meta-add](api/rest-api/other/meta-add.md) * [$meta-delete](api/rest-api/other/meta-delete.md) + * [SMART Health Cards](api/rest-api/other/smart-health-cards.md) * [Bulk API](api/bulk-api/README.md) * [Configure Access Policies for Bulk API](api/bulk-api/configure-access-policies-for-bulk-api.md) * [$dump](api/bulk-api/dump.md) From dc4c177ab671b73a20670d15f6cbc0c065564b27 Mon Sep 17 00:00:00 2001 From: spicyfalafel <58147555+spicyfalafel@users.noreply.github.com> Date: Mon, 3 Aug 2026 11:06:59 +0200 Subject: [PATCH 06/22] [docs] SMART Health Cards: trim the Configuration paragraph --- docs/api/rest-api/other/smart-health-cards.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api/rest-api/other/smart-health-cards.md b/docs/api/rest-api/other/smart-health-cards.md index 94dea7389..4c968f489 100644 --- a/docs/api/rest-api/other/smart-health-cards.md +++ b/docs/api/rest-api/other/smart-health-cards.md @@ -12,7 +12,7 @@ Aidbox publishes the verification public key at a JWKS endpoint, so any SMART He ## Configuration -The operation signs cards with an issuer **EC P-256 private key**, set as a PEM (PKCS#8) in `module.health-cards-links.issuer-private-key` (env `BOX_MODULE_HEALTH_CARDS_LINKS_ISSUER_PRIVATE_KEY`). You configure only the private key; Aidbox derives the public key, its `kid` ([RFC 7638](https://www.rfc-editor.org/rfc/rfc7638) JWK thumbprint) and the JWKS from it. Until it is set the operation returns `422` and the JWKS endpoint `404`. +The operation signs cards with an issuer **EC P-256 private key**, set as a PEM in `module.health-cards-links.issuer-private-key` (env `BOX_MODULE_HEALTH_CARDS_LINKS_ISSUER_PRIVATE_KEY`); the public key and JWKS are derived from it. Until it is set the operation returns `422` and the JWKS endpoint `404`. Generate a key with OpenSSL: From ae3afe7c8d8d6a9d69a00e2e9cdc866d0c0a2be4 Mon Sep 17 00:00:00 2001 From: spicyfalafel <58147555+spicyfalafel@users.noreply.github.com> Date: Mon, 3 Aug 2026 11:11:22 +0200 Subject: [PATCH 07/22] [docs] SMART Health Cards: trim intro, add env export, drop repeatable-params section --- docs/api/rest-api/other/smart-health-cards.md | 24 ++++--------------- 1 file changed, 4 insertions(+), 20 deletions(-) diff --git a/docs/api/rest-api/other/smart-health-cards.md b/docs/api/rest-api/other/smart-health-cards.md index 4c968f489..9b4c428e1 100644 --- a/docs/api/rest-api/other/smart-health-cards.md +++ b/docs/api/rest-api/other/smart-health-cards.md @@ -6,7 +6,7 @@ description: >- # SMART Health Cards -Since the 2608 version, Aidbox can issue [SMART Health Cards](https://smarthealth.cards/): verifiable health credentials packaged as a signed, compact JSON Web Signature (JWS) that a patient presents as a QR code or file. The [`$health-cards-issue`](https://spec.smarthealth.cards/#via-fhir-health-cards-issue-operation) operation gathers the patient's clinical resources, minifies them into a `verifiableCredential` Bundle, and signs it with the issuer's ES256 key. +Since the 2608 version, Aidbox can issue [SMART Health Cards](https://smarthealth.cards/): verifiable health credentials packaged as a signed, compact JSON Web Signature (JWS) that a patient presents as a QR code or file. The [`$health-cards-issue`](https://spec.smarthealth.cards/#via-fhir-health-cards-issue-operation) operation issues a signed card from the patient's clinical resources. Aidbox publishes the verification public key at a JWKS endpoint, so any SMART Health Cards verifier can validate the cards. @@ -14,10 +14,12 @@ Aidbox publishes the verification public key at a JWKS endpoint, so any SMART He The operation signs cards with an issuer **EC P-256 private key**, set as a PEM in `module.health-cards-links.issuer-private-key` (env `BOX_MODULE_HEALTH_CARDS_LINKS_ISSUER_PRIVATE_KEY`); the public key and JWKS are derived from it. Until it is set the operation returns `422` and the JWKS endpoint `404`. -Generate a key with OpenSSL: +Generate a key with OpenSSL and set it as the issuer key: ```bash openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out issuer.pem + +export BOX_MODULE_HEALTH_CARDS_LINKS_ISSUER_PRIVATE_KEY="$(cat issuer.pem)" ``` {% hint style="warning" %} @@ -95,24 +97,6 @@ POST /fhir/Patient/pt-1/$health-cards-issue `verifiableCredential.valueString` is the signed SMART Health Card JWS: a raw-DEFLATE-compressed (`zip:"DEF"`) payload signed with ES256, whose protected header carries the issuer `kid`. Each `resourceLink` maps a bundled resource (`resource:N`) to its source Aidbox resource (`hostedResource`). -### Repeatable parameters - -`credentialType`, `credentialValueSet`, and `includeIdentityClaim` take one `parameter` entry per value. This request asks for a card carrying both immunizations and observations (`credentialType` as a logical AND), limits the identity claim to the name, and skips resources modified before a cutoff: - -```json -{ - "resourceType": "Parameters", - "parameter": [ - { "name": "credentialType", "valueUri": "Immunization" }, - { "name": "credentialType", "valueUri": "Observation" }, - { "name": "includeIdentityClaim", "valueString": "Patient.name" }, - { "name": "_since", "valueInstant": "2024-01-01T00:00:00Z" } - ] -} -``` - -Add a `credentialValueSet` entry the same way to filter the kept resources, for example `https://terminology.smarthealth.cards/ValueSet/immunization-covid-cvx`. - ## Input parameters ### credentialType From f990c6298f006f9aa9a1e6153ef1ed4058f1042d Mon Sep 17 00:00:00 2001 From: spicyfalafel <58147555+spicyfalafel@users.noreply.github.com> Date: Mon, 3 Aug 2026 11:12:00 +0200 Subject: [PATCH 08/22] [docs] SMART Health Cards: set the key via deployment env, not shell export --- docs/api/rest-api/other/smart-health-cards.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/api/rest-api/other/smart-health-cards.md b/docs/api/rest-api/other/smart-health-cards.md index 9b4c428e1..a4dda03f1 100644 --- a/docs/api/rest-api/other/smart-health-cards.md +++ b/docs/api/rest-api/other/smart-health-cards.md @@ -14,14 +14,14 @@ Aidbox publishes the verification public key at a JWKS endpoint, so any SMART He The operation signs cards with an issuer **EC P-256 private key**, set as a PEM in `module.health-cards-links.issuer-private-key` (env `BOX_MODULE_HEALTH_CARDS_LINKS_ISSUER_PRIVATE_KEY`); the public key and JWKS are derived from it. Until it is set the operation returns `422` and the JWKS endpoint `404`. -Generate a key with OpenSSL and set it as the issuer key: +Generate a key with OpenSSL: ```bash openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out issuer.pem - -export BOX_MODULE_HEALTH_CARDS_LINKS_ISSUER_PRIVATE_KEY="$(cat issuer.pem)" ``` +Put the PEM contents into `BOX_MODULE_HEALTH_CARDS_LINKS_ISSUER_PRIVATE_KEY` in your deployment (for example the `environment` section of your Docker Compose file). + {% hint style="warning" %} Keep the private key secret and back it up. Rotating it changes the `kid`, so cards signed with the previous key stop verifying against the published JWKS. {% endhint %} From 8a884f4ffdcba1a6076187baea8d3777e66e2cac Mon Sep 17 00:00:00 2001 From: spicyfalafel <58147555+spicyfalafel@users.noreply.github.com> Date: Mon, 3 Aug 2026 11:14:52 +0200 Subject: [PATCH 09/22] [docs] SMART Health Cards: clarify AccessPolicy under OrgBAC --- docs/api/rest-api/other/smart-health-cards.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/api/rest-api/other/smart-health-cards.md b/docs/api/rest-api/other/smart-health-cards.md index a4dda03f1..81d2e051d 100644 --- a/docs/api/rest-api/other/smart-health-cards.md +++ b/docs/api/rest-api/other/smart-health-cards.md @@ -212,6 +212,8 @@ POST /Organization//fhir/Patient//$health-cards-issue It issues a card only from data in that organization. The target `Patient` and every internal read run inside the organization's compartment, so a `Patient` that belongs to another organization is not visible and the request returns `403`. The `resourceLink.hostedResource` URLs are scoped to the same `/Organization//fhir` base. +The organization compartment is the access gate here, so the `FhirSearch` / `FhirRead` policies above are not needed on this path; they apply to the global `/fhir/...` route. Under an organization the internal reads run as the `orgbac-fhir-read` and `orgbac-fhir-search` operations, so any operation-linked policy targets those instead. + ## Errors The operation returns every error as an `OperationOutcome`. From 72f3195ee6bc72ca4ad86abb18e131c73b56dd3b Mon Sep 17 00:00:00 2001 From: spicyfalafel <58147555+spicyfalafel@users.noreply.github.com> Date: Mon, 3 Aug 2026 11:19:22 +0200 Subject: [PATCH 10/22] [docs] SMART Health Cards: correct OrgBAC access (policies still required) --- docs/api/rest-api/other/smart-health-cards.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api/rest-api/other/smart-health-cards.md b/docs/api/rest-api/other/smart-health-cards.md index 81d2e051d..0e5721e6a 100644 --- a/docs/api/rest-api/other/smart-health-cards.md +++ b/docs/api/rest-api/other/smart-health-cards.md @@ -212,7 +212,7 @@ POST /Organization//fhir/Patient//$health-cards-issue It issues a card only from data in that organization. The target `Patient` and every internal read run inside the organization's compartment, so a `Patient` that belongs to another organization is not visible and the request returns `403`. The `resourceLink.hostedResource` URLs are scoped to the same `/Organization//fhir` base. -The organization compartment is the access gate here, so the `FhirSearch` / `FhirRead` policies above are not needed on this path; they apply to the global `/fhir/...` route. Under an organization the internal reads run as the `orgbac-fhir-read` and `orgbac-fhir-search` operations, so any operation-linked policy targets those instead. +Access Policies still apply: the organization compartment restricts the data to the organization, but it does not by itself grant access, and Aidbox denies a request that no policy allows. The `FhirSearch` / `FhirRead` examples above are for the global `/fhir/...` route; under an organization the internal reads run as the `orgbac-fhir-read` and `orgbac-fhir-search` operations, so operation-linked policies target those instead. ## Errors From 4ff406378b615b92cc2fe91e1a9ee4b72712ee17 Mon Sep 17 00:00:00 2001 From: spicyfalafel <58147555+spicyfalafel@users.noreply.github.com> Date: Mon, 3 Aug 2026 11:43:16 +0200 Subject: [PATCH 11/22] [docs] SMART Health Cards: shorten OrgBAC section, add org AccessPolicy example --- docs/api/rest-api/other/smart-health-cards.md | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/docs/api/rest-api/other/smart-health-cards.md b/docs/api/rest-api/other/smart-health-cards.md index 0e5721e6a..8fa02b986 100644 --- a/docs/api/rest-api/other/smart-health-cards.md +++ b/docs/api/rest-api/other/smart-health-cards.md @@ -210,9 +210,22 @@ Under [organization-based access control](../../../access-control/authorization/ POST /Organization//fhir/Patient//$health-cards-issue ``` -It issues a card only from data in that organization. The target `Patient` and every internal read run inside the organization's compartment, so a `Patient` that belongs to another organization is not visible and the request returns `403`. The `resourceLink.hostedResource` URLs are scoped to the same `/Organization//fhir` base. +It issues a card only from the organization's data: the target `Patient` and every internal read run inside the organization's compartment, so a `Patient` from another organization returns `403`. `hostedResource` URLs use the same `/Organization//fhir` base. -Access Policies still apply: the organization compartment restricts the data to the organization, but it does not by itself grant access, and Aidbox denies a request that no policy allows. The `FhirSearch` / `FhirRead` examples above are for the global `/fhir/...` route; under an organization the internal reads run as the `orgbac-fhir-read` and `orgbac-fhir-search` operations, so operation-linked policies target those instead. +Access Policies still apply (the compartment restricts data but does not grant access). The internal reads run as the `orgbac-fhir-read` / `orgbac-fhir-search` operations, so link org policies to those rather than `FhirSearch` / `FhirRead`: + +```json +{ + "resourceType": "AccessPolicy", + "id": "health-cards-org-issuer-search", + "engine": "matcho", + "link": [{ "reference": "Operation/orgbac-fhir-search" }], + "matcho": { + "client": { "id": "my-client-id" }, + "params": { "resource/type": { "$enum": ["Immunization", "Observation"] } } + } +} +``` ## Errors From 5dd55405b846644c4cd99e51409fad067805d410 Mon Sep 17 00:00:00 2001 From: spicyfalafel <58147555+spicyfalafel@users.noreply.github.com> Date: Mon, 3 Aug 2026 12:01:45 +0200 Subject: [PATCH 12/22] [docs] SMART Health Cards: clearer access-control (what to configure), trims, OrgBAC response --- docs/api/rest-api/other/smart-health-cards.md | 40 ++++++++++++++++--- 1 file changed, 34 insertions(+), 6 deletions(-) diff --git a/docs/api/rest-api/other/smart-health-cards.md b/docs/api/rest-api/other/smart-health-cards.md index 8fa02b986..ea8b103a6 100644 --- a/docs/api/rest-api/other/smart-health-cards.md +++ b/docs/api/rest-api/other/smart-health-cards.md @@ -12,7 +12,7 @@ Aidbox publishes the verification public key at a JWKS endpoint, so any SMART He ## Configuration -The operation signs cards with an issuer **EC P-256 private key**, set as a PEM in `module.health-cards-links.issuer-private-key` (env `BOX_MODULE_HEALTH_CARDS_LINKS_ISSUER_PRIVATE_KEY`); the public key and JWKS are derived from it. Until it is set the operation returns `422` and the JWKS endpoint `404`. +The operation signs cards with an issuer **EC P-256 private key**, set as a PEM in `module.health-cards-links.issuer-private-key` (env `BOX_MODULE_HEALTH_CARDS_LINKS_ISSUER_PRIVATE_KEY`); the public key and JWKS are derived from it. Generate a key with OpenSSL: @@ -95,7 +95,7 @@ POST /fhir/Patient/pt-1/$health-cards-issue } ``` -`verifiableCredential.valueString` is the signed SMART Health Card JWS: a raw-DEFLATE-compressed (`zip:"DEF"`) payload signed with ES256, whose protected header carries the issuer `kid`. Each `resourceLink` maps a bundled resource (`resource:N`) to its source Aidbox resource (`hostedResource`). +`verifiableCredential.valueString` is the signed SMART Health Card (a compact JWS). Each `resourceLink` maps a bundled resource (`resource:N`) to its source Aidbox resource (`hostedResource`). ## Input parameters @@ -124,7 +124,7 @@ Controls which `Patient` identity fields the card carries: | Value | Effect | | ----------------------------------------------------------- | ------------------------------------------------------------------------------- | | omitted or `true` | Default claims: `Patient.name` (family and given only) and `Patient.birthDate`. | -| a list of strings, e.g. `Patient.name`, `Patient.birthDate` | Only the named fields. | +| `Patient.name`, `Patient.gender` (one `valueString` each) | Only the named fields. | | `false` | Omit the `Patient` from the card entirely. | ### \_since @@ -155,18 +155,31 @@ GET /health-cards/.well-known/jwks.json } ``` -The `kid` matches the `kid` in the JWS protected header, so a verifier can select the right key. The card issuer identifier (the `iss` claim) is `/health-cards`, and per the SMART Health Cards spec its key set lives at `/.well-known/jwks.json`. The endpoint returns `404` while the issuer key is not configured. +A verifier picks the right key by the `kid` in the card's JWS header. The card's issuer (`iss`) is `/health-cards`, and its key set lives at `/.well-known/jwks.json`. Returns `404` until the issuer key is configured. ## Access control -`$health-cards-issue` reads the patient's data by re-dispatching internal FHIR requests through the regular request pipeline, so Aidbox enforces the caller's [Access Policies](../../../access-control/authorization/access-policies.md). It never issues a card for data the caller may not read, and propagates a denied internal read verbatim (for example `403`). +A caller must be allowed to **invoke the operation** and to **read the patient's data**. The operation re-dispatches its reads through the regular pipeline, so your [Access Policies](../../../access-control/authorization/access-policies.md) gate them; a read the caller may not perform stops the card (for example `403`). The `credentialValueSet` terminology check (`$validate-code`) runs in-process and needs no policy. For a single call, the operation issues these internal requests, each authorized against the caller's policies: - `GET /fhir/Patient/{id}?_elements=name,birthDate` reads the identity claims as `FhirRead` (skipped when `includeIdentityClaim` is `false`; `_elements` lists the requested claim fields). - `GET /fhir/{type}?{compartment-param}=Patient/{id}` searches each requested `credentialType` as `FhirSearch`, using the type's Patient-compartment search parameter (`patient` for `Immunization`, `subject` for `Observation` and `Condition`, and so on) and following `next` links to page through every match. Adds `&_lastUpdated=ge{_since}` when `_since` is set. -Grant the caller a policy for each. For the type searches, link an [AccessPolicy](../../../access-control/authorization/access-policies.md) to `Operation/FhirSearch` and restrict it inside `matcho`. For example, to let one client build cards from `Immunization` and `Observation`: +You can grant this at two levels. + +**Simplest** — one policy for the client, which covers the operation call and every read: + +```json +{ + "resourceType": "AccessPolicy", + "id": "health-cards-issuer", + "engine": "allow", + "link": [{ "reference": "Client/my-client-id" }] +} +``` + +**Granular** — a policy per operation. The type searches run as `FhirSearch`: ```json { @@ -200,6 +213,8 @@ Add each resource type you request to the `$enum`. For the `Patient` read, add a } ``` +Both examples allow any search or read on those types. To tighten them, add `"request-method": "get"` to `matcho`; for per-patient control (only the caller's own patients) use a `sql` or `complex` engine. + See [more AccessPolicy examples](../../../tutorials/security-access-control-tutorials/accesspolicy-examples.md). ### Organization scope (OrgBAC) @@ -227,6 +242,19 @@ Access Policies still apply (the compartment restricts data but does not grant a } ``` +The response is the same `Parameters`, with each `hostedResource` under the organization: + +```json +{ + "name": "resourceLink", + "part": [ + { "name": "vcIndex", "valueInteger": 0 }, + { "name": "bundledResource", "valueUri": "resource:1" }, + { "name": "hostedResource", "valueUri": "https:///Organization//fhir/Immunization/imm-1" } + ] +} +``` + ## Errors The operation returns every error as an `OperationOutcome`. From 97b227e38f0d271e2c1799d2380f259be6dd99c1 Mon Sep 17 00:00:00 2001 From: spicyfalafel <58147555+spicyfalafel@users.noreply.github.com> Date: Mon, 3 Aug 2026 12:03:32 +0200 Subject: [PATCH 13/22] [docs] SMART Health Cards: drop broad client policy, rewrite credentialValueSet --- docs/api/rest-api/other/smart-health-cards.md | 23 +++---------------- 1 file changed, 3 insertions(+), 20 deletions(-) diff --git a/docs/api/rest-api/other/smart-health-cards.md b/docs/api/rest-api/other/smart-health-cards.md index ea8b103a6..5e27cb5a1 100644 --- a/docs/api/rest-api/other/smart-health-cards.md +++ b/docs/api/rest-api/other/smart-health-cards.md @@ -109,13 +109,9 @@ Every issued card's `vc.type` array includes `https://smarthealth.cards#health-c ### credentialValueSet -Restricts the card's resources to members of the given ValueSet(s). Aidbox checks each resource's clinical code (`Immunization.vaccineCode`, `Observation.code`, `MedicationRequest.medicationCodeableConcept`, `*.type`) with [`$validate-code`](validate.md). +Keeps only resources whose clinical code is a member of the given ValueSet. For example, supply a ValueSet of COVID-19 vaccine codes to include only COVID-19 immunizations. Aidbox validates each resource's code (`Immunization.vaccineCode`, `Observation.code`, `MedicationRequest.medicationCodeableConcept`, `*.type`) against the ValueSet with [`$validate-code`](validate.md), so the ValueSet must be resolvable by the terminology (installed in a package). An unresolvable ValueSet fails the request with `400`. -Multiple `credentialValueSet` parameters apply as a **card-level logical AND**: Aidbox keeps a resource when it belongs to at least one supplied ValueSet, and issues the card only when the kept resources cover **every** supplied ValueSet. If some ValueSet has no matching resource, Aidbox cannot satisfy the request and returns `404`. Use this to request, for example, a card that holds both a COVID-19 vaccine **and** an mpox vaccine. - -{% hint style="info" %} -Aidbox's terminology must resolve the ValueSet. A non-member code is filtered out; a ValueSet that cannot be resolved fails the request with `400` rather than a misleading empty result. -{% endhint %} +Passing several `credentialValueSet` parameters combines them as a card-level logical AND: a resource is kept if it matches at least one, and the card is issued only when every ValueSet matched some resource (otherwise `404`). So `[covid-vaccines, mpox-vaccines]` requests a card that carries both a COVID-19 and an mpox vaccine. ### includeIdentityClaim @@ -166,20 +162,7 @@ For a single call, the operation issues these internal requests, each authorized - `GET /fhir/Patient/{id}?_elements=name,birthDate` reads the identity claims as `FhirRead` (skipped when `includeIdentityClaim` is `false`; `_elements` lists the requested claim fields). - `GET /fhir/{type}?{compartment-param}=Patient/{id}` searches each requested `credentialType` as `FhirSearch`, using the type's Patient-compartment search parameter (`patient` for `Immunization`, `subject` for `Observation` and `Condition`, and so on) and following `next` links to page through every match. Adds `&_lastUpdated=ge{_since}` when `_since` is set. -You can grant this at two levels. - -**Simplest** — one policy for the client, which covers the operation call and every read: - -```json -{ - "resourceType": "AccessPolicy", - "id": "health-cards-issuer", - "engine": "allow", - "link": [{ "reference": "Client/my-client-id" }] -} -``` - -**Granular** — a policy per operation. The type searches run as `FhirSearch`: +The type searches run as `FhirSearch`. Link an [AccessPolicy](../../../access-control/authorization/access-policies.md) to `Operation/FhirSearch` and restrict it to the resource types inside `matcho`: ```json { From 261c2b1a0349dddc8ad431da47da9a34289021c9 Mon Sep 17 00:00:00 2001 From: spicyfalafel <58147555+spicyfalafel@users.noreply.github.com> Date: Mon, 3 Aug 2026 12:04:54 +0200 Subject: [PATCH 14/22] [docs] SMART Health Cards: add a credentialValueSet example request --- docs/api/rest-api/other/smart-health-cards.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/docs/api/rest-api/other/smart-health-cards.md b/docs/api/rest-api/other/smart-health-cards.md index 5e27cb5a1..b29e83d3a 100644 --- a/docs/api/rest-api/other/smart-health-cards.md +++ b/docs/api/rest-api/other/smart-health-cards.md @@ -111,6 +111,21 @@ Every issued card's `vc.type` array includes `https://smarthealth.cards#health-c Keeps only resources whose clinical code is a member of the given ValueSet. For example, supply a ValueSet of COVID-19 vaccine codes to include only COVID-19 immunizations. Aidbox validates each resource's code (`Immunization.vaccineCode`, `Observation.code`, `MedicationRequest.medicationCodeableConcept`, `*.type`) against the ValueSet with [`$validate-code`](validate.md), so the ValueSet must be resolvable by the terminology (installed in a package). An unresolvable ValueSet fails the request with `400`. +For example, a COVID-19-only immunization card: + +```json +{ + "resourceType": "Parameters", + "parameter": [ + { "name": "credentialType", "valueUri": "Immunization" }, + { + "name": "credentialValueSet", + "valueUri": "https://terminology.smarthealth.cards/ValueSet/immunization-covid-cvx" + } + ] +} +``` + Passing several `credentialValueSet` parameters combines them as a card-level logical AND: a resource is kept if it matches at least one, and the card is issued only when every ValueSet matched some resource (otherwise `404`). So `[covid-vaccines, mpox-vaccines]` requests a card that carries both a COVID-19 and an mpox vaccine. ### includeIdentityClaim From 3784627c6804e172f599125d0d67afa176b9c023 Mon Sep 17 00:00:00 2001 From: spicyfalafel <58147555+spicyfalafel@users.noreply.github.com> Date: Mon, 3 Aug 2026 12:05:55 +0200 Subject: [PATCH 15/22] [docs] SMART Health Cards: drop the tightening note and examples link --- docs/api/rest-api/other/smart-health-cards.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/docs/api/rest-api/other/smart-health-cards.md b/docs/api/rest-api/other/smart-health-cards.md index b29e83d3a..674ac3fc0 100644 --- a/docs/api/rest-api/other/smart-health-cards.md +++ b/docs/api/rest-api/other/smart-health-cards.md @@ -211,10 +211,6 @@ Add each resource type you request to the `$enum`. For the `Patient` read, add a } ``` -Both examples allow any search or read on those types. To tighten them, add `"request-method": "get"` to `matcho`; for per-patient control (only the caller's own patients) use a `sql` or `complex` engine. - -See [more AccessPolicy examples](../../../tutorials/security-access-control-tutorials/accesspolicy-examples.md). - ### Organization scope (OrgBAC) Under [organization-based access control](../../../access-control/authorization/scoped-api/organization-based-hierarchical-access-control.md), the operation is also served at: From c7dcc7e9e4a57464a53e8f119b8ba13489b74695 Mon Sep 17 00:00:00 2001 From: spicyfalafel <58147555+spicyfalafel@users.noreply.github.com> Date: Mon, 3 Aug 2026 12:07:34 +0200 Subject: [PATCH 16/22] [docs] SMART Health Cards: list common credentialValueSets + spec link --- docs/api/rest-api/other/smart-health-cards.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/api/rest-api/other/smart-health-cards.md b/docs/api/rest-api/other/smart-health-cards.md index 674ac3fc0..c93623a6c 100644 --- a/docs/api/rest-api/other/smart-health-cards.md +++ b/docs/api/rest-api/other/smart-health-cards.md @@ -128,6 +128,17 @@ For example, a COVID-19-only immunization card: Passing several `credentialValueSet` parameters combines them as a card-level logical AND: a resource is kept if it matches at least one, and the card is issued only when every ValueSet matched some resource (otherwise `404`). So `[covid-vaccines, mpox-vaccines]` requests a card that carries both a COVID-19 and an mpox vaccine. +Any ValueSet that Aidbox's terminology can resolve works. The SMART Health Cards project publishes a [standard set of Health Card value sets](https://terminology.smarthealth.cards/artifacts.html); install the `terminology.smarthealth.cards` package to use them. The common ones, under `https://terminology.smarthealth.cards/ValueSet/`: + +| ValueSet | Contents | +| --- | --- | +| `immunization-covid-all` | COVID-19 vaccines (CVX, SNOMED CT, ICD-11) | +| `immunization-covid-cvx` | COVID-19 vaccines (CVX) | +| `immunization-orthopoxvirus-all` | mpox / orthopoxvirus vaccines | +| `immunization-all-cvx` | All CVX vaccine codes | +| `lab-qualitative-test-covid` | Qualitative COVID-19 lab tests (LOINC) | +| `lab-qualitative-result` | Qualitative infectious-disease lab results | + ### includeIdentityClaim Controls which `Patient` identity fields the card carries: From 1ac0c55ee27d230671bbbbad6817436f62dc656d Mon Sep 17 00:00:00 2001 From: spicyfalafel <58147555+spicyfalafel@users.noreply.github.com> Date: Mon, 3 Aug 2026 12:15:28 +0200 Subject: [PATCH 17/22] [docs] SMART Health Cards: env-var block in Configuration, wording fixes --- docs/api/rest-api/other/smart-health-cards.md | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/docs/api/rest-api/other/smart-health-cards.md b/docs/api/rest-api/other/smart-health-cards.md index c93623a6c..03eb21f2d 100644 --- a/docs/api/rest-api/other/smart-health-cards.md +++ b/docs/api/rest-api/other/smart-health-cards.md @@ -12,15 +12,17 @@ Aidbox publishes the verification public key at a JWKS endpoint, so any SMART He ## Configuration -The operation signs cards with an issuer **EC P-256 private key**, set as a PEM in `module.health-cards-links.issuer-private-key` (env `BOX_MODULE_HEALTH_CARDS_LINKS_ISSUER_PRIVATE_KEY`); the public key and JWKS are derived from it. +The operation signs cards with an issuer **EC P-256 private key**, set as a PEM in `module.health-cards-links.issuer-private-key`; the public key and JWKS are derived from it. -Generate a key with OpenSSL: +Generate a key with OpenSSL and set it as the issuer key: ```bash openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out issuer.pem ``` -Put the PEM contents into `BOX_MODULE_HEALTH_CARDS_LINKS_ISSUER_PRIVATE_KEY` in your deployment (for example the `environment` section of your Docker Compose file). +``` +BOX_MODULE_HEALTH_CARDS_LINKS_ISSUER_PRIVATE_KEY= +``` {% hint style="warning" %} Keep the private key secret and back it up. Rotating it changes the `kid`, so cards signed with the previous key stop verifying against the published JWKS. @@ -103,9 +105,9 @@ POST /fhir/Patient/pt-1/$health-cards-issue The operation accepts any FHIR resource type in the Patient compartment, such as `Immunization`, `Observation`, or `Condition`. It searches the patient's compartment for that type and adds the matching resources to the card. -Per the [spec](https://spec.smarthealth.cards/), `credentialType` is required (at least one). Aidbox combines multiple values with logical AND: the card carries every requested type the patient has. Aidbox rejects a missing or unsupported `credentialType` with `400`. +`credentialType` is required (at least one). Aidbox combines multiple values with logical AND: the card carries every requested type the patient has. Aidbox rejects a missing or unsupported `credentialType` with `400`. -Every issued card's `vc.type` array includes `https://smarthealth.cards#health-card`. The deprecated type URIs `#covid19`, `#immunization`, and `#laboratory` still work; see [Legacy credential types](smart-health-cards.md#legacy-credential-types). +Every issued card's `vc.type` array includes `https://smarthealth.cards#health-card`. The deprecated type URIs `#covid19`, `#immunization`, and `#laboratory` also work. ### credentialValueSet @@ -126,7 +128,7 @@ For example, a COVID-19-only immunization card: } ``` -Passing several `credentialValueSet` parameters combines them as a card-level logical AND: a resource is kept if it matches at least one, and the card is issued only when every ValueSet matched some resource (otherwise `404`). So `[covid-vaccines, mpox-vaccines]` requests a card that carries both a COVID-19 and an mpox vaccine. +Passing several `credentialValueSet` parameters combines them as a card-level logical AND: a resource is kept if it matches at least one, and the card is issued only when every ValueSet matched some resource (otherwise `404`). So passing a COVID-19 vaccine ValueSet and an mpox vaccine ValueSet requests a card that carries a vaccine from each. Any ValueSet that Aidbox's terminology can resolve works. The SMART Health Cards project publishes a [standard set of Health Card value sets](https://terminology.smarthealth.cards/artifacts.html); install the `terminology.smarthealth.cards` package to use them. The common ones, under `https://terminology.smarthealth.cards/ValueSet/`: From 8727a7e581971ad374d4a49b1e827967cfa25b7b Mon Sep 17 00:00:00 2001 From: spicyfalafel <58147555+spicyfalafel@users.noreply.github.com> Date: Mon, 3 Aug 2026 13:31:52 +0200 Subject: [PATCH 18/22] Fix SMART Health Cards access-control policies and credentialValueSet guidance - Add the missing operation-invoke AccessPolicy (Operation/fhir-patient-health-cards-issue) - Scope FhirSearch policy by compartment param (patient/subject) via $one-of - Add OrgBAC operation-invoke and read policies; keep kebab-case orgbac op ids - Drop the non-working package-install instruction for credentialValueSet - Clarify the validated coded fields Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/api/rest-api/other/smart-health-cards.md | 68 +++++++++++++++---- 1 file changed, 54 insertions(+), 14 deletions(-) diff --git a/docs/api/rest-api/other/smart-health-cards.md b/docs/api/rest-api/other/smart-health-cards.md index 03eb21f2d..ee2a26c76 100644 --- a/docs/api/rest-api/other/smart-health-cards.md +++ b/docs/api/rest-api/other/smart-health-cards.md @@ -107,11 +107,9 @@ The operation accepts any FHIR resource type in the Patient compartment, such as `credentialType` is required (at least one). Aidbox combines multiple values with logical AND: the card carries every requested type the patient has. Aidbox rejects a missing or unsupported `credentialType` with `400`. -Every issued card's `vc.type` array includes `https://smarthealth.cards#health-card`. The deprecated type URIs `#covid19`, `#immunization`, and `#laboratory` also work. - ### credentialValueSet -Keeps only resources whose clinical code is a member of the given ValueSet. For example, supply a ValueSet of COVID-19 vaccine codes to include only COVID-19 immunizations. Aidbox validates each resource's code (`Immunization.vaccineCode`, `Observation.code`, `MedicationRequest.medicationCodeableConcept`, `*.type`) against the ValueSet with [`$validate-code`](validate.md), so the ValueSet must be resolvable by the terminology (installed in a package). An unresolvable ValueSet fails the request with `400`. +Keeps only resources whose clinical code is a member of the given ValueSet. For example, supply a ValueSet of COVID-19 vaccine codes to include only COVID-19 immunizations. Aidbox validates the resource's coded fields (`vaccineCode`, `code`, `medicationCodeableConcept`, and `type`) against the ValueSet with [`$validate-code`](validate.md), so the ValueSet must be loaded into Aidbox's terminology. An unresolvable ValueSet fails the request with `400`. For example, a COVID-19-only immunization card: @@ -130,7 +128,7 @@ For example, a COVID-19-only immunization card: Passing several `credentialValueSet` parameters combines them as a card-level logical AND: a resource is kept if it matches at least one, and the card is issued only when every ValueSet matched some resource (otherwise `404`). So passing a COVID-19 vaccine ValueSet and an mpox vaccine ValueSet requests a card that carries a vaccine from each. -Any ValueSet that Aidbox's terminology can resolve works. The SMART Health Cards project publishes a [standard set of Health Card value sets](https://terminology.smarthealth.cards/artifacts.html); install the `terminology.smarthealth.cards` package to use them. The common ones, under `https://terminology.smarthealth.cards/ValueSet/`: +Any ValueSet that Aidbox's terminology can resolve works. The standard SMART Health Cards value sets reference external code systems (CVX, SNOMED CT, LOINC), so validating their codes needs those code systems available to Aidbox's terminology, not just the value set definitions. For reference, the SMART Health Cards project publishes a [standard set of Health Card value sets](https://terminology.smarthealth.cards/artifacts.html); the common ones, under `https://terminology.smarthealth.cards/ValueSet/`: | ValueSet | Contents | | --- | --- | @@ -183,14 +181,27 @@ A verifier picks the right key by the `kid` in the card's JWS header. The card's ## Access control -A caller must be allowed to **invoke the operation** and to **read the patient's data**. The operation re-dispatches its reads through the regular pipeline, so your [Access Policies](../../../access-control/authorization/access-policies.md) gate them; a read the caller may not perform stops the card (for example `403`). The `credentialValueSet` terminology check (`$validate-code`) runs in-process and needs no policy. +A caller needs both permission to **invoke the operation** and permission for the **internal reads** it issues. The operation re-dispatches those reads through the regular pipeline, so your [Access Policies](../../../access-control/authorization/access-policies.md) gate them; a read the caller may not perform stops the card (for example `403`). The `credentialValueSet` terminology check (`$validate-code`) runs in-process and needs no policy. -For a single call, the operation issues these internal requests, each authorized against the caller's policies: +For a single call the operation issues these authorized requests: +- `POST /fhir/Patient/{id}/$health-cards-issue`, the operation itself (`fhir-patient-health-cards-issue`). - `GET /fhir/Patient/{id}?_elements=name,birthDate` reads the identity claims as `FhirRead` (skipped when `includeIdentityClaim` is `false`; `_elements` lists the requested claim fields). -- `GET /fhir/{type}?{compartment-param}=Patient/{id}` searches each requested `credentialType` as `FhirSearch`, using the type's Patient-compartment search parameter (`patient` for `Immunization`, `subject` for `Observation` and `Condition`, and so on) and following `next` links to page through every match. Adds `&_lastUpdated=ge{_since}` when `_since` is set. +- `GET /fhir/{type}?{compartment-param}=Patient/{id}` searches each requested `credentialType` as `FhirSearch`, using the type's Patient-compartment parameter (`patient` for `Immunization`, `subject` for `Observation` and `Condition`, and so on) and following `next` links to page through every match. Adds `&_lastUpdated=ge{_since}` when `_since` is set. + +Each policy links to an operation. First, allow invoking the operation itself: + +```json +{ + "resourceType": "AccessPolicy", + "id": "health-cards-issuer-op", + "engine": "matcho", + "link": [{ "reference": "Operation/fhir-patient-health-cards-issue" }], + "matcho": { "client": { "id": "my-client-id" } } +} +``` -The type searches run as `FhirSearch`. Link an [AccessPolicy](../../../access-control/authorization/access-policies.md) to `Operation/FhirSearch` and restrict it to the resource types inside `matcho`: +Next, allow the type searches. Link to `FhirSearch` and, with `$one-of`, match the compartment parameter each requested type uses so the policy only permits patient-scoped searches: ```json { @@ -201,15 +212,16 @@ The type searches run as `FhirSearch`. Link an [AccessPolicy](../../../access-co "matcho": { "client": { "id": "my-client-id" }, "params": { - "resource/type": { - "$enum": ["Immunization", "Observation"] - } + "$one-of": [ + { "resource/type": "Immunization", "patient": "present?" }, + { "resource/type": { "$enum": ["Observation", "Condition"] }, "subject": "present?" } + ] } } } ``` -Add each resource type you request to the `$enum`. For the `Patient` read, add a policy linked to `Operation/FhirRead`: +List the resource types you request under the compartment parameter each one uses (`patient` for `Immunization`, `subject` for `Observation` and `Condition`). Finally, allow the `Patient` read: ```json { @@ -234,7 +246,17 @@ POST /Organization//fhir/Patient//$health-cards-issue It issues a card only from the organization's data: the target `Patient` and every internal read run inside the organization's compartment, so a `Patient` from another organization returns `403`. `hostedResource` URLs use the same `/Organization//fhir` base. -Access Policies still apply (the compartment restricts data but does not grant access). The internal reads run as the `orgbac-fhir-read` / `orgbac-fhir-search` operations, so link org policies to those rather than `FhirSearch` / `FhirRead`: +Access Policies still apply (the compartment restricts data but does not grant access). Under OrgBAC the operation and its reads run as the org-scoped operations `orgbac-fhir-health-cards-issue`, `orgbac-fhir-read`, and `orgbac-fhir-search` (kebab-case route ids, not the camel-case `FhirRead` / `FhirSearch`), so link the org policies to those. The three policies mirror the ones above with the ids swapped: + +```json +{ + "resourceType": "AccessPolicy", + "id": "health-cards-org-issuer-op", + "engine": "matcho", + "link": [{ "reference": "Operation/orgbac-fhir-health-cards-issue" }], + "matcho": { "client": { "id": "my-client-id" } } +} +``` ```json { @@ -244,7 +266,25 @@ Access Policies still apply (the compartment restricts data but does not grant a "link": [{ "reference": "Operation/orgbac-fhir-search" }], "matcho": { "client": { "id": "my-client-id" }, - "params": { "resource/type": { "$enum": ["Immunization", "Observation"] } } + "params": { + "$one-of": [ + { "resource/type": "Immunization", "patient": "present?" }, + { "resource/type": { "$enum": ["Observation", "Condition"] }, "subject": "present?" } + ] + } + } +} +``` + +```json +{ + "resourceType": "AccessPolicy", + "id": "health-cards-org-issuer-read", + "engine": "matcho", + "link": [{ "reference": "Operation/orgbac-fhir-read" }], + "matcho": { + "client": { "id": "my-client-id" }, + "params": { "resource/type": "Patient" } } } ``` From c2ea27095391ab18333d61e147f2f005930962a5 Mon Sep 17 00:00:00 2001 From: spicyfalafel <58147555+spicyfalafel@users.noreply.github.com> Date: Mon, 3 Aug 2026 13:44:36 +0200 Subject: [PATCH 19/22] Document where to get SMART Health Cards value sets and how to resolve them Verified: loading a value set alone is not enough under the local engine ($validate-code returns false, "code system not found"); the hybrid engine with an external terminology server that supplies CVX/SNOMED/LOINC resolves them. Point to the IG artifacts, per-ValueSet JSON, package.tgz, and GitHub source. Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/api/rest-api/other/smart-health-cards.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/api/rest-api/other/smart-health-cards.md b/docs/api/rest-api/other/smart-health-cards.md index ee2a26c76..ca9b932c3 100644 --- a/docs/api/rest-api/other/smart-health-cards.md +++ b/docs/api/rest-api/other/smart-health-cards.md @@ -128,7 +128,11 @@ For example, a COVID-19-only immunization card: Passing several `credentialValueSet` parameters combines them as a card-level logical AND: a resource is kept if it matches at least one, and the card is issued only when every ValueSet matched some resource (otherwise `404`). So passing a COVID-19 vaccine ValueSet and an mpox vaccine ValueSet requests a card that carries a vaccine from each. -Any ValueSet that Aidbox's terminology can resolve works. The standard SMART Health Cards value sets reference external code systems (CVX, SNOMED CT, LOINC), so validating their codes needs those code systems available to Aidbox's terminology, not just the value set definitions. For reference, the SMART Health Cards project publishes a [standard set of Health Card value sets](https://terminology.smarthealth.cards/artifacts.html); the common ones, under `https://terminology.smarthealth.cards/ValueSet/`: +Any ValueSet that Aidbox's terminology can resolve works. The standard SMART Health Cards value sets are published by the [SMART Health Cards terminology IG](https://terminology.smarthealth.cards/artifacts.html) (each downloadable as a `ValueSet` resource at `https://terminology.smarthealth.cards/ValueSet-.json`, or together in the IG's `package.tgz`; source on GitHub at [dvci/shc-terminology](https://github.com/dvci/shc-terminology)). They are not in the public FHIR package registries, so `BOX_BOOTSTRAP_FHIR_PACKAGES` cannot fetch them by name; load the value sets into Aidbox yourself, for example with `PUT /fhir/ValueSet/`. + +Loading a value set alone is not enough to validate a code: these value sets reference external code systems (CVX, SNOMED CT, LOINC), so Aidbox needs those code systems too. Run the [terminology engine](../../../terminology-module/aidbox-terminology-module/hybrid.md) in `hybrid` mode against a terminology server that provides them. Under the `local` engine with only the value set loaded, `$validate-code` returns `false` with a "code system not found" issue, so the card comes back empty (`404`). + +The common value sets, under `https://terminology.smarthealth.cards/ValueSet/`: | ValueSet | Contents | | --- | --- | From e7c632d4f2a0a2d098289470fa69e5bab2302f03 Mon Sep 17 00:00:00 2001 From: spicyfalafel <58147555+spicyfalafel@users.noreply.github.com> Date: Mon, 3 Aug 2026 14:02:02 +0200 Subject: [PATCH 20/22] Trim credentialValueSet source list to the IG plus the hybrid-engine requirement Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/api/rest-api/other/smart-health-cards.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/api/rest-api/other/smart-health-cards.md b/docs/api/rest-api/other/smart-health-cards.md index ca9b932c3..da432ccf9 100644 --- a/docs/api/rest-api/other/smart-health-cards.md +++ b/docs/api/rest-api/other/smart-health-cards.md @@ -128,9 +128,7 @@ For example, a COVID-19-only immunization card: Passing several `credentialValueSet` parameters combines them as a card-level logical AND: a resource is kept if it matches at least one, and the card is issued only when every ValueSet matched some resource (otherwise `404`). So passing a COVID-19 vaccine ValueSet and an mpox vaccine ValueSet requests a card that carries a vaccine from each. -Any ValueSet that Aidbox's terminology can resolve works. The standard SMART Health Cards value sets are published by the [SMART Health Cards terminology IG](https://terminology.smarthealth.cards/artifacts.html) (each downloadable as a `ValueSet` resource at `https://terminology.smarthealth.cards/ValueSet-.json`, or together in the IG's `package.tgz`; source on GitHub at [dvci/shc-terminology](https://github.com/dvci/shc-terminology)). They are not in the public FHIR package registries, so `BOX_BOOTSTRAP_FHIR_PACKAGES` cannot fetch them by name; load the value sets into Aidbox yourself, for example with `PUT /fhir/ValueSet/`. - -Loading a value set alone is not enough to validate a code: these value sets reference external code systems (CVX, SNOMED CT, LOINC), so Aidbox needs those code systems too. Run the [terminology engine](../../../terminology-module/aidbox-terminology-module/hybrid.md) in `hybrid` mode against a terminology server that provides them. Under the `local` engine with only the value set loaded, `$validate-code` returns `false` with a "code system not found" issue, so the card comes back empty (`404`). +Any ValueSet that Aidbox's terminology can resolve works. The standard SMART Health Cards value sets come from the [SMART Health Cards terminology IG](https://terminology.smarthealth.cards/artifacts.html); load the ones you need into Aidbox as `ValueSet` resources. Their codes come from external code systems (CVX, SNOMED CT, LOINC), so also run the [terminology engine](../../../terminology-module/aidbox-terminology-module/hybrid.md) in `hybrid` mode against a terminology server that provides those code systems, otherwise the codes will not validate. The common value sets, under `https://terminology.smarthealth.cards/ValueSet/`: From 67935db6f1af955e5cbe025f049492580fe76181 Mon Sep 17 00:00:00 2001 From: spicyfalafel <58147555+spicyfalafel@users.noreply.github.com> Date: Mon, 3 Aug 2026 14:07:31 +0200 Subject: [PATCH 21/22] Set SMART Health Cards introduced version back to 2607 Co-Authored-By: Claude Opus 4.8 (1M context) --- .../organization-based-hierarchical-access-control.md | 2 +- docs/api/rest-api/other/smart-health-cards.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/access-control/authorization/scoped-api/organization-based-hierarchical-access-control.md b/docs/access-control/authorization/scoped-api/organization-based-hierarchical-access-control.md index 10e317079..357d1f2a4 100644 --- a/docs/access-control/authorization/scoped-api/organization-based-hierarchical-access-control.md +++ b/docs/access-control/authorization/scoped-api/organization-based-hierarchical-access-control.md @@ -297,7 +297,7 @@ See also [$document endpoint](../../../api/rest-api/other/document.md) POST /Organization//fhir/Patient//$health-cards-issue ``` -Issues a SMART Health Card scoped to the organization: the target `Patient` and every internal read run inside the organization's compartment. Available starting from version 2608. +Issues a SMART Health Card scoped to the organization: the target `Patient` and every internal read run inside the organization's compartment. Available starting from version 2607. See also [SMART Health Cards](../../../api/rest-api/other/smart-health-cards.md) diff --git a/docs/api/rest-api/other/smart-health-cards.md b/docs/api/rest-api/other/smart-health-cards.md index da432ccf9..b6a7fcef4 100644 --- a/docs/api/rest-api/other/smart-health-cards.md +++ b/docs/api/rest-api/other/smart-health-cards.md @@ -6,7 +6,7 @@ description: >- # SMART Health Cards -Since the 2608 version, Aidbox can issue [SMART Health Cards](https://smarthealth.cards/): verifiable health credentials packaged as a signed, compact JSON Web Signature (JWS) that a patient presents as a QR code or file. The [`$health-cards-issue`](https://spec.smarthealth.cards/#via-fhir-health-cards-issue-operation) operation issues a signed card from the patient's clinical resources. +Since the 2607 version, Aidbox can issue [SMART Health Cards](https://smarthealth.cards/): verifiable health credentials packaged as a signed, compact JSON Web Signature (JWS) that a patient presents as a QR code or file. The [`$health-cards-issue`](https://spec.smarthealth.cards/#via-fhir-health-cards-issue-operation) operation issues a signed card from the patient's clinical resources. Aidbox publishes the verification public key at a JWKS endpoint, so any SMART Health Cards verifier can validate the cards. From 13a5a11d74bf0b482dad49b9a834aabf4d81ff2b Mon Sep 17 00:00:00 2001 From: spicyfalafel <58147555+spicyfalafel@users.noreply.github.com> Date: Mon, 3 Aug 2026 15:27:29 +0200 Subject: [PATCH 22/22] Remove the legacy credential types section (content-based model only) Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/api/rest-api/other/smart-health-cards.md | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/docs/api/rest-api/other/smart-health-cards.md b/docs/api/rest-api/other/smart-health-cards.md index b6a7fcef4..a3be30bf4 100644 --- a/docs/api/rest-api/other/smart-health-cards.md +++ b/docs/api/rest-api/other/smart-health-cards.md @@ -315,15 +315,3 @@ The operation returns every error as an `OperationOutcome`. | `400` | A requested `credentialValueSet` could not be resolved. | | `404` | No resources match the requested credential type or value sets. | | `403` | An Access Policy denies reading the patient's data. | - -## Legacy credential types - -Before v1.4.0, SMART Health Cards classified a card by a top-level type URI. The framework deprecated these in favor of classifying a card by its contents, but Aidbox accepts them as `credentialType` values: - -| Value | Meaning | -| ---------------------------------------- | ---------------------------------------------------------------------- | -| `https://smarthealth.cards#covid19` | COVID-19 `Immunization`s only, filtered to COVID-19 CVX vaccine codes. | -| `https://smarthealth.cards#immunization` | All `Immunization`s. | -| `https://smarthealth.cards#laboratory` | `Observation`s. | - -Aidbox also stamps these URIs into the card's `vc.type` from its contents: `#immunization` when the card carries immunizations, `#covid19` for COVID-19 immunizations, and `#laboratory` when it carries observations. A card built only from other resource types carries just `#health-card`.