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 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.
@@ -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 './insurance-consent.js';
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 './insurance-consent.js';
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.7",
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
- "./insurance-consent": {
44
- "types": "./dist/insurance-consent.d.ts",
45
- "default": "./dist/insurance-consent.js"
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.11"
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
- }