mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-12 22:26:13 +00:00
test(export): prove the scope holds at the artefact, not just at the switch
Five layers, because the defect had five places to hide. The catalogue guard checks what the compiler cannot state: every leaf resolves a label a human can read, every group resolves one, the fenced tier stays last, and the shipped template intersects the fence at nothing. The selection guard pins the three minting functions against each other. A stored blob this build cannot read resolves to the empty selection, never to a default set — that is what makes the revoked legacy links revocable rather than silently re-scoped. A request naming an id the server does not know is refused by name rather than dropped, because dropping it would narrow a scope the caller did express. The egress guard freezes the set of aggregator callers with the source of each one's selection written down. The required brand-typed parameter already stops a new path compiling without an answer; this stops one being added without a human writing the answer down, which is the moment a reviewer asks the question. The behavioural sweep aggregates every leaf in and out against a fixture that carries a row for each, and asserts the payload differs. It is parametrised over the catalogue, so a leaf added later is covered the day it is added. Its positive control asserts the all-on payload is genuinely populated first, so a "no difference" result means the gate is inert and not that the account is empty. Beside it: an unmapped measurement type is excluded rather than passed through, no table behind an unchosen leaf is queried at all, and an empty selection produces an empty record. The artefact layer runs the real route and reads what comes out — the FHIR bundle, the PDF text and the zip's contents — with a positive control beside every exclusion. Mutation-checked: making the gate admit everything reddens the sweep for every leaf; passing an unmapped type through reddens the fail-closed case; dropping the mood query's condition reddens the zero-read case; returning the BMI unconditionally reddens the sweep naming BODY_MASS_INDEX; adding a fenced leaf to the template reddens the purity guard; removing the panel's seeding block reddens the first-run and repeat-run cases; dropping the insurance refusal reddens the share-link case; adding a caller outside the frozen list reddens the egress guard naming the file. The panel suite grew past the practice name it was written for and is renamed for what it now covers: the three states, the fenced tier having no group control, and the scope line naming its sensitive inclusions. Integration coverage moves to the leaf model, including the case this release exists for: an account with data in every domain and a caller that named nothing gets an empty record. The OpenAPI contract catches up with the routes — the required selection, the refused keys, the share summary's new fields, the moved `Patient/$everything`, the Coverage search and the five typed reads.
This commit is contained in:
1 parent
582db8a538
commit
f42934487d
70 files changed
+5646
-2459
No files matched your search
+282
-141
@@ -871,10 +871,12 @@ paths:
|
||||
tags:
|
||||
- Export
|
||||
summary: Generate a health-record export (PDF / FHIR / package)
|
||||
description: "v1.7.0 flagship export. Returns the doctor-handover artefact in the requested `format`: `pdf` →
|
||||
application/pdf, `fhir` → application/fhir+json (HL7 FHIR R4 document Bundle), `package` → application/zip (PDF
|
||||
+ FHIR + README). Auth via cookie or Bearer; shared `export:<userId>` rate bucket (10/h). Strict validation:
|
||||
unknown keys 422."
|
||||
description: "Returns the doctor-handover artefact in the requested `format`: `pdf` → application/pdf, `fhir` →
|
||||
application/fhir+json (HL7 FHIR R4 document Bundle), `package` → application/zip (PDF + FHIR + README).
|
||||
`selection` is REQUIRED and carries the leaf inclusion list — membership is inclusion, absence is exclusion, and
|
||||
there is no server-side default. An unknown leaf id is a 422 that names it; the retired grouped `sections` shape
|
||||
is a 422 too, because accepting it would mean choosing a scope the caller did not express. Auth via cookie or
|
||||
Bearer; shared `export:<userId>` rate bucket (10/h). Strict validation: unknown keys 422."
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
@@ -904,65 +906,23 @@ paths:
|
||||
minimum: 1
|
||||
maximum: 365
|
||||
additionalProperties: false
|
||||
sections:
|
||||
selection:
|
||||
type: object
|
||||
properties:
|
||||
vitals:
|
||||
type: object
|
||||
properties:
|
||||
weight:
|
||||
type: boolean
|
||||
bp:
|
||||
type: boolean
|
||||
pulse:
|
||||
type: boolean
|
||||
oxygenSaturation:
|
||||
type: boolean
|
||||
bodyFat:
|
||||
type: boolean
|
||||
bodyComposition:
|
||||
type: boolean
|
||||
cardioFitness:
|
||||
type: object
|
||||
properties:
|
||||
restingHeartRate:
|
||||
type: boolean
|
||||
hrv:
|
||||
type: boolean
|
||||
vo2max:
|
||||
type: boolean
|
||||
activity:
|
||||
type: object
|
||||
properties:
|
||||
steps:
|
||||
type: boolean
|
||||
distance:
|
||||
type: boolean
|
||||
sleep:
|
||||
type: boolean
|
||||
glucose:
|
||||
type: boolean
|
||||
medications:
|
||||
type: object
|
||||
properties:
|
||||
list:
|
||||
type: boolean
|
||||
compliance:
|
||||
type: boolean
|
||||
mood:
|
||||
type: boolean
|
||||
bmi:
|
||||
type: boolean
|
||||
cycle:
|
||||
type: boolean
|
||||
labs:
|
||||
type: boolean
|
||||
allergies:
|
||||
type: boolean
|
||||
familyHistory:
|
||||
type: boolean
|
||||
mentalHealthScreeners:
|
||||
type: boolean
|
||||
v:
|
||||
type: number
|
||||
const: 2
|
||||
leaves:
|
||||
maxItems: 90
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
minLength: 1
|
||||
maxLength: 64
|
||||
required:
|
||||
- v
|
||||
- leaves
|
||||
additionalProperties: false
|
||||
locale:
|
||||
type: string
|
||||
enum:
|
||||
@@ -977,12 +937,11 @@ paths:
|
||||
maxLength: 120
|
||||
includeCharts:
|
||||
type: boolean
|
||||
includeAiSummary:
|
||||
type: boolean
|
||||
germanAtc:
|
||||
type: boolean
|
||||
required:
|
||||
- format
|
||||
- selection
|
||||
additionalProperties: false
|
||||
responses:
|
||||
"200":
|
||||
@@ -1010,14 +969,13 @@ paths:
|
||||
- Export
|
||||
summary: Create a clinician share link (v1.11.0)
|
||||
description: "Owner-only. Mints an `hls_` token (192-bit), stores only its HMAC hash, and returns the raw token EXACTLY
|
||||
ONCE in the response. v1.18.7 also mints a passphrase second factor (returned once as `passphrase`, with
|
||||
`shareUrl` and a `qrUrl` carrying the passphrase in the URL fragment); a leaked URL without the passphrase
|
||||
cannot open the record. Every scope column (window, sections, FHIR resource types, API toggle) is frozen
|
||||
write-once. v1.28 accepts an optional `documentIds` array (bounded ≤50): each id is validated as the caller's
|
||||
own live document and frozen onto the link at create (a foreign/deleted id → 422); the summary DTO then reports
|
||||
`documentCount`. A documents-only share (empty report sections + non-empty `documentIds`) is valid. `expiresAt`
|
||||
is required and capped at 90 days. Auth via cookie or Bearer; rate-limited (`share-link:<userId>`, 20/h).
|
||||
Strict: unknown keys 422."
|
||||
ONCE in the response, together with a passphrase second factor and a `qrUrl` carrying that passphrase in the URL
|
||||
fragment; a leaked URL without the passphrase cannot open the record. The window and the `selection` are frozen
|
||||
write-once — there is no widen path. `selection` omitted means an empty scope, not a default one, and the
|
||||
`INSURANCE` leaf is refused with a 422 because the share view never decrypts the insurance number. An optional
|
||||
`documentIds` array (bounded ≤50) is validated as the caller's own live documents and frozen onto the link; a
|
||||
documents-only share (no report scope, non-empty `documentIds`) is valid. `expiresAt` is required and capped at
|
||||
90 days. Auth via cookie or Bearer; rate-limited (`share-link:<userId>`, 20/h). Strict: unknown keys 422."
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
@@ -1039,65 +997,23 @@ paths:
|
||||
format: date-time
|
||||
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
|
||||
- type: "null"
|
||||
sections:
|
||||
selection:
|
||||
type: object
|
||||
properties:
|
||||
vitals:
|
||||
type: object
|
||||
properties:
|
||||
weight:
|
||||
type: boolean
|
||||
bp:
|
||||
type: boolean
|
||||
pulse:
|
||||
type: boolean
|
||||
oxygenSaturation:
|
||||
type: boolean
|
||||
bodyFat:
|
||||
type: boolean
|
||||
bodyComposition:
|
||||
type: boolean
|
||||
cardioFitness:
|
||||
type: object
|
||||
properties:
|
||||
restingHeartRate:
|
||||
type: boolean
|
||||
hrv:
|
||||
type: boolean
|
||||
vo2max:
|
||||
type: boolean
|
||||
activity:
|
||||
type: object
|
||||
properties:
|
||||
steps:
|
||||
type: boolean
|
||||
distance:
|
||||
type: boolean
|
||||
sleep:
|
||||
type: boolean
|
||||
glucose:
|
||||
type: boolean
|
||||
medications:
|
||||
type: object
|
||||
properties:
|
||||
list:
|
||||
type: boolean
|
||||
compliance:
|
||||
type: boolean
|
||||
mood:
|
||||
type: boolean
|
||||
bmi:
|
||||
type: boolean
|
||||
cycle:
|
||||
type: boolean
|
||||
labs:
|
||||
type: boolean
|
||||
allergies:
|
||||
type: boolean
|
||||
familyHistory:
|
||||
type: boolean
|
||||
mentalHealthScreeners:
|
||||
type: boolean
|
||||
v:
|
||||
type: number
|
||||
const: 2
|
||||
leaves:
|
||||
maxItems: 90
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
minLength: 1
|
||||
maxLength: 64
|
||||
required:
|
||||
- v
|
||||
- leaves
|
||||
additionalProperties: false
|
||||
documentIds:
|
||||
maxItems: 50
|
||||
type: array
|
||||
@@ -1272,8 +1188,9 @@ paths:
|
||||
- FHIR
|
||||
summary: FHIR R4 CapabilityStatement (v1.11.0)
|
||||
description: "Read-only FHIR R4 capability statement for the REST face. Declares the served resource types (Patient,
|
||||
Observation, MedicationStatement, MedicationAdministration), the `$everything` operation, and the
|
||||
`application/fhir+json` format. Auth: `fhir:read` scope (cookie sessions also pass)."
|
||||
Coverage, Observation, MedicationStatement, MedicationAdministration), their `read` and `search-type`
|
||||
interactions, the `Patient/$everything` operation, and the `application/fhir+json` format. Every advertised
|
||||
interaction has a route. Auth: `fhir:read` scope (cookie sessions also pass)."
|
||||
responses:
|
||||
"200":
|
||||
description: CapabilityStatement (application/fhir+json).
|
||||
@@ -1393,14 +1310,204 @@ paths:
|
||||
"401": *a1
|
||||
"422": *a3
|
||||
"429": *a4
|
||||
/api/fhir/$everything:
|
||||
/api/fhir/Coverage:
|
||||
get:
|
||||
tags:
|
||||
- FHIR
|
||||
summary: FHIR R4 Coverage search
|
||||
description: "Read-only `searchset` Bundle of the caller's own Coverage resource, when an insurer is recorded and the
|
||||
insurance leaf is in the saved selection. Auth: `fhir:read` scope. Offset paging via `_count` (clamped ≤200) /
|
||||
`_offset`. `userId` is narrowed from auth."
|
||||
parameters:
|
||||
- in: query
|
||||
name: _count
|
||||
schema:
|
||||
type: number
|
||||
- in: query
|
||||
name: _offset
|
||||
schema:
|
||||
type: number
|
||||
responses:
|
||||
"200":
|
||||
description: searchset Bundle (application/fhir+json).
|
||||
content:
|
||||
application/fhir+json:
|
||||
schema:
|
||||
type: string
|
||||
format: binary
|
||||
"401": *a1
|
||||
"422": *a3
|
||||
"429": *a4
|
||||
/api/fhir/Patient/{id}:
|
||||
get:
|
||||
tags:
|
||||
- FHIR
|
||||
summary: FHIR R4 Patient read
|
||||
description: "Read-only `Patient` by id, out of the caller's own record. An id belonging to someone else is
|
||||
indistinguishable from one that never existed — both answer a `not-found` OperationOutcome. Auth: `fhir:read`
|
||||
scope; `userId` is narrowed from auth."
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
responses:
|
||||
"200":
|
||||
description: The resource (application/fhir+json).
|
||||
content:
|
||||
application/fhir+json:
|
||||
schema:
|
||||
type: string
|
||||
format: binary
|
||||
"401": *a1
|
||||
"404":
|
||||
description: OperationOutcome (application/fhir+json).
|
||||
content:
|
||||
application/fhir+json:
|
||||
schema:
|
||||
type: string
|
||||
format: binary
|
||||
"422": *a3
|
||||
"429": *a4
|
||||
/api/fhir/Coverage/{id}:
|
||||
get:
|
||||
tags:
|
||||
- FHIR
|
||||
summary: FHIR R4 Coverage read
|
||||
description: "Read-only `Coverage` by id, out of the caller's own record. An id belonging to someone else is
|
||||
indistinguishable from one that never existed — both answer a `not-found` OperationOutcome. Auth: `fhir:read`
|
||||
scope; `userId` is narrowed from auth."
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
responses:
|
||||
"200":
|
||||
description: The resource (application/fhir+json).
|
||||
content:
|
||||
application/fhir+json:
|
||||
schema:
|
||||
type: string
|
||||
format: binary
|
||||
"401": *a1
|
||||
"404":
|
||||
description: OperationOutcome (application/fhir+json).
|
||||
content:
|
||||
application/fhir+json:
|
||||
schema:
|
||||
type: string
|
||||
format: binary
|
||||
"422": *a3
|
||||
"429": *a4
|
||||
/api/fhir/Observation/{id}:
|
||||
get:
|
||||
tags:
|
||||
- FHIR
|
||||
summary: FHIR R4 Observation read
|
||||
description: "Read-only `Observation` by id, out of the caller's own record. An id belonging to someone else is
|
||||
indistinguishable from one that never existed — both answer a `not-found` OperationOutcome. Auth: `fhir:read`
|
||||
scope; `userId` is narrowed from auth."
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
responses:
|
||||
"200":
|
||||
description: The resource (application/fhir+json).
|
||||
content:
|
||||
application/fhir+json:
|
||||
schema:
|
||||
type: string
|
||||
format: binary
|
||||
"401": *a1
|
||||
"404":
|
||||
description: OperationOutcome (application/fhir+json).
|
||||
content:
|
||||
application/fhir+json:
|
||||
schema:
|
||||
type: string
|
||||
format: binary
|
||||
"422": *a3
|
||||
"429": *a4
|
||||
/api/fhir/MedicationStatement/{id}:
|
||||
get:
|
||||
tags:
|
||||
- FHIR
|
||||
summary: FHIR R4 MedicationStatement read
|
||||
description: "Read-only `MedicationStatement` by id, out of the caller's own record. An id belonging to someone else is
|
||||
indistinguishable from one that never existed — both answer a `not-found` OperationOutcome. Auth: `fhir:read`
|
||||
scope; `userId` is narrowed from auth."
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
responses:
|
||||
"200":
|
||||
description: The resource (application/fhir+json).
|
||||
content:
|
||||
application/fhir+json:
|
||||
schema:
|
||||
type: string
|
||||
format: binary
|
||||
"401": *a1
|
||||
"404":
|
||||
description: OperationOutcome (application/fhir+json).
|
||||
content:
|
||||
application/fhir+json:
|
||||
schema:
|
||||
type: string
|
||||
format: binary
|
||||
"422": *a3
|
||||
"429": *a4
|
||||
/api/fhir/MedicationAdministration/{id}:
|
||||
get:
|
||||
tags:
|
||||
- FHIR
|
||||
summary: FHIR R4 MedicationAdministration read
|
||||
description: "Read-only `MedicationAdministration` by id, out of the caller's own record. An id belonging to someone
|
||||
else is indistinguishable from one that never existed — both answer a `not-found` OperationOutcome. Auth:
|
||||
`fhir:read` scope; `userId` is narrowed from auth."
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
responses:
|
||||
"200":
|
||||
description: The resource (application/fhir+json).
|
||||
content:
|
||||
application/fhir+json:
|
||||
schema:
|
||||
type: string
|
||||
format: binary
|
||||
"401": *a1
|
||||
"404":
|
||||
description: OperationOutcome (application/fhir+json).
|
||||
content:
|
||||
application/fhir+json:
|
||||
schema:
|
||||
type: string
|
||||
format: binary
|
||||
"422": *a3
|
||||
"429": *a4
|
||||
/api/fhir/Patient/$everything:
|
||||
get:
|
||||
tags:
|
||||
- FHIR
|
||||
summary: FHIR R4 $everything (v1.11.0)
|
||||
description: "Read-only `$everything` operation: every resource in the caller's own record (Patient, Coverage,
|
||||
Observations, MedicationStatements, MedicationAdministrations) in one `searchset` Bundle. Auth: `fhir:read`
|
||||
scope. Offset paging via `_count` (≤200) / `_offset`."
|
||||
description: "Read-only `Patient/$everything`: every resource the document bundle carries — Composition, Device,
|
||||
Patient, Coverage, Observations (including the cycle ones), MedicationStatements, MedicationAdministrations,
|
||||
Conditions, Encounters, AllergyIntolerances, FamilyMemberHistories and the DiagnosticReport — flattened into one
|
||||
`searchset` Bundle. Scoped to the owner's SAVED report selection; an account that never saved one gets nothing.
|
||||
Auth: `fhir:read` scope. Offset paging via `_count` (≤200) / `_offset`."
|
||||
parameters:
|
||||
- in: query
|
||||
name: _count
|
||||
@@ -15670,23 +15777,35 @@ components:
|
||||
minimum: -9007199254740991
|
||||
maximum: 9007199254740991
|
||||
description: Maximum lifetime of a share link, in days. No never-expiring share.
|
||||
resourceTypes:
|
||||
reportDownload:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: FHIR resource types a share link may be scoped to serve.
|
||||
sections:
|
||||
description: Machine formats a share link serves as a download under its token, behind the same unlock as the page.
|
||||
selectionVersion:
|
||||
type: integer
|
||||
minimum: -9007199254740991
|
||||
maximum: 9007199254740991
|
||||
description: Version of the leaf-selection grammar this build speaks.
|
||||
groups:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: Scopeable report sections a share link may toggle.
|
||||
description: Selection group ids, in presentation order.
|
||||
leaves:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: Every leaf id a report selection may carry. Membership is inclusion; absence is exclusion.
|
||||
required:
|
||||
- supported
|
||||
- maxDays
|
||||
- resourceTypes
|
||||
- sections
|
||||
- reportDownload
|
||||
- selectionVersion
|
||||
- groups
|
||||
- leaves
|
||||
additionalProperties: false
|
||||
description: Clinician share-link surface descriptor (v1.11).
|
||||
description: Clinician share-link surface descriptor.
|
||||
required:
|
||||
- apiContractVersion
|
||||
- derivedMetricIds
|
||||
@@ -15719,6 +15838,14 @@ components:
|
||||
type: boolean
|
||||
documentCount:
|
||||
type: number
|
||||
needsReselection:
|
||||
type: boolean
|
||||
allowFhirApi:
|
||||
type: boolean
|
||||
resourceTypes:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
expiresAt:
|
||||
type: string
|
||||
createdAt:
|
||||
@@ -15755,6 +15882,9 @@ components:
|
||||
- rangeEnd
|
||||
- protected
|
||||
- documentCount
|
||||
- needsReselection
|
||||
- allowFhirApi
|
||||
- resourceTypes
|
||||
- expiresAt
|
||||
- createdAt
|
||||
- revokedAt
|
||||
@@ -15803,6 +15933,14 @@ components:
|
||||
type: boolean
|
||||
documentCount:
|
||||
type: number
|
||||
needsReselection:
|
||||
type: boolean
|
||||
allowFhirApi:
|
||||
type: boolean
|
||||
resourceTypes:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
expiresAt:
|
||||
type: string
|
||||
createdAt:
|
||||
@@ -15826,6 +15964,9 @@ components:
|
||||
- rangeEnd
|
||||
- protected
|
||||
- documentCount
|
||||
- needsReselection
|
||||
- allowFhirApi
|
||||
- resourceTypes
|
||||
- expiresAt
|
||||
- createdAt
|
||||
- revokedAt
|
||||
|
||||
Reference in new issue
Block a user