vet-data-utils-ts 0.5.7 → 0.5.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -9
- package/THIRD_PARTY_NOTICES.md +6 -0
- package/dist/clinical-bundle-reader.d.ts +28 -0
- package/dist/clinical-bundle-reader.js +100 -0
- package/dist/immunization.d.ts +50 -0
- package/dist/immunization.js +48 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.js +4 -1
- package/dist/international-health-card.d.ts +34 -0
- package/dist/international-health-card.js +68 -0
- package/dist/shc.d.ts +73 -0
- package/dist/shc.js +268 -0
- package/package.json +19 -5
- package/dist/insurance-consent.d.ts +0 -126
- package/dist/insurance-consent.js +0 -228
package/README.md
CHANGED
|
@@ -1,5 +1,35 @@
|
|
|
1
1
|
# vet-data-utils-ts
|
|
2
2
|
|
|
3
|
+
## Veterinary immunization credentials
|
|
4
|
+
|
|
5
|
+
`buildVeterinaryImmunization(...)` creates a FHIR R4 `Immunization` with the
|
|
6
|
+
animal in `patient` and the veterinarian's real `PractitionerRole` in
|
|
7
|
+
`performer.actor`. The caller must supply a governed vaccine coding selected
|
|
8
|
+
for the animal species, jurisdiction and current terminology release; this
|
|
9
|
+
package does not guess a rabies or medicinal-product code.
|
|
10
|
+
|
|
11
|
+
`prepareSmartHealthCard(...)` minimizes a FHIR Bundle, serializes it
|
|
12
|
+
deterministically and returns the exact raw-DEFLATE ES256 signing input. The
|
|
13
|
+
tenant issuer signs that input with its dedicated P-256 credential key and
|
|
14
|
+
publishes the matching public JWK at `iss + /.well-known/jwks.json`.
|
|
15
|
+
`encodeSmartHealthCardQr(...)` then produces standard `shc:/` numeric QR data.
|
|
16
|
+
|
|
17
|
+
`encodePostQuantumCompanionProofUri(...)` carries an RFC 7797 detached ML-DSA
|
|
18
|
+
JWS as `pqc:/...`. This is a VetChain transition transport, not part of the
|
|
19
|
+
SMART Health Cards standard. `encodePostQuantumCompanionProofQr(...)` applies
|
|
20
|
+
the same two-decimal-digits-per-JWS-character encoding used by SHC, defaults to
|
|
21
|
+
two numbered large QR labels, and never repeats the external payload. Each
|
|
22
|
+
attester signs the exact uncompressed SHC payload bytes; verification therefore
|
|
23
|
+
requires both QR sets.
|
|
24
|
+
|
|
25
|
+
`buildSmartHealthCardPayloadReferences(...)` returns two deliberately distinct
|
|
26
|
+
SHA3-384 identifiers over those exact canonical bytes: a
|
|
27
|
+
`urn:multibase:z...` multihash for exact ledger lookup and a base58btc CIDv1
|
|
28
|
+
with the `raw` codec for content-addressed retrieval. The IHC print projection
|
|
29
|
+
shows FHIR `Immunization.occurrenceDateTime`, `vaccineCode` and
|
|
30
|
+
`protocolApplied.targetDisease`. Its issuer-supplied validity interval is not
|
|
31
|
+
FHIR `Immunization.expirationDate`, which means vaccine-batch expiry.
|
|
32
|
+
|
|
3
33
|
Development and releases follow the mandatory
|
|
4
34
|
[`local-first TDD and release contract`](docs/LOCAL_FIRST_RELEASE_CONTRACT.md).
|
|
5
35
|
|
|
@@ -34,15 +64,6 @@ future PATCH. The matching `normalize*FlatClaimsResource()` functions reject
|
|
|
34
64
|
nested FHIR persistence fields and unknown claims. Native FHIR JSON remains an
|
|
35
65
|
explicit import, projection or export boundary.
|
|
36
66
|
|
|
37
|
-
Animal-insurance eligibility Consent uses the same boundary through
|
|
38
|
-
`vet-data-utils-ts/insurance-consent`: FHIR-equivalent authorization semantics
|
|
39
|
-
live in canonical flat claims, the complete query-shaped ODRL Agreement lives
|
|
40
|
-
in an `application/odrl+json` attachment, semantic divergence fails closed,
|
|
41
|
-
and native R4/R5 appears only when explicitly requested. Aggregations such as
|
|
42
|
-
"latest date" and "count" remain ODRL-only because FHIR Consent has no native
|
|
43
|
-
field for them. See
|
|
44
|
-
[`docs/insurance-consent-odrl.md`](docs/insurance-consent-odrl.md).
|
|
45
|
-
|
|
46
67
|
Reusable Communication screens receive immutable workflow presets from
|
|
47
68
|
`vet-data-utils-ts/communication`. The research-agreement screen is fixed to
|
|
48
69
|
FHIR `notification` plus HL7 v3 ActReason `HRESCH` and does not expose a topic
|
|
@@ -64,6 +85,16 @@ for immunologicals); ordinary human ATC is not substituted for it. Veterinary
|
|
|
64
85
|
allergy product coding may also use ATCvet, while manifestations use an allowed
|
|
65
86
|
SNOMED source.
|
|
66
87
|
|
|
88
|
+
The shared IPS reader boundary is `projectClinicalBundleForReading(...)` from
|
|
89
|
+
`vet-data-utils-ts/clinical-bundle-reader`. It accepts a native FHIR Bundle
|
|
90
|
+
(including R4/R5 resources supported by the common normalizer) or resources
|
|
91
|
+
already carrying canonical `meta.claims`, creates a presentation-only copy,
|
|
92
|
+
and fills the governed primary `*-text`/`*-display` claims through an injected
|
|
93
|
+
exact terminology resolver. The resolver is called with `system`, `code` and
|
|
94
|
+
locale; the original verified Bundle is never mutated. Local UI labels render
|
|
95
|
+
as `text (English display)` outside English, English renders only `display`,
|
|
96
|
+
and missing labels fall back to the canonical `system|code` token.
|
|
97
|
+
|
|
67
98
|
The deferred spreadsheet shape for member and employee role assignments is
|
|
68
99
|
documented in [`docs/member-import-contract.md`](docs/member-import-contract.md).
|
|
69
100
|
It is not an implemented importer API.
|
package/THIRD_PARTY_NOTICES.md
CHANGED
|
@@ -11,3 +11,9 @@ The generated ISO jurisdiction catalogue is derived from Debian `iso-codes`
|
|
|
11
11
|
|
|
12
12
|
The generated catalogue may be replaced or updated independently by running
|
|
13
13
|
`npm run update:iso-jurisdictions` against the version pinned in that script.
|
|
14
|
+
|
|
15
|
+
SMART Health Card payloads use `pako` for browser-safe RFC 1951 raw DEFLATE:
|
|
16
|
+
|
|
17
|
+
- Source: https://github.com/nodeca/pako
|
|
18
|
+
- Copyright: Vitaly Puzrin and Andrey Tupitsin
|
|
19
|
+
- License: MIT
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export type ClinicalTerminologyLookupRequest = Readonly<{
|
|
2
|
+
resourceType: string;
|
|
3
|
+
field: string;
|
|
4
|
+
system: string;
|
|
5
|
+
code: string;
|
|
6
|
+
version?: string;
|
|
7
|
+
language: string;
|
|
8
|
+
}>;
|
|
9
|
+
export type ClinicalTerminologyLabels = Readonly<{
|
|
10
|
+
text?: string;
|
|
11
|
+
display?: string;
|
|
12
|
+
}>;
|
|
13
|
+
export type ClinicalBundleReaderOptions = Readonly<{
|
|
14
|
+
language: string;
|
|
15
|
+
resolveTerminology(request: ClinicalTerminologyLookupRequest): Promise<ClinicalTerminologyLabels>;
|
|
16
|
+
}>;
|
|
17
|
+
/**
|
|
18
|
+
* Creates a presentation-only Bundle for the shared IPS reader. Native FHIR
|
|
19
|
+
* and already-normalized `resource.meta.claims` converge on the same flat
|
|
20
|
+
* claims without changing the verified/signed input.
|
|
21
|
+
*/
|
|
22
|
+
export declare function projectClinicalBundleForReading<T extends Record<string, unknown>>(bundle: T, options: ClinicalBundleReaderOptions): Promise<T>;
|
|
23
|
+
export declare function formatLocalizedTerminologyLabel(input: Readonly<{
|
|
24
|
+
code: string;
|
|
25
|
+
text?: string;
|
|
26
|
+
display?: string;
|
|
27
|
+
language: string;
|
|
28
|
+
}>): string;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { normalizeClaimsFromFhirResource } from 'gdc-common-utils-ts/utils/interoperable-resource-operation';
|
|
2
|
+
const PRIMARY_CODE_PROJECTIONS = Object.freeze({
|
|
3
|
+
Condition: { field: 'Condition.code', code: 'Condition.code', text: 'Condition.code-text', display: 'Condition.code-display' },
|
|
4
|
+
Procedure: { field: 'Procedure.code', code: 'Procedure.code', text: 'Procedure.code-text', display: 'Procedure.code-display' },
|
|
5
|
+
DiagnosticReport: { field: 'DiagnosticReport.code', code: 'DiagnosticReport.code', text: 'DiagnosticReport.code-text', display: 'DiagnosticReport.code-display' },
|
|
6
|
+
Observation: { field: 'Observation.code', code: 'Observation.code', text: 'Observation.code-text', display: 'Observation.code-display' },
|
|
7
|
+
AllergyIntolerance: { field: 'AllergyIntolerance.code', code: 'AllergyIntolerance.code', text: 'AllergyIntolerance.code-text', display: 'AllergyIntolerance.code-display' },
|
|
8
|
+
Immunization: { field: 'Immunization.vaccineCode', code: 'Immunization.vaccine-code', text: 'Immunization.vaccine-code-text', display: 'Immunization.vaccine-code-display' },
|
|
9
|
+
});
|
|
10
|
+
/**
|
|
11
|
+
* Creates a presentation-only Bundle for the shared IPS reader. Native FHIR
|
|
12
|
+
* and already-normalized `resource.meta.claims` converge on the same flat
|
|
13
|
+
* claims without changing the verified/signed input.
|
|
14
|
+
*/
|
|
15
|
+
export async function projectClinicalBundleForReading(bundle, options) {
|
|
16
|
+
if (bundle.resourceType !== 'Bundle' || !Array.isArray(bundle.entry)) {
|
|
17
|
+
throw new TypeError('clinical_bundle_required');
|
|
18
|
+
}
|
|
19
|
+
const projected = structuredClone(bundle);
|
|
20
|
+
const entries = projected.entry;
|
|
21
|
+
for (const entry of entries) {
|
|
22
|
+
const resource = asRecord(entry.resource);
|
|
23
|
+
if (!resource.resourceType)
|
|
24
|
+
continue;
|
|
25
|
+
entry.resource = await projectResource(resource, options);
|
|
26
|
+
}
|
|
27
|
+
return projected;
|
|
28
|
+
}
|
|
29
|
+
export function formatLocalizedTerminologyLabel(input) {
|
|
30
|
+
const text = clean(input.text);
|
|
31
|
+
const display = clean(input.display);
|
|
32
|
+
const language = input.language.trim().toLowerCase().split(/[-_]/)[0];
|
|
33
|
+
if (language === 'en')
|
|
34
|
+
return display || text || input.code;
|
|
35
|
+
if (text && display && text.localeCompare(display, undefined, { sensitivity: 'accent' }) !== 0) {
|
|
36
|
+
return `${text} (${display})`;
|
|
37
|
+
}
|
|
38
|
+
return text || display || input.code;
|
|
39
|
+
}
|
|
40
|
+
async function projectResource(resource, options) {
|
|
41
|
+
const resourceType = clean(resource.resourceType);
|
|
42
|
+
const existingMeta = asRecord(resource.meta);
|
|
43
|
+
const existingClaims = asRecord(existingMeta.claims);
|
|
44
|
+
const claims = Object.keys(existingClaims).length
|
|
45
|
+
? { ...existingClaims }
|
|
46
|
+
: normalizeClaimsFromFhirResource(resource, {});
|
|
47
|
+
const projection = PRIMARY_CODE_PROJECTIONS[resourceType];
|
|
48
|
+
if (projection) {
|
|
49
|
+
const token = firstToken(claims[projection.code]);
|
|
50
|
+
const parsed = parseCodingToken(token);
|
|
51
|
+
const needsText = !clean(claims[projection.text]);
|
|
52
|
+
const needsDisplay = !clean(claims[projection.display]);
|
|
53
|
+
if (parsed && (needsText || needsDisplay)) {
|
|
54
|
+
const labels = await options.resolveTerminology({
|
|
55
|
+
resourceType,
|
|
56
|
+
field: projection.field,
|
|
57
|
+
...parsed,
|
|
58
|
+
language: options.language,
|
|
59
|
+
});
|
|
60
|
+
if (needsText && clean(labels.text))
|
|
61
|
+
claims[projection.text] = clean(labels.text);
|
|
62
|
+
if (needsDisplay && clean(labels.display))
|
|
63
|
+
claims[projection.display] = clean(labels.display);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
...resource,
|
|
68
|
+
language: clean(resource.language) || options.language,
|
|
69
|
+
meta: { ...existingMeta, claims },
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
function parseCodingToken(value) {
|
|
73
|
+
const separator = value.lastIndexOf('|');
|
|
74
|
+
if (separator <= 0 || separator === value.length - 1)
|
|
75
|
+
return undefined;
|
|
76
|
+
const systemAndVersion = value.slice(0, separator);
|
|
77
|
+
const code = value.slice(separator + 1);
|
|
78
|
+
const versionSeparator = systemAndVersion.indexOf('|');
|
|
79
|
+
if (versionSeparator > 0) {
|
|
80
|
+
return {
|
|
81
|
+
system: systemAndVersion.slice(0, versionSeparator),
|
|
82
|
+
version: systemAndVersion.slice(versionSeparator + 1),
|
|
83
|
+
code,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
return { system: systemAndVersion, code };
|
|
87
|
+
}
|
|
88
|
+
function firstToken(value) {
|
|
89
|
+
if (Array.isArray(value))
|
|
90
|
+
return clean(value[0]);
|
|
91
|
+
return clean(value).split(',')[0]?.trim() ?? '';
|
|
92
|
+
}
|
|
93
|
+
function asRecord(value) {
|
|
94
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
95
|
+
? value
|
|
96
|
+
: {};
|
|
97
|
+
}
|
|
98
|
+
function clean(value) {
|
|
99
|
+
return typeof value === 'string' ? value.trim() : '';
|
|
100
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export type FhirCoding = Readonly<{
|
|
2
|
+
system: string;
|
|
3
|
+
code: string;
|
|
4
|
+
version?: string;
|
|
5
|
+
display?: string;
|
|
6
|
+
}>;
|
|
7
|
+
export type FhirCodeableConcept = Readonly<{
|
|
8
|
+
coding: readonly FhirCoding[];
|
|
9
|
+
text?: string;
|
|
10
|
+
}>;
|
|
11
|
+
export type VeterinaryImmunizationInput = Readonly<{
|
|
12
|
+
identifier?: Readonly<{
|
|
13
|
+
system?: string;
|
|
14
|
+
value: string;
|
|
15
|
+
}>;
|
|
16
|
+
animalReference: string;
|
|
17
|
+
occurrenceDateTime: string;
|
|
18
|
+
vaccineCode: FhirCodeableConcept;
|
|
19
|
+
performerPractitionerRoleReference: string;
|
|
20
|
+
primarySource?: boolean;
|
|
21
|
+
lotNumber?: string;
|
|
22
|
+
expirationDate?: string;
|
|
23
|
+
manufacturerReference?: string;
|
|
24
|
+
site?: FhirCodeableConcept;
|
|
25
|
+
route?: FhirCodeableConcept;
|
|
26
|
+
doseQuantity?: Readonly<{
|
|
27
|
+
value: number;
|
|
28
|
+
unit?: string;
|
|
29
|
+
system?: string;
|
|
30
|
+
code?: string;
|
|
31
|
+
}>;
|
|
32
|
+
protocolApplied?: readonly Readonly<{
|
|
33
|
+
series?: string;
|
|
34
|
+
doseNumberPositiveInt?: number;
|
|
35
|
+
seriesDosesPositiveInt?: number;
|
|
36
|
+
targetDisease?: readonly FhirCodeableConcept[];
|
|
37
|
+
}>[];
|
|
38
|
+
}>;
|
|
39
|
+
export type VeterinaryImmunizationR4 = Readonly<Record<string, unknown> & {
|
|
40
|
+
resourceType: 'Immunization';
|
|
41
|
+
status: 'completed';
|
|
42
|
+
}>;
|
|
43
|
+
/**
|
|
44
|
+
* Builds the FHIR R4 Immunization recorded by a veterinary professional.
|
|
45
|
+
*
|
|
46
|
+
* Vaccine coding is deliberately supplied by the caller: the applicable code
|
|
47
|
+
* system and allowed vaccines depend on species, jurisdiction and the active
|
|
48
|
+
* terminology release. This helper never guesses a rabies or product code.
|
|
49
|
+
*/
|
|
50
|
+
export declare function buildVeterinaryImmunization(input: Partial<VeterinaryImmunizationInput>): VeterinaryImmunizationR4;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
function required(value, errorCode) {
|
|
2
|
+
if (typeof value !== 'string' || !value.trim())
|
|
3
|
+
throw new TypeError(errorCode);
|
|
4
|
+
return value.trim();
|
|
5
|
+
}
|
|
6
|
+
function validateCodeableConcept(value) {
|
|
7
|
+
if (!value || typeof value !== 'object')
|
|
8
|
+
throw new TypeError('veterinary_immunization_vaccine_coding_required');
|
|
9
|
+
const coding = value.coding;
|
|
10
|
+
if (!Array.isArray(coding) || coding.length === 0 || coding.some(item => {
|
|
11
|
+
if (!item || typeof item !== 'object')
|
|
12
|
+
return true;
|
|
13
|
+
const candidate = item;
|
|
14
|
+
return typeof candidate.system !== 'string' || !candidate.system.trim()
|
|
15
|
+
|| typeof candidate.code !== 'string' || !candidate.code.trim();
|
|
16
|
+
}))
|
|
17
|
+
throw new TypeError('veterinary_immunization_vaccine_coding_required');
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Builds the FHIR R4 Immunization recorded by a veterinary professional.
|
|
21
|
+
*
|
|
22
|
+
* Vaccine coding is deliberately supplied by the caller: the applicable code
|
|
23
|
+
* system and allowed vaccines depend on species, jurisdiction and the active
|
|
24
|
+
* terminology release. This helper never guesses a rabies or product code.
|
|
25
|
+
*/
|
|
26
|
+
export function buildVeterinaryImmunization(input) {
|
|
27
|
+
const animalReference = required(input.animalReference, 'veterinary_immunization_animal_reference_required');
|
|
28
|
+
const performerReference = required(input.performerPractitionerRoleReference, 'veterinary_immunization_performer_practitioner_role_required');
|
|
29
|
+
const occurrenceDateTime = required(input.occurrenceDateTime, 'veterinary_immunization_occurrence_required');
|
|
30
|
+
validateCodeableConcept(input.vaccineCode);
|
|
31
|
+
return {
|
|
32
|
+
resourceType: 'Immunization',
|
|
33
|
+
...(input.identifier ? { identifier: [{ ...input.identifier }] } : {}),
|
|
34
|
+
status: 'completed',
|
|
35
|
+
vaccineCode: input.vaccineCode,
|
|
36
|
+
patient: { reference: animalReference },
|
|
37
|
+
occurrenceDateTime,
|
|
38
|
+
primarySource: input.primarySource ?? true,
|
|
39
|
+
performer: [{ actor: { reference: performerReference } }],
|
|
40
|
+
...(input.lotNumber ? { lotNumber: input.lotNumber } : {}),
|
|
41
|
+
...(input.expirationDate ? { expirationDate: input.expirationDate } : {}),
|
|
42
|
+
...(input.manufacturerReference ? { manufacturer: { reference: input.manufacturerReference } } : {}),
|
|
43
|
+
...(input.site ? { site: input.site } : {}),
|
|
44
|
+
...(input.route ? { route: input.route } : {}),
|
|
45
|
+
...(input.doseQuantity ? { doseQuantity: input.doseQuantity } : {}),
|
|
46
|
+
...(input.protocolApplied ? { protocolApplied: input.protocolApplied } : {}),
|
|
47
|
+
};
|
|
48
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -2,14 +2,17 @@ export * from './animal-card.js';
|
|
|
2
2
|
export * from './assistant.js';
|
|
3
3
|
export * from './communication.js';
|
|
4
4
|
export * from './clinical-terminology.js';
|
|
5
|
+
export * from './clinical-bundle-reader.js';
|
|
5
6
|
export * from './digital-twin.js';
|
|
6
7
|
export * from './emergency.js';
|
|
7
8
|
export * from './financial.js';
|
|
8
9
|
export * from './group.js';
|
|
9
|
-
export * from './
|
|
10
|
+
export * from './immunization.js';
|
|
11
|
+
export * from './international-health-card.js';
|
|
10
12
|
export * from './iso-jurisdictions.js';
|
|
11
13
|
export * from './organization-application.js';
|
|
12
14
|
export * from './payment.js';
|
|
13
15
|
export * from './research-study.js';
|
|
14
16
|
export * from './sectors.js';
|
|
17
|
+
export * from './shc.js';
|
|
15
18
|
export * from './veterinary-sections.js';
|
package/dist/index.js
CHANGED
|
@@ -2,14 +2,17 @@ export * from './animal-card.js';
|
|
|
2
2
|
export * from './assistant.js';
|
|
3
3
|
export * from './communication.js';
|
|
4
4
|
export * from './clinical-terminology.js';
|
|
5
|
+
export * from './clinical-bundle-reader.js';
|
|
5
6
|
export * from './digital-twin.js';
|
|
6
7
|
export * from './emergency.js';
|
|
7
8
|
export * from './financial.js';
|
|
8
9
|
export * from './group.js';
|
|
9
|
-
export * from './
|
|
10
|
+
export * from './immunization.js';
|
|
11
|
+
export * from './international-health-card.js';
|
|
10
12
|
export * from './iso-jurisdictions.js';
|
|
11
13
|
export * from './organization-application.js';
|
|
12
14
|
export * from './payment.js';
|
|
13
15
|
export * from './research-study.js';
|
|
14
16
|
export * from './sectors.js';
|
|
17
|
+
export * from './shc.js';
|
|
15
18
|
export * from './veterinary-sections.js';
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type SmartHealthCardPayloadReferences } from './shc.js';
|
|
2
|
+
export type InternationalHealthCardCodedValue = Readonly<{
|
|
3
|
+
system: string;
|
|
4
|
+
code: string;
|
|
5
|
+
display?: string;
|
|
6
|
+
}>;
|
|
7
|
+
export type InternationalHealthCardVaccination = Readonly<{
|
|
8
|
+
resourceType: 'Immunization';
|
|
9
|
+
administeredAt: string;
|
|
10
|
+
validFrom: string;
|
|
11
|
+
validUntil: string;
|
|
12
|
+
product: InternationalHealthCardCodedValue;
|
|
13
|
+
targetDiseases: readonly InternationalHealthCardCodedValue[];
|
|
14
|
+
authorOrganizationUrn: string;
|
|
15
|
+
/** FHIR Immunization.expirationDate: expiry of the vaccine batch, not credential validity. */
|
|
16
|
+
vaccineBatchExpiresOn?: string;
|
|
17
|
+
}>;
|
|
18
|
+
export type InternationalHealthCardPrintData = Readonly<{
|
|
19
|
+
vaccination: InternationalHealthCardVaccination;
|
|
20
|
+
payloadReferences: SmartHealthCardPayloadReferences;
|
|
21
|
+
}>;
|
|
22
|
+
/**
|
|
23
|
+
* Projects human-readable IHC data from the authoritative FHIR R4 resource.
|
|
24
|
+
* `validFrom` and `validUntil` are credential/travel-policy dates supplied by
|
|
25
|
+
* the issuer; FHIR `Immunization.expirationDate` remains the vaccine batch
|
|
26
|
+
* expiry and is never reused as the validity end.
|
|
27
|
+
*/
|
|
28
|
+
export declare function buildInternationalHealthCardPrintData(input: Readonly<{
|
|
29
|
+
authoritativeBundle: Record<string, unknown>;
|
|
30
|
+
validFrom: string;
|
|
31
|
+
validUntil: string;
|
|
32
|
+
authorOrganizationUrn: string;
|
|
33
|
+
payloadBytes: Uint8Array;
|
|
34
|
+
}>): InternationalHealthCardPrintData;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { buildSmartHealthCardPayloadReferences } from './shc.js';
|
|
2
|
+
function record(value) {
|
|
3
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : undefined;
|
|
4
|
+
}
|
|
5
|
+
function requiredText(value, error) {
|
|
6
|
+
if (typeof value !== 'string' || !value.trim())
|
|
7
|
+
throw new TypeError(error);
|
|
8
|
+
return value.trim();
|
|
9
|
+
}
|
|
10
|
+
function firstCoding(value, error) {
|
|
11
|
+
const concept = record(value);
|
|
12
|
+
const coding = Array.isArray(concept?.coding) ? concept.coding.map(record).find(Boolean) : undefined;
|
|
13
|
+
if (!coding)
|
|
14
|
+
throw new TypeError(error);
|
|
15
|
+
const system = requiredText(coding.system, error);
|
|
16
|
+
const code = requiredText(coding.code, error);
|
|
17
|
+
const display = typeof coding.display === 'string' && coding.display.trim()
|
|
18
|
+
? coding.display.trim()
|
|
19
|
+
: typeof concept?.text === 'string' && concept.text.trim() ? concept.text.trim() : undefined;
|
|
20
|
+
return { system, code, ...(display ? { display } : {}) };
|
|
21
|
+
}
|
|
22
|
+
function targetDiseases(immunization) {
|
|
23
|
+
const protocols = Array.isArray(immunization.protocolApplied) ? immunization.protocolApplied : [];
|
|
24
|
+
return protocols.flatMap(protocolValue => {
|
|
25
|
+
const protocol = record(protocolValue);
|
|
26
|
+
const diseases = Array.isArray(protocol?.targetDisease) ? protocol.targetDisease : [];
|
|
27
|
+
return diseases.map(disease => firstCoding(disease, 'international_health_card_target_disease_coding_required'));
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Projects human-readable IHC data from the authoritative FHIR R4 resource.
|
|
32
|
+
* `validFrom` and `validUntil` are credential/travel-policy dates supplied by
|
|
33
|
+
* the issuer; FHIR `Immunization.expirationDate` remains the vaccine batch
|
|
34
|
+
* expiry and is never reused as the validity end.
|
|
35
|
+
*/
|
|
36
|
+
export function buildInternationalHealthCardPrintData(input) {
|
|
37
|
+
const entries = Array.isArray(input.authoritativeBundle?.entry) ? input.authoritativeBundle.entry : [];
|
|
38
|
+
const immunization = entries
|
|
39
|
+
.map(entry => record(record(entry)?.resource))
|
|
40
|
+
.find(resource => resource?.resourceType === 'Immunization' && resource.status === 'completed');
|
|
41
|
+
if (!immunization)
|
|
42
|
+
throw new TypeError('international_health_card_completed_immunization_required');
|
|
43
|
+
const validFrom = requiredText(input.validFrom, 'international_health_card_valid_from_required');
|
|
44
|
+
const validUntil = requiredText(input.validUntil, 'international_health_card_valid_until_required');
|
|
45
|
+
if (Number.isNaN(Date.parse(validFrom)) || Number.isNaN(Date.parse(validUntil))
|
|
46
|
+
|| Date.parse(validFrom) > Date.parse(validUntil)) {
|
|
47
|
+
throw new TypeError('international_health_card_validity_invalid');
|
|
48
|
+
}
|
|
49
|
+
const authorOrganizationUrn = requiredText(input.authorOrganizationUrn, 'international_health_card_author_organization_urn_required');
|
|
50
|
+
if (!/^urn:cds-[a-z]{2}(?:-[a-z0-9]{2,3})?:v[1-9][0-9]*:organization:[a-z][a-z0-9-]*:[^:]+$/i.test(authorOrganizationUrn)) {
|
|
51
|
+
throw new TypeError('international_health_card_author_organization_urn_invalid');
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
vaccination: {
|
|
55
|
+
resourceType: 'Immunization',
|
|
56
|
+
administeredAt: requiredText(immunization.occurrenceDateTime, 'international_health_card_occurrence_required'),
|
|
57
|
+
validFrom,
|
|
58
|
+
validUntil,
|
|
59
|
+
product: firstCoding(immunization.vaccineCode, 'international_health_card_vaccine_coding_required'),
|
|
60
|
+
targetDiseases: targetDiseases(immunization),
|
|
61
|
+
authorOrganizationUrn,
|
|
62
|
+
...(typeof immunization.expirationDate === 'string' && immunization.expirationDate.trim()
|
|
63
|
+
? { vaccineBatchExpiresOn: immunization.expirationDate.trim() }
|
|
64
|
+
: {}),
|
|
65
|
+
},
|
|
66
|
+
payloadReferences: buildSmartHealthCardPayloadReferences(input.payloadBytes),
|
|
67
|
+
};
|
|
68
|
+
}
|
package/dist/shc.d.ts
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
export declare const SmartHealthCardQrPrefix: "shc:/";
|
|
2
|
+
export declare const PostQuantumCompanionProofPrefix: "pqc:/";
|
|
3
|
+
export declare const SmartHealthCardMaxSingleQrLength = 1195;
|
|
4
|
+
export declare const SmartHealthCardMaxQrChunkBodyLength = 1191;
|
|
5
|
+
export declare const PostQuantumCompanionProofDefaultQrCount = 2;
|
|
6
|
+
export type PostQuantumCompanionProofQrOptions = Readonly<{
|
|
7
|
+
/** Exact number of companion QR labels produced per detached proof. Defaults to two. */
|
|
8
|
+
qrCount?: number;
|
|
9
|
+
}>;
|
|
10
|
+
export type SmartHealthCardPayloadReferences = Readonly<{
|
|
11
|
+
/** Exact SHA3-384 multihash encoded as base58btc and wrapped for ledger lookup. */
|
|
12
|
+
multihashUrn: string;
|
|
13
|
+
/** CIDv1(raw, SHA3-384) encoded as base58btc for content-addressed retrieval. */
|
|
14
|
+
cidV1: string;
|
|
15
|
+
}>;
|
|
16
|
+
export type SmartHealthCardPayload = Readonly<{
|
|
17
|
+
iss: string;
|
|
18
|
+
nbf: number;
|
|
19
|
+
vc: Readonly<{
|
|
20
|
+
type: readonly string[];
|
|
21
|
+
credentialSubject: Readonly<{
|
|
22
|
+
fhirVersion: string;
|
|
23
|
+
fhirBundle: Record<string, unknown>;
|
|
24
|
+
}>;
|
|
25
|
+
}>;
|
|
26
|
+
}>;
|
|
27
|
+
export type PreparedSmartHealthCard = Readonly<{
|
|
28
|
+
protectedHeader: Readonly<{
|
|
29
|
+
alg: 'ES256';
|
|
30
|
+
zip: 'DEF';
|
|
31
|
+
kid: string;
|
|
32
|
+
}>;
|
|
33
|
+
payload: SmartHealthCardPayload;
|
|
34
|
+
payloadBytes: Uint8Array;
|
|
35
|
+
encodedHeader: string;
|
|
36
|
+
encodedPayload: string;
|
|
37
|
+
signingInput: string;
|
|
38
|
+
signingBytes: Uint8Array;
|
|
39
|
+
}>;
|
|
40
|
+
/** Applies the payload-size rules required by the SMART Health Cards framework. */
|
|
41
|
+
export declare function minimizeFhirBundleForSmartHealthCard(input: Record<string, unknown>): Record<string, unknown>;
|
|
42
|
+
/** Prepares the exact raw-DEFLATE compact-JWS input to be signed by an external ES256 issuer key. */
|
|
43
|
+
export declare function prepareSmartHealthCard(input: Readonly<{
|
|
44
|
+
issuer: string;
|
|
45
|
+
notBefore: number;
|
|
46
|
+
kid: string;
|
|
47
|
+
fhirVersion: string;
|
|
48
|
+
fhirBundle: Record<string, unknown>;
|
|
49
|
+
additionalTypes?: readonly string[];
|
|
50
|
+
}>): PreparedSmartHealthCard;
|
|
51
|
+
export declare function assembleSmartHealthCardJws(prepared: Pick<PreparedSmartHealthCard, 'encodedHeader' | 'encodedPayload'>, signatureBase64Url: string): string;
|
|
52
|
+
/**
|
|
53
|
+
* Builds both project-supported base58btc references over the exact canonical
|
|
54
|
+
* SHC payload bytes. The multihash URN is a ledger lookup key; the CID is a
|
|
55
|
+
* content identifier. They are deliberately not aliases.
|
|
56
|
+
*/
|
|
57
|
+
export declare function buildSmartHealthCardPayloadReferences(payloadBytes: Uint8Array): SmartHealthCardPayloadReferences;
|
|
58
|
+
/** Encodes one compact SHC JWS into one or more standard numeric QR payloads. */
|
|
59
|
+
export declare function encodeSmartHealthCardQr(compactJws: string): readonly string[];
|
|
60
|
+
/** Reassembles standard SHC QR payloads regardless of scan order. */
|
|
61
|
+
export declare function decodeSmartHealthCardQr(qrValues: readonly string[]): string;
|
|
62
|
+
/** Encodes an RFC 7797 ML-DSA detached JWS as the version-one `pqc:/` companion transport. */
|
|
63
|
+
export declare function encodePostQuantumCompanionProofUri(detachedJws: string): string;
|
|
64
|
+
export declare function decodePostQuantumCompanionProofUri(uri: string): string;
|
|
65
|
+
/**
|
|
66
|
+
* Encodes an ML-DSA detached JWS using the same two-decimal-digits-per-JWS-
|
|
67
|
+
* character technique as SHC. The external SHC payload is never repeated.
|
|
68
|
+
*/
|
|
69
|
+
export declare function encodePostQuantumCompanionProofQr(detachedJws: string, options?: PostQuantumCompanionProofQrOptions): readonly string[];
|
|
70
|
+
/** Reassembles one complete detached proof regardless of companion QR scan order. */
|
|
71
|
+
export declare function decodePostQuantumCompanionProofQr(qrValues: readonly string[]): string;
|
|
72
|
+
/** Returns the exact uncompressed payload bytes that every detached companion proof verifies. */
|
|
73
|
+
export declare function decodeSmartHealthCardPayloadBytes(compactJws: string): Uint8Array;
|
package/dist/shc.js
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import { canonicalizeFhirResource } from 'gdc-common-utils-ts/utils/fhir-cid';
|
|
2
|
+
import { Content } from 'gdc-common-utils-ts/utils/content';
|
|
3
|
+
import { buildRawCidV1FromUtf8String } from 'gdc-common-utils-ts/utils/multiformat-profile';
|
|
4
|
+
import { encodeMultibaseSha3 } from 'gdc-common-utils-ts/utils/multibasehash';
|
|
5
|
+
import { deflateRaw, inflateRaw } from 'pako';
|
|
6
|
+
export const SmartHealthCardQrPrefix = 'shc:/';
|
|
7
|
+
export const PostQuantumCompanionProofPrefix = 'pqc:/';
|
|
8
|
+
export const SmartHealthCardMaxSingleQrLength = 1195;
|
|
9
|
+
export const SmartHealthCardMaxQrChunkBodyLength = 1191;
|
|
10
|
+
export const PostQuantumCompanionProofDefaultQrCount = 2;
|
|
11
|
+
function isRecord(value) {
|
|
12
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
13
|
+
}
|
|
14
|
+
function nonEmptyString(value, errorCode) {
|
|
15
|
+
if (typeof value !== 'string' || !value.trim())
|
|
16
|
+
throw new TypeError(errorCode);
|
|
17
|
+
return value.trim();
|
|
18
|
+
}
|
|
19
|
+
function collectBundleReferences(bundle) {
|
|
20
|
+
const references = new Map();
|
|
21
|
+
const entries = Array.isArray(bundle.entry) ? bundle.entry : [];
|
|
22
|
+
entries.forEach((entry, index) => {
|
|
23
|
+
if (!isRecord(entry) || !isRecord(entry.resource))
|
|
24
|
+
return;
|
|
25
|
+
const shortReference = `resource:${index}`;
|
|
26
|
+
if (typeof entry.fullUrl === 'string')
|
|
27
|
+
references.set(entry.fullUrl, shortReference);
|
|
28
|
+
if (typeof entry.resource.resourceType === 'string' && typeof entry.resource.id === 'string') {
|
|
29
|
+
references.set(`${entry.resource.resourceType}/${entry.resource.id}`, shortReference);
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
return references;
|
|
33
|
+
}
|
|
34
|
+
function minimizeValue(value, references, resourceRoot = false) {
|
|
35
|
+
if (Array.isArray(value))
|
|
36
|
+
return value.map(item => minimizeValue(item, references));
|
|
37
|
+
if (!isRecord(value))
|
|
38
|
+
return value;
|
|
39
|
+
const output = {};
|
|
40
|
+
const isResource = resourceRoot || typeof value.resourceType === 'string';
|
|
41
|
+
const isCoding = typeof value.system === 'string' && typeof value.code === 'string';
|
|
42
|
+
const isCodeableConcept = Array.isArray(value.coding);
|
|
43
|
+
for (const [key, child] of Object.entries(value)) {
|
|
44
|
+
if (isResource && (key === 'id' || key === 'text'))
|
|
45
|
+
continue;
|
|
46
|
+
if (isResource && key === 'meta') {
|
|
47
|
+
if (isRecord(child) && Array.isArray(child.security)) {
|
|
48
|
+
output.meta = { security: minimizeValue(child.security, references) };
|
|
49
|
+
}
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (isCoding && key === 'display')
|
|
53
|
+
continue;
|
|
54
|
+
if (isCodeableConcept && key === 'text')
|
|
55
|
+
continue;
|
|
56
|
+
if (key === 'reference' && typeof child === 'string' && references.has(child)) {
|
|
57
|
+
output.reference = references.get(child);
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
output[key] = minimizeValue(child, references);
|
|
61
|
+
}
|
|
62
|
+
return output;
|
|
63
|
+
}
|
|
64
|
+
/** Applies the payload-size rules required by the SMART Health Cards framework. */
|
|
65
|
+
export function minimizeFhirBundleForSmartHealthCard(input) {
|
|
66
|
+
if (input.resourceType !== 'Bundle')
|
|
67
|
+
throw new TypeError('shc_fhir_bundle_required');
|
|
68
|
+
const references = collectBundleReferences(input);
|
|
69
|
+
const minimized = minimizeValue(input, references, true);
|
|
70
|
+
const entries = Array.isArray(minimized.entry) ? minimized.entry : [];
|
|
71
|
+
minimized.entry = entries.map((entry, index) => isRecord(entry)
|
|
72
|
+
? { ...entry, fullUrl: `resource:${index}` }
|
|
73
|
+
: entry);
|
|
74
|
+
return JSON.parse(canonicalizeFhirResource(minimized, {
|
|
75
|
+
stripMetaVersionId: true,
|
|
76
|
+
stripNarrativeText: true,
|
|
77
|
+
stripNestedElementIds: true,
|
|
78
|
+
}));
|
|
79
|
+
}
|
|
80
|
+
/** Prepares the exact raw-DEFLATE compact-JWS input to be signed by an external ES256 issuer key. */
|
|
81
|
+
export function prepareSmartHealthCard(input) {
|
|
82
|
+
const issuer = nonEmptyString(input.issuer, 'shc_issuer_required');
|
|
83
|
+
if (!issuer.startsWith('https://') || issuer.endsWith('/'))
|
|
84
|
+
throw new TypeError('shc_issuer_https_without_trailing_slash_required');
|
|
85
|
+
if (!Number.isInteger(input.notBefore) || input.notBefore < 0)
|
|
86
|
+
throw new TypeError('shc_not_before_seconds_required');
|
|
87
|
+
const kid = nonEmptyString(input.kid, 'shc_kid_required');
|
|
88
|
+
const fhirVersion = nonEmptyString(input.fhirVersion, 'shc_fhir_version_required');
|
|
89
|
+
const payload = {
|
|
90
|
+
iss: issuer,
|
|
91
|
+
nbf: input.notBefore,
|
|
92
|
+
vc: {
|
|
93
|
+
type: ['https://smarthealth.cards#health-card', ...(input.additionalTypes ?? [])],
|
|
94
|
+
credentialSubject: {
|
|
95
|
+
fhirVersion,
|
|
96
|
+
fhirBundle: minimizeFhirBundleForSmartHealthCard(input.fhirBundle),
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
const protectedHeader = { alg: 'ES256', zip: 'DEF', kid };
|
|
101
|
+
const encodedHeader = Content.objectToRawBase64UrlSafe(protectedHeader);
|
|
102
|
+
const canonicalPayload = canonicalizeFhirResource(payload, {
|
|
103
|
+
stripMetaVersionId: false,
|
|
104
|
+
});
|
|
105
|
+
const payloadBytes = Content.stringToBytesUTF8(canonicalPayload);
|
|
106
|
+
const encodedPayload = Content.bytesToRawBase64UrlSafe(deflateRaw(payloadBytes));
|
|
107
|
+
const signingInput = `${encodedHeader}.${encodedPayload}`;
|
|
108
|
+
return {
|
|
109
|
+
protectedHeader,
|
|
110
|
+
payload: JSON.parse(canonicalPayload),
|
|
111
|
+
payloadBytes,
|
|
112
|
+
encodedHeader,
|
|
113
|
+
encodedPayload,
|
|
114
|
+
signingInput,
|
|
115
|
+
signingBytes: Content.stringToBytesUTF8(signingInput),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
export function assembleSmartHealthCardJws(prepared, signatureBase64Url) {
|
|
119
|
+
const signature = nonEmptyString(signatureBase64Url, 'shc_signature_required');
|
|
120
|
+
if (!/^[A-Za-z0-9_-]+$/.test(signature))
|
|
121
|
+
throw new TypeError('shc_signature_base64url_required');
|
|
122
|
+
return `${prepared.encodedHeader}.${prepared.encodedPayload}.${signature}`;
|
|
123
|
+
}
|
|
124
|
+
function encodeNumericBody(value) {
|
|
125
|
+
return [...value].map(character => {
|
|
126
|
+
const encoded = character.charCodeAt(0) - 45;
|
|
127
|
+
if (encoded < 0 || encoded > 99)
|
|
128
|
+
throw new TypeError('shc_jws_character_invalid');
|
|
129
|
+
return encoded.toString().padStart(2, '0');
|
|
130
|
+
}).join('');
|
|
131
|
+
}
|
|
132
|
+
function decodeNumericBody(value) {
|
|
133
|
+
if (!value || value.length % 2 !== 0 || !/^\d+$/.test(value))
|
|
134
|
+
throw new TypeError('shc_qr_numeric_body_invalid');
|
|
135
|
+
let decoded = '';
|
|
136
|
+
for (let index = 0; index < value.length; index += 2) {
|
|
137
|
+
const pair = Number(value.slice(index, index + 2));
|
|
138
|
+
if (pair > 77)
|
|
139
|
+
throw new TypeError('shc_qr_numeric_pair_invalid');
|
|
140
|
+
decoded += String.fromCharCode(pair + 45);
|
|
141
|
+
}
|
|
142
|
+
return decoded;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Builds both project-supported base58btc references over the exact canonical
|
|
146
|
+
* SHC payload bytes. The multihash URN is a ledger lookup key; the CID is a
|
|
147
|
+
* content identifier. They are deliberately not aliases.
|
|
148
|
+
*/
|
|
149
|
+
export function buildSmartHealthCardPayloadReferences(payloadBytes) {
|
|
150
|
+
if (!(payloadBytes instanceof Uint8Array) || payloadBytes.length === 0) {
|
|
151
|
+
throw new TypeError('shc_payload_bytes_required');
|
|
152
|
+
}
|
|
153
|
+
const payloadText = Content.bytesToStringUTF8(payloadBytes);
|
|
154
|
+
return {
|
|
155
|
+
multihashUrn: `urn:multibase:${encodeMultibaseSha3(payloadBytes, 384)}`,
|
|
156
|
+
cidV1: buildRawCidV1FromUtf8String(payloadText),
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
/** Encodes one compact SHC JWS into one or more standard numeric QR payloads. */
|
|
160
|
+
export function encodeSmartHealthCardQr(compactJws) {
|
|
161
|
+
const jws = nonEmptyString(compactJws, 'shc_compact_jws_required');
|
|
162
|
+
if (jws.length <= SmartHealthCardMaxSingleQrLength)
|
|
163
|
+
return [`${SmartHealthCardQrPrefix}${encodeNumericBody(jws)}`];
|
|
164
|
+
const chunkCount = Math.ceil(jws.length / SmartHealthCardMaxQrChunkBodyLength);
|
|
165
|
+
const chunkLength = Math.ceil(jws.length / chunkCount);
|
|
166
|
+
return Array.from({ length: chunkCount }, (_, index) => {
|
|
167
|
+
const chunk = jws.slice(index * chunkLength, (index + 1) * chunkLength);
|
|
168
|
+
return `${SmartHealthCardQrPrefix}${index + 1}/${chunkCount}/${encodeNumericBody(chunk)}`;
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
/** Reassembles standard SHC QR payloads regardless of scan order. */
|
|
172
|
+
export function decodeSmartHealthCardQr(qrValues) {
|
|
173
|
+
if (!Array.isArray(qrValues) || qrValues.length === 0)
|
|
174
|
+
throw new TypeError('shc_qr_required');
|
|
175
|
+
if (qrValues.length === 1 && /^shc:\/\d+$/.test(qrValues[0])) {
|
|
176
|
+
return decodeNumericBody(qrValues[0].slice(SmartHealthCardQrPrefix.length));
|
|
177
|
+
}
|
|
178
|
+
const chunks = qrValues.map(value => {
|
|
179
|
+
const match = /^shc:\/(\d+)\/(\d+)\/(\d+)$/.exec(value);
|
|
180
|
+
if (!match)
|
|
181
|
+
throw new TypeError('shc_qr_chunk_invalid');
|
|
182
|
+
return { index: Number(match[1]), count: Number(match[2]), value: decodeNumericBody(match[3]) };
|
|
183
|
+
});
|
|
184
|
+
const expectedCount = chunks[0].count;
|
|
185
|
+
if (expectedCount !== chunks.length
|
|
186
|
+
|| chunks.some(chunk => chunk.count !== expectedCount || chunk.index < 1 || chunk.index > expectedCount)
|
|
187
|
+
|| new Set(chunks.map(chunk => chunk.index)).size !== expectedCount) {
|
|
188
|
+
throw new TypeError('shc_qr_chunk_set_invalid');
|
|
189
|
+
}
|
|
190
|
+
return chunks.sort((left, right) => left.index - right.index).map(chunk => chunk.value).join('');
|
|
191
|
+
}
|
|
192
|
+
function validatePostQuantumDetachedJws(detachedJws) {
|
|
193
|
+
const match = /^([A-Za-z0-9_-]+)\.\.([A-Za-z0-9_-]+)$/.exec(detachedJws);
|
|
194
|
+
if (!match)
|
|
195
|
+
throw new TypeError('pqc_detached_jws_invalid');
|
|
196
|
+
let header;
|
|
197
|
+
try {
|
|
198
|
+
header = Content.base64UrlSafeToJSON(match[1]);
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
throw new TypeError('pqc_detached_jws_invalid');
|
|
202
|
+
}
|
|
203
|
+
if (!isRecord(header)
|
|
204
|
+
|| !['ML-DSA-44', 'ML-DSA-65', 'ML-DSA-87'].includes(String(header.alg))
|
|
205
|
+
|| typeof header.kid !== 'string' || !header.kid
|
|
206
|
+
|| header.b64 !== false
|
|
207
|
+
|| !Array.isArray(header.crit) || !header.crit.includes('b64')) {
|
|
208
|
+
throw new TypeError('pqc_detached_jws_invalid');
|
|
209
|
+
}
|
|
210
|
+
return detachedJws;
|
|
211
|
+
}
|
|
212
|
+
/** Encodes an RFC 7797 ML-DSA detached JWS as the version-one `pqc:/` companion transport. */
|
|
213
|
+
export function encodePostQuantumCompanionProofUri(detachedJws) {
|
|
214
|
+
const validated = validatePostQuantumDetachedJws(detachedJws);
|
|
215
|
+
return `${PostQuantumCompanionProofPrefix}${validated}`;
|
|
216
|
+
}
|
|
217
|
+
export function decodePostQuantumCompanionProofUri(uri) {
|
|
218
|
+
if (typeof uri !== 'string' || !uri.startsWith(PostQuantumCompanionProofPrefix)) {
|
|
219
|
+
throw new TypeError('pqc_companion_proof_uri_invalid');
|
|
220
|
+
}
|
|
221
|
+
return validatePostQuantumDetachedJws(uri.slice(PostQuantumCompanionProofPrefix.length));
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Encodes an ML-DSA detached JWS using the same two-decimal-digits-per-JWS-
|
|
225
|
+
* character technique as SHC. The external SHC payload is never repeated.
|
|
226
|
+
*/
|
|
227
|
+
export function encodePostQuantumCompanionProofQr(detachedJws, options = {}) {
|
|
228
|
+
const validated = validatePostQuantumDetachedJws(detachedJws);
|
|
229
|
+
const qrCount = options.qrCount ?? PostQuantumCompanionProofDefaultQrCount;
|
|
230
|
+
if (!Number.isInteger(qrCount) || qrCount < 1 || qrCount > 99 || qrCount > validated.length) {
|
|
231
|
+
throw new TypeError('pqc_companion_proof_qr_count_invalid');
|
|
232
|
+
}
|
|
233
|
+
const chunkLength = Math.ceil(validated.length / qrCount);
|
|
234
|
+
if (qrCount === 1)
|
|
235
|
+
return [`${PostQuantumCompanionProofPrefix}${encodeNumericBody(validated)}`];
|
|
236
|
+
return Array.from({ length: qrCount }, (_, index) => {
|
|
237
|
+
const chunk = validated.slice(index * chunkLength, (index + 1) * chunkLength);
|
|
238
|
+
return `${PostQuantumCompanionProofPrefix}${index + 1}/${qrCount}/${encodeNumericBody(chunk)}`;
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
/** Reassembles one complete detached proof regardless of companion QR scan order. */
|
|
242
|
+
export function decodePostQuantumCompanionProofQr(qrValues) {
|
|
243
|
+
if (!Array.isArray(qrValues) || qrValues.length === 0)
|
|
244
|
+
throw new TypeError('pqc_companion_proof_qr_required');
|
|
245
|
+
if (qrValues.length === 1 && /^pqc:\/\d+$/.test(qrValues[0])) {
|
|
246
|
+
return validatePostQuantumDetachedJws(decodeNumericBody(qrValues[0].slice(PostQuantumCompanionProofPrefix.length)));
|
|
247
|
+
}
|
|
248
|
+
const chunks = qrValues.map(value => {
|
|
249
|
+
const match = /^pqc:\/(\d+)\/(\d+)\/(\d+)$/.exec(value);
|
|
250
|
+
if (!match)
|
|
251
|
+
throw new TypeError('pqc_companion_proof_qr_chunk_invalid');
|
|
252
|
+
return { index: Number(match[1]), count: Number(match[2]), value: decodeNumericBody(match[3]) };
|
|
253
|
+
});
|
|
254
|
+
const expectedCount = chunks[0].count;
|
|
255
|
+
if (expectedCount !== chunks.length
|
|
256
|
+
|| chunks.some(chunk => chunk.count !== expectedCount || chunk.index < 1 || chunk.index > expectedCount)
|
|
257
|
+
|| new Set(chunks.map(chunk => chunk.index)).size !== expectedCount) {
|
|
258
|
+
throw new TypeError('pqc_companion_proof_qr_chunk_set_invalid');
|
|
259
|
+
}
|
|
260
|
+
return validatePostQuantumDetachedJws(chunks.sort((left, right) => left.index - right.index).map(chunk => chunk.value).join(''));
|
|
261
|
+
}
|
|
262
|
+
/** Returns the exact uncompressed payload bytes that every detached companion proof verifies. */
|
|
263
|
+
export function decodeSmartHealthCardPayloadBytes(compactJws) {
|
|
264
|
+
const parts = compactJws.split('.');
|
|
265
|
+
if (parts.length !== 3)
|
|
266
|
+
throw new TypeError('shc_compact_jws_invalid');
|
|
267
|
+
return inflateRaw(Content.base64ToBytes(parts[1]));
|
|
268
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vet-data-utils-ts",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.9",
|
|
4
4
|
"description": "Browser-safe governed VetChain data contracts",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Connecting Solution & Applications Ltd",
|
|
@@ -24,6 +24,10 @@
|
|
|
24
24
|
"types": "./dist/clinical-terminology.d.ts",
|
|
25
25
|
"default": "./dist/clinical-terminology.js"
|
|
26
26
|
},
|
|
27
|
+
"./clinical-bundle-reader": {
|
|
28
|
+
"types": "./dist/clinical-bundle-reader.d.ts",
|
|
29
|
+
"default": "./dist/clinical-bundle-reader.js"
|
|
30
|
+
},
|
|
27
31
|
"./digital-twin": {
|
|
28
32
|
"types": "./dist/digital-twin.d.ts",
|
|
29
33
|
"default": "./dist/digital-twin.js"
|
|
@@ -40,9 +44,13 @@
|
|
|
40
44
|
"types": "./dist/group.d.ts",
|
|
41
45
|
"default": "./dist/group.js"
|
|
42
46
|
},
|
|
43
|
-
"./
|
|
44
|
-
"types": "./dist/
|
|
45
|
-
"default": "./dist/
|
|
47
|
+
"./immunization": {
|
|
48
|
+
"types": "./dist/immunization.d.ts",
|
|
49
|
+
"default": "./dist/immunization.js"
|
|
50
|
+
},
|
|
51
|
+
"./international-health-card": {
|
|
52
|
+
"types": "./dist/international-health-card.d.ts",
|
|
53
|
+
"default": "./dist/international-health-card.js"
|
|
46
54
|
},
|
|
47
55
|
"./iso-jurisdictions": {
|
|
48
56
|
"types": "./dist/iso-jurisdictions.d.ts",
|
|
@@ -64,6 +72,10 @@
|
|
|
64
72
|
"types": "./dist/sectors.d.ts",
|
|
65
73
|
"default": "./dist/sectors.js"
|
|
66
74
|
},
|
|
75
|
+
"./shc": {
|
|
76
|
+
"types": "./dist/shc.d.ts",
|
|
77
|
+
"default": "./dist/shc.js"
|
|
78
|
+
},
|
|
67
79
|
"./veterinary-sections": {
|
|
68
80
|
"types": "./dist/veterinary-sections.d.ts",
|
|
69
81
|
"default": "./dist/veterinary-sections.js"
|
|
@@ -89,10 +101,12 @@
|
|
|
89
101
|
},
|
|
90
102
|
"devDependencies": {
|
|
91
103
|
"@types/node": "^22.0.0",
|
|
104
|
+
"@types/pako": "^2.0.4",
|
|
92
105
|
"typescript": "^5.5.4"
|
|
93
106
|
},
|
|
94
107
|
"dependencies": {
|
|
95
|
-
"gdc-common-utils-ts": "2.9.
|
|
108
|
+
"gdc-common-utils-ts": "2.9.10",
|
|
109
|
+
"pako": "^2.2.0"
|
|
96
110
|
},
|
|
97
111
|
"engines": {
|
|
98
112
|
"node": ">=20"
|
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
import { ResourceTypesFhirR4 } from 'gdc-common-utils-ts/constants/fhir-resource-types';
|
|
2
|
-
/** W3C ODRL and VetChain profile identifiers used by insurance eligibility consent. */
|
|
3
|
-
export declare const AnimalInsuranceConsentVocabulary: Readonly<{
|
|
4
|
-
readonly OdrlContext: "http://www.w3.org/ns/odrl.jsonld";
|
|
5
|
-
readonly Agreement: "Agreement";
|
|
6
|
-
readonly Read: "read";
|
|
7
|
-
readonly Equal: "eq";
|
|
8
|
-
readonly GreaterThanOrEqual: "gteq";
|
|
9
|
-
readonly LessThanOrEqual: "lteq";
|
|
10
|
-
readonly MediaType: "application/odrl+json";
|
|
11
|
-
readonly Profile: "https://vetchain.app/ns/odrl/animal-insurance-eligibility/v1";
|
|
12
|
-
readonly EligibilitySummaryTarget: "https://vetchain.app/ns/asset/animal-insurance-eligibility-summary";
|
|
13
|
-
readonly FactKindOperand: "https://vetchain.app/ns/odrl/operand/eligibility-fact-kind";
|
|
14
|
-
readonly ClinicalCodeOperand: "https://vetchain.app/ns/odrl/operand/clinical-code";
|
|
15
|
-
readonly DataPeriodStartOperand: "https://vetchain.app/ns/odrl/operand/data-period-start";
|
|
16
|
-
readonly DataPeriodEndOperand: "https://vetchain.app/ns/odrl/operand/data-period-end";
|
|
17
|
-
}>;
|
|
18
|
-
export declare const AnimalInsuranceEligibilityFactKinds: Readonly<{
|
|
19
|
-
readonly LatestEventDate: "latest-event-date";
|
|
20
|
-
readonly EventCount: "event-count";
|
|
21
|
-
}>;
|
|
22
|
-
export declare const AnimalInsuranceConsentErrors: Readonly<{
|
|
23
|
-
readonly InvalidOdrl: "insurance_consent_odrl_invalid";
|
|
24
|
-
readonly OdrlClaimsDiverge: "insurance_consent_odrl_claims_diverge";
|
|
25
|
-
readonly ResearchForbidden: "insurance_consent_research_forbidden";
|
|
26
|
-
readonly FactsRequired: "insurance_consent_facts_required";
|
|
27
|
-
}>;
|
|
28
|
-
/** Reusable values for package, GW and portal contract tests. */
|
|
29
|
-
export declare const ANIMAL_INSURANCE_CONSENT_TEST_DATA: Readonly<{
|
|
30
|
-
readonly consentId: "animal-insurance-consent-test-01";
|
|
31
|
-
readonly consentIdentifier: "urn:uuid:54b4d158-ed78-48b4-b006-84ba5a5d85b4";
|
|
32
|
-
readonly subjectReference: "Patient/animal-beneficiary-test-01";
|
|
33
|
-
readonly controllerReference: "RelatedPerson/controller-test-01";
|
|
34
|
-
readonly insurerReference: "Organization/animal-insurer-test-01";
|
|
35
|
-
readonly managerReference: "Organization/vetchain-consent-manager-test-01";
|
|
36
|
-
readonly enforcerReference: "Organization/vetchain-consent-enforcer-test-01";
|
|
37
|
-
readonly createdAt: "2026-09-08";
|
|
38
|
-
readonly consentPeriodStart: "2026-09-08T00:00:00Z";
|
|
39
|
-
readonly consentPeriodEnd: "2026-10-08T00:00:00Z";
|
|
40
|
-
readonly dataPeriodStart: "2025-09-08T00:00:00Z";
|
|
41
|
-
readonly dataPeriodEnd: "2026-09-08T00:00:00Z";
|
|
42
|
-
readonly actorRole: "http://terminology.hl7.org/CodeSystem/v3-RoleCode|PAYOR";
|
|
43
|
-
readonly action: "http://terminology.hl7.org/CodeSystem/consentaction|access";
|
|
44
|
-
readonly purpose: "http://terminology.hl7.org/CodeSystem/v3-ActReason|HOPERAT";
|
|
45
|
-
readonly scope: "http://terminology.hl7.org/CodeSystem/consentscope|patient-privacy";
|
|
46
|
-
readonly category: "http://loinc.org|59284-0";
|
|
47
|
-
readonly resourceType: "http://hl7.org/fhir/fhir-types|Observation";
|
|
48
|
-
readonly clinicalCode: "http://loinc.org|85353-1";
|
|
49
|
-
readonly secondClinicalCode: "http://loinc.org|8310-5";
|
|
50
|
-
readonly clinicalCodeList: "http://loinc.org|85353-1,http://loinc.org|8310-5";
|
|
51
|
-
readonly status: "active";
|
|
52
|
-
readonly decision: "permit";
|
|
53
|
-
readonly factKind: "latest-event-date";
|
|
54
|
-
readonly forbiddenPurpose: "http://terminology.hl7.org/CodeSystem/v3-ActReason|HRESCH";
|
|
55
|
-
readonly divergentInsurerReference: "Organization/different-animal-insurer-test-01";
|
|
56
|
-
readonly jurisdiction: "CA";
|
|
57
|
-
readonly fhirConsentResourceType: "Consent";
|
|
58
|
-
}>;
|
|
59
|
-
export type AnimalInsuranceEligibilityFact = Readonly<{
|
|
60
|
-
kind: typeof AnimalInsuranceEligibilityFactKinds[keyof typeof AnimalInsuranceEligibilityFactKinds];
|
|
61
|
-
clinicalCode: string;
|
|
62
|
-
}>;
|
|
63
|
-
export type AnimalInsuranceConsentInput = Readonly<{
|
|
64
|
-
id: string;
|
|
65
|
-
identifier: string;
|
|
66
|
-
subjectReference: string;
|
|
67
|
-
grantorReference: string;
|
|
68
|
-
granteeReference: string;
|
|
69
|
-
managerReference: string;
|
|
70
|
-
controllerReference: string;
|
|
71
|
-
createdAt: string;
|
|
72
|
-
consentPeriod: Readonly<{
|
|
73
|
-
start: string;
|
|
74
|
-
end: string;
|
|
75
|
-
}>;
|
|
76
|
-
dataPeriod: Readonly<{
|
|
77
|
-
start: string;
|
|
78
|
-
end: string;
|
|
79
|
-
}>;
|
|
80
|
-
actorRole: string;
|
|
81
|
-
action: string;
|
|
82
|
-
purpose: string;
|
|
83
|
-
scope: string;
|
|
84
|
-
category: string;
|
|
85
|
-
resourceType: string;
|
|
86
|
-
facts: readonly AnimalInsuranceEligibilityFact[];
|
|
87
|
-
}>;
|
|
88
|
-
/** Complete reusable builder input used by downstream GW, SDK and portal tests. */
|
|
89
|
-
export declare const ANIMAL_INSURANCE_CONSENT_TEST_INPUT: AnimalInsuranceConsentInput;
|
|
90
|
-
export type ClaimsFirstAnimalInsuranceConsent = Readonly<{
|
|
91
|
-
resourceType: typeof ResourceTypesFhirR4.Consent;
|
|
92
|
-
id: string;
|
|
93
|
-
meta: Readonly<{
|
|
94
|
-
claims: Readonly<Record<string, string>>;
|
|
95
|
-
}>;
|
|
96
|
-
}>;
|
|
97
|
-
export type AnimalInsuranceOdrlConstraint = Readonly<{
|
|
98
|
-
leftOperand: string;
|
|
99
|
-
operator: string;
|
|
100
|
-
rightOperand: string;
|
|
101
|
-
}>;
|
|
102
|
-
export type AnimalInsuranceOdrlPolicy = Readonly<{
|
|
103
|
-
'@context': typeof AnimalInsuranceConsentVocabulary.OdrlContext;
|
|
104
|
-
'@type': typeof AnimalInsuranceConsentVocabulary.Agreement;
|
|
105
|
-
uid: string;
|
|
106
|
-
profile: typeof AnimalInsuranceConsentVocabulary.Profile;
|
|
107
|
-
assigner: string;
|
|
108
|
-
assignee: string;
|
|
109
|
-
permission: readonly Readonly<{
|
|
110
|
-
target: typeof AnimalInsuranceConsentVocabulary.EligibilitySummaryTarget;
|
|
111
|
-
action: typeof AnimalInsuranceConsentVocabulary.Read;
|
|
112
|
-
constraint: readonly AnimalInsuranceOdrlConstraint[];
|
|
113
|
-
}>[];
|
|
114
|
-
}>;
|
|
115
|
-
/**
|
|
116
|
-
* Builds an atomic claims-first FHIR Consent for one insurer eligibility query.
|
|
117
|
-
* Standard authorization semantics are mirrored in flat Consent claims; the
|
|
118
|
-
* complete query-shaping policy remains an ODRL JSON attachment.
|
|
119
|
-
*/
|
|
120
|
-
export declare function buildAnimalInsuranceEligibilityConsent(input: AnimalInsuranceConsentInput): ClaimsFirstAnimalInsuranceConsent;
|
|
121
|
-
/** Parses the attached ODRL and fails closed unless its authorization semantics match the claims. */
|
|
122
|
-
export declare function validateAnimalInsuranceConsentOdrlParity(resource: ClaimsFirstAnimalInsuranceConsent): AnimalInsuranceOdrlPolicy;
|
|
123
|
-
/** Validates parity, then exports a native FHIR R4 Consent without ODRL-only fields. */
|
|
124
|
-
export declare function projectAnimalInsuranceConsentToFhirR4(resource: ClaimsFirstAnimalInsuranceConsent): Record<string, unknown>;
|
|
125
|
-
/** Validates parity, then exports a native FHIR R5 Consent without ODRL-only fields. */
|
|
126
|
-
export declare function projectAnimalInsuranceConsentToFhirR5(resource: ClaimsFirstAnimalInsuranceConsent): Record<string, unknown>;
|
|
@@ -1,228 +0,0 @@
|
|
|
1
|
-
import { ClaimConsent, ConsentDecisions, ConsentStatuses, } from 'gdc-common-utils-ts/models/consent-rule';
|
|
2
|
-
import { HealthcareConsentPurposes, } from 'gdc-common-utils-ts/constants/healthcare';
|
|
3
|
-
import { ResourceTypesFhirR4 } from 'gdc-common-utils-ts/constants/fhir-resource-types';
|
|
4
|
-
import { consentFlatToFhirR4, consentFlatToFhirR5, } from 'gdc-common-utils-ts/convert/convert-consent';
|
|
5
|
-
/** W3C ODRL and VetChain profile identifiers used by insurance eligibility consent. */
|
|
6
|
-
export const AnimalInsuranceConsentVocabulary = Object.freeze({
|
|
7
|
-
OdrlContext: 'http://www.w3.org/ns/odrl.jsonld',
|
|
8
|
-
Agreement: 'Agreement',
|
|
9
|
-
Read: 'read',
|
|
10
|
-
Equal: 'eq',
|
|
11
|
-
GreaterThanOrEqual: 'gteq',
|
|
12
|
-
LessThanOrEqual: 'lteq',
|
|
13
|
-
MediaType: 'application/odrl+json',
|
|
14
|
-
Profile: 'https://vetchain.app/ns/odrl/animal-insurance-eligibility/v1',
|
|
15
|
-
EligibilitySummaryTarget: 'https://vetchain.app/ns/asset/animal-insurance-eligibility-summary',
|
|
16
|
-
FactKindOperand: 'https://vetchain.app/ns/odrl/operand/eligibility-fact-kind',
|
|
17
|
-
ClinicalCodeOperand: 'https://vetchain.app/ns/odrl/operand/clinical-code',
|
|
18
|
-
DataPeriodStartOperand: 'https://vetchain.app/ns/odrl/operand/data-period-start',
|
|
19
|
-
DataPeriodEndOperand: 'https://vetchain.app/ns/odrl/operand/data-period-end',
|
|
20
|
-
});
|
|
21
|
-
export const AnimalInsuranceEligibilityFactKinds = Object.freeze({
|
|
22
|
-
LatestEventDate: 'latest-event-date',
|
|
23
|
-
EventCount: 'event-count',
|
|
24
|
-
});
|
|
25
|
-
export const AnimalInsuranceConsentErrors = Object.freeze({
|
|
26
|
-
InvalidOdrl: 'insurance_consent_odrl_invalid',
|
|
27
|
-
OdrlClaimsDiverge: 'insurance_consent_odrl_claims_diverge',
|
|
28
|
-
ResearchForbidden: 'insurance_consent_research_forbidden',
|
|
29
|
-
FactsRequired: 'insurance_consent_facts_required',
|
|
30
|
-
});
|
|
31
|
-
/** Reusable values for package, GW and portal contract tests. */
|
|
32
|
-
export const ANIMAL_INSURANCE_CONSENT_TEST_DATA = Object.freeze({
|
|
33
|
-
consentId: 'animal-insurance-consent-test-01',
|
|
34
|
-
consentIdentifier: 'urn:uuid:54b4d158-ed78-48b4-b006-84ba5a5d85b4',
|
|
35
|
-
subjectReference: 'Patient/animal-beneficiary-test-01',
|
|
36
|
-
controllerReference: 'RelatedPerson/controller-test-01',
|
|
37
|
-
insurerReference: 'Organization/animal-insurer-test-01',
|
|
38
|
-
managerReference: 'Organization/vetchain-consent-manager-test-01',
|
|
39
|
-
enforcerReference: 'Organization/vetchain-consent-enforcer-test-01',
|
|
40
|
-
createdAt: '2026-09-08',
|
|
41
|
-
consentPeriodStart: '2026-09-08T00:00:00Z',
|
|
42
|
-
consentPeriodEnd: '2026-10-08T00:00:00Z',
|
|
43
|
-
dataPeriodStart: '2025-09-08T00:00:00Z',
|
|
44
|
-
dataPeriodEnd: '2026-09-08T00:00:00Z',
|
|
45
|
-
actorRole: 'http://terminology.hl7.org/CodeSystem/v3-RoleCode|PAYOR',
|
|
46
|
-
action: 'http://terminology.hl7.org/CodeSystem/consentaction|access',
|
|
47
|
-
purpose: `http://terminology.hl7.org/CodeSystem/v3-ActReason|${HealthcareConsentPurposes.Operations}`,
|
|
48
|
-
scope: 'http://terminology.hl7.org/CodeSystem/consentscope|patient-privacy',
|
|
49
|
-
category: 'http://loinc.org|59284-0',
|
|
50
|
-
resourceType: 'http://hl7.org/fhir/fhir-types|Observation',
|
|
51
|
-
clinicalCode: 'http://loinc.org|85353-1',
|
|
52
|
-
secondClinicalCode: 'http://loinc.org|8310-5',
|
|
53
|
-
clinicalCodeList: 'http://loinc.org|85353-1,http://loinc.org|8310-5',
|
|
54
|
-
status: ConsentStatuses.Active,
|
|
55
|
-
decision: ConsentDecisions.Permit,
|
|
56
|
-
factKind: AnimalInsuranceEligibilityFactKinds.LatestEventDate,
|
|
57
|
-
forbiddenPurpose: `http://terminology.hl7.org/CodeSystem/v3-ActReason|${HealthcareConsentPurposes.Research}`,
|
|
58
|
-
divergentInsurerReference: 'Organization/different-animal-insurer-test-01',
|
|
59
|
-
jurisdiction: 'CA',
|
|
60
|
-
fhirConsentResourceType: ResourceTypesFhirR4.Consent,
|
|
61
|
-
});
|
|
62
|
-
/** Complete reusable builder input used by downstream GW, SDK and portal tests. */
|
|
63
|
-
export const ANIMAL_INSURANCE_CONSENT_TEST_INPUT = Object.freeze({
|
|
64
|
-
id: ANIMAL_INSURANCE_CONSENT_TEST_DATA.consentId,
|
|
65
|
-
identifier: ANIMAL_INSURANCE_CONSENT_TEST_DATA.consentIdentifier,
|
|
66
|
-
subjectReference: ANIMAL_INSURANCE_CONSENT_TEST_DATA.subjectReference,
|
|
67
|
-
grantorReference: ANIMAL_INSURANCE_CONSENT_TEST_DATA.controllerReference,
|
|
68
|
-
granteeReference: ANIMAL_INSURANCE_CONSENT_TEST_DATA.insurerReference,
|
|
69
|
-
managerReference: ANIMAL_INSURANCE_CONSENT_TEST_DATA.managerReference,
|
|
70
|
-
controllerReference: ANIMAL_INSURANCE_CONSENT_TEST_DATA.enforcerReference,
|
|
71
|
-
createdAt: ANIMAL_INSURANCE_CONSENT_TEST_DATA.createdAt,
|
|
72
|
-
consentPeriod: Object.freeze({
|
|
73
|
-
start: ANIMAL_INSURANCE_CONSENT_TEST_DATA.consentPeriodStart,
|
|
74
|
-
end: ANIMAL_INSURANCE_CONSENT_TEST_DATA.consentPeriodEnd,
|
|
75
|
-
}),
|
|
76
|
-
dataPeriod: Object.freeze({
|
|
77
|
-
start: ANIMAL_INSURANCE_CONSENT_TEST_DATA.dataPeriodStart,
|
|
78
|
-
end: ANIMAL_INSURANCE_CONSENT_TEST_DATA.dataPeriodEnd,
|
|
79
|
-
}),
|
|
80
|
-
actorRole: ANIMAL_INSURANCE_CONSENT_TEST_DATA.actorRole,
|
|
81
|
-
action: ANIMAL_INSURANCE_CONSENT_TEST_DATA.action,
|
|
82
|
-
purpose: ANIMAL_INSURANCE_CONSENT_TEST_DATA.purpose,
|
|
83
|
-
scope: ANIMAL_INSURANCE_CONSENT_TEST_DATA.scope,
|
|
84
|
-
category: ANIMAL_INSURANCE_CONSENT_TEST_DATA.category,
|
|
85
|
-
resourceType: ANIMAL_INSURANCE_CONSENT_TEST_DATA.resourceType,
|
|
86
|
-
facts: Object.freeze([{
|
|
87
|
-
kind: ANIMAL_INSURANCE_CONSENT_TEST_DATA.factKind,
|
|
88
|
-
clinicalCode: ANIMAL_INSURANCE_CONSENT_TEST_DATA.clinicalCode,
|
|
89
|
-
}, {
|
|
90
|
-
kind: AnimalInsuranceEligibilityFactKinds.EventCount,
|
|
91
|
-
clinicalCode: ANIMAL_INSURANCE_CONSENT_TEST_DATA.secondClinicalCode,
|
|
92
|
-
}]),
|
|
93
|
-
});
|
|
94
|
-
function encodeBase64Utf8(value) {
|
|
95
|
-
const bytes = new TextEncoder().encode(value);
|
|
96
|
-
let binary = '';
|
|
97
|
-
bytes.forEach((byte) => { binary += String.fromCharCode(byte); });
|
|
98
|
-
return btoa(binary);
|
|
99
|
-
}
|
|
100
|
-
function decodeBase64Utf8(value) {
|
|
101
|
-
const binary = atob(value);
|
|
102
|
-
const bytes = Uint8Array.from(binary, (character) => character.charCodeAt(0));
|
|
103
|
-
return new TextDecoder().decode(bytes);
|
|
104
|
-
}
|
|
105
|
-
function buildPolicy(input) {
|
|
106
|
-
return {
|
|
107
|
-
'@context': AnimalInsuranceConsentVocabulary.OdrlContext,
|
|
108
|
-
'@type': AnimalInsuranceConsentVocabulary.Agreement,
|
|
109
|
-
uid: input.identifier,
|
|
110
|
-
profile: AnimalInsuranceConsentVocabulary.Profile,
|
|
111
|
-
assigner: input.grantorReference,
|
|
112
|
-
assignee: input.granteeReference,
|
|
113
|
-
permission: input.facts.map((fact) => ({
|
|
114
|
-
target: AnimalInsuranceConsentVocabulary.EligibilitySummaryTarget,
|
|
115
|
-
action: AnimalInsuranceConsentVocabulary.Read,
|
|
116
|
-
constraint: [
|
|
117
|
-
{ leftOperand: AnimalInsuranceConsentVocabulary.FactKindOperand, operator: AnimalInsuranceConsentVocabulary.Equal, rightOperand: fact.kind },
|
|
118
|
-
{ leftOperand: AnimalInsuranceConsentVocabulary.ClinicalCodeOperand, operator: AnimalInsuranceConsentVocabulary.Equal, rightOperand: fact.clinicalCode },
|
|
119
|
-
{ leftOperand: AnimalInsuranceConsentVocabulary.DataPeriodStartOperand, operator: AnimalInsuranceConsentVocabulary.GreaterThanOrEqual, rightOperand: input.dataPeriod.start },
|
|
120
|
-
{ leftOperand: AnimalInsuranceConsentVocabulary.DataPeriodEndOperand, operator: AnimalInsuranceConsentVocabulary.LessThanOrEqual, rightOperand: input.dataPeriod.end },
|
|
121
|
-
],
|
|
122
|
-
})),
|
|
123
|
-
};
|
|
124
|
-
}
|
|
125
|
-
function rejectResearchSemantics(input) {
|
|
126
|
-
if (input.purpose.split('|').at(-1) === HealthcareConsentPurposes.Research
|
|
127
|
-
|| input.resourceType.split('|').at(-1) === ResourceTypesFhirR4.ResearchStudy) {
|
|
128
|
-
throw new TypeError(AnimalInsuranceConsentErrors.ResearchForbidden);
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
/**
|
|
132
|
-
* Builds an atomic claims-first FHIR Consent for one insurer eligibility query.
|
|
133
|
-
* Standard authorization semantics are mirrored in flat Consent claims; the
|
|
134
|
-
* complete query-shaping policy remains an ODRL JSON attachment.
|
|
135
|
-
*/
|
|
136
|
-
export function buildAnimalInsuranceEligibilityConsent(input) {
|
|
137
|
-
rejectResearchSemantics(input);
|
|
138
|
-
if (!input.facts.length)
|
|
139
|
-
throw new TypeError(AnimalInsuranceConsentErrors.FactsRequired);
|
|
140
|
-
if (input.facts.some((fact) => !Object.values(AnimalInsuranceEligibilityFactKinds).includes(fact.kind))) {
|
|
141
|
-
throw new TypeError(AnimalInsuranceConsentErrors.InvalidOdrl);
|
|
142
|
-
}
|
|
143
|
-
const policy = buildPolicy(input);
|
|
144
|
-
return {
|
|
145
|
-
resourceType: ResourceTypesFhirR4.Consent,
|
|
146
|
-
id: input.id,
|
|
147
|
-
meta: {
|
|
148
|
-
claims: {
|
|
149
|
-
'@context': 'org.hl7.fhir.api',
|
|
150
|
-
[ClaimConsent.identifier]: input.identifier,
|
|
151
|
-
[ClaimConsent.status]: ConsentStatuses.Active,
|
|
152
|
-
[ClaimConsent.subject]: input.subjectReference,
|
|
153
|
-
[ClaimConsent.date]: input.createdAt,
|
|
154
|
-
[ClaimConsent.decision]: ConsentDecisions.Permit,
|
|
155
|
-
[ClaimConsent.periodStart]: input.consentPeriod.start,
|
|
156
|
-
[ClaimConsent.periodEnd]: input.consentPeriod.end,
|
|
157
|
-
[ClaimConsent.dataPeriodStart]: input.dataPeriod.start,
|
|
158
|
-
[ClaimConsent.dataPeriodEnd]: input.dataPeriod.end,
|
|
159
|
-
[ClaimConsent.grantor]: input.grantorReference,
|
|
160
|
-
[ClaimConsent.grantee]: input.granteeReference,
|
|
161
|
-
[ClaimConsent.manager]: input.managerReference,
|
|
162
|
-
[ClaimConsent.controller]: input.controllerReference,
|
|
163
|
-
[ClaimConsent.actorIdentifier]: input.granteeReference,
|
|
164
|
-
[ClaimConsent.actorRole]: input.actorRole,
|
|
165
|
-
[ClaimConsent.action]: input.action,
|
|
166
|
-
[ClaimConsent.purpose]: input.purpose,
|
|
167
|
-
[ClaimConsent.scope]: input.scope,
|
|
168
|
-
[ClaimConsent.category]: input.category,
|
|
169
|
-
[ClaimConsent.resourceType]: input.resourceType,
|
|
170
|
-
[ClaimConsent.provisionCode]: input.facts.map((fact) => fact.clinicalCode).join(','),
|
|
171
|
-
[ClaimConsent.attachmentContentType]: AnimalInsuranceConsentVocabulary.MediaType,
|
|
172
|
-
[ClaimConsent.attachmentData]: encodeBase64Utf8(JSON.stringify(policy)),
|
|
173
|
-
},
|
|
174
|
-
},
|
|
175
|
-
};
|
|
176
|
-
}
|
|
177
|
-
function constraintValue(permission, operand) {
|
|
178
|
-
return permission.constraint.find((constraint) => constraint.leftOperand === operand)?.rightOperand;
|
|
179
|
-
}
|
|
180
|
-
/** Parses the attached ODRL and fails closed unless its authorization semantics match the claims. */
|
|
181
|
-
export function validateAnimalInsuranceConsentOdrlParity(resource) {
|
|
182
|
-
const claims = resource?.meta?.claims;
|
|
183
|
-
if (resource?.resourceType !== ResourceTypesFhirR4.Consent
|
|
184
|
-
|| claims?.[ClaimConsent.attachmentContentType] !== AnimalInsuranceConsentVocabulary.MediaType) {
|
|
185
|
-
throw new TypeError(AnimalInsuranceConsentErrors.InvalidOdrl);
|
|
186
|
-
}
|
|
187
|
-
let policy;
|
|
188
|
-
try {
|
|
189
|
-
policy = JSON.parse(decodeBase64Utf8(claims[ClaimConsent.attachmentData]));
|
|
190
|
-
}
|
|
191
|
-
catch {
|
|
192
|
-
throw new TypeError(AnimalInsuranceConsentErrors.InvalidOdrl);
|
|
193
|
-
}
|
|
194
|
-
const permissions = Array.isArray(policy.permission) ? policy.permission : [];
|
|
195
|
-
const codes = permissions.map((permission) => constraintValue(permission, AnimalInsuranceConsentVocabulary.ClinicalCodeOperand)).filter(Boolean);
|
|
196
|
-
const periodsMatch = permissions.every((permission) => constraintValue(permission, AnimalInsuranceConsentVocabulary.DataPeriodStartOperand) === claims[ClaimConsent.dataPeriodStart]
|
|
197
|
-
&& constraintValue(permission, AnimalInsuranceConsentVocabulary.DataPeriodEndOperand) === claims[ClaimConsent.dataPeriodEnd]);
|
|
198
|
-
const coreMatches = policy['@context'] === AnimalInsuranceConsentVocabulary.OdrlContext
|
|
199
|
-
&& policy['@type'] === AnimalInsuranceConsentVocabulary.Agreement
|
|
200
|
-
&& policy.profile === AnimalInsuranceConsentVocabulary.Profile
|
|
201
|
-
&& policy.uid === claims[ClaimConsent.identifier]
|
|
202
|
-
&& policy.assigner === claims[ClaimConsent.grantor]
|
|
203
|
-
&& policy.assignee === claims[ClaimConsent.grantee]
|
|
204
|
-
&& claims[ClaimConsent.actorIdentifier] === claims[ClaimConsent.grantee]
|
|
205
|
-
&& claims[ClaimConsent.decision] === ConsentDecisions.Permit
|
|
206
|
-
&& permissions.length > 0
|
|
207
|
-
&& permissions.every((permission) => permission.target === AnimalInsuranceConsentVocabulary.EligibilitySummaryTarget
|
|
208
|
-
&& permission.action === AnimalInsuranceConsentVocabulary.Read)
|
|
209
|
-
&& codes.join(',') === claims[ClaimConsent.provisionCode]
|
|
210
|
-
&& periodsMatch;
|
|
211
|
-
if (!coreMatches)
|
|
212
|
-
throw new TypeError(AnimalInsuranceConsentErrors.OdrlClaimsDiverge);
|
|
213
|
-
rejectResearchSemantics({
|
|
214
|
-
purpose: claims[ClaimConsent.purpose],
|
|
215
|
-
resourceType: claims[ClaimConsent.resourceType],
|
|
216
|
-
});
|
|
217
|
-
return policy;
|
|
218
|
-
}
|
|
219
|
-
/** Validates parity, then exports a native FHIR R4 Consent without ODRL-only fields. */
|
|
220
|
-
export function projectAnimalInsuranceConsentToFhirR4(resource) {
|
|
221
|
-
validateAnimalInsuranceConsentOdrlParity(resource);
|
|
222
|
-
return consentFlatToFhirR4(resource.meta.claims);
|
|
223
|
-
}
|
|
224
|
-
/** Validates parity, then exports a native FHIR R5 Consent without ODRL-only fields. */
|
|
225
|
-
export function projectAnimalInsuranceConsentToFhirR5(resource) {
|
|
226
|
-
validateAnimalInsuranceConsentOdrlParity(resource);
|
|
227
|
-
return consentFlatToFhirR5(resource.meta.claims);
|
|
228
|
-
}
|