vet-data-utils-ts 0.5.6 → 0.5.8

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
 
@@ -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,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
@@ -6,9 +6,12 @@ export * from './digital-twin.js';
6
6
  export * from './emergency.js';
7
7
  export * from './financial.js';
8
8
  export * from './group.js';
9
+ export * from './immunization.js';
10
+ export * from './international-health-card.js';
9
11
  export * from './iso-jurisdictions.js';
10
12
  export * from './organization-application.js';
11
13
  export * from './payment.js';
12
14
  export * from './research-study.js';
13
15
  export * from './sectors.js';
16
+ export * from './shc.js';
14
17
  export * from './veterinary-sections.js';
package/dist/index.js CHANGED
@@ -6,9 +6,12 @@ export * from './digital-twin.js';
6
6
  export * from './emergency.js';
7
7
  export * from './financial.js';
8
8
  export * from './group.js';
9
+ export * from './immunization.js';
10
+ export * from './international-health-card.js';
9
11
  export * from './iso-jurisdictions.js';
10
12
  export * from './organization-application.js';
11
13
  export * from './payment.js';
12
14
  export * from './research-study.js';
13
15
  export * from './sectors.js';
16
+ export * from './shc.js';
14
17
  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.6",
3
+ "version": "0.5.8",
4
4
  "description": "Browser-safe governed VetChain data contracts",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Connecting Solution & Applications Ltd",
@@ -40,6 +40,14 @@
40
40
  "types": "./dist/group.d.ts",
41
41
  "default": "./dist/group.js"
42
42
  },
43
+ "./immunization": {
44
+ "types": "./dist/immunization.d.ts",
45
+ "default": "./dist/immunization.js"
46
+ },
47
+ "./international-health-card": {
48
+ "types": "./dist/international-health-card.d.ts",
49
+ "default": "./dist/international-health-card.js"
50
+ },
43
51
  "./iso-jurisdictions": {
44
52
  "types": "./dist/iso-jurisdictions.d.ts",
45
53
  "default": "./dist/iso-jurisdictions.js"
@@ -60,6 +68,10 @@
60
68
  "types": "./dist/sectors.d.ts",
61
69
  "default": "./dist/sectors.js"
62
70
  },
71
+ "./shc": {
72
+ "types": "./dist/shc.d.ts",
73
+ "default": "./dist/shc.js"
74
+ },
63
75
  "./veterinary-sections": {
64
76
  "types": "./dist/veterinary-sections.d.ts",
65
77
  "default": "./dist/veterinary-sections.js"
@@ -85,10 +97,12 @@
85
97
  },
86
98
  "devDependencies": {
87
99
  "@types/node": "^22.0.0",
100
+ "@types/pako": "^2.0.4",
88
101
  "typescript": "^5.5.4"
89
102
  },
90
103
  "dependencies": {
91
- "gdc-common-utils-ts": "2.9.10"
104
+ "gdc-common-utils-ts": "2.9.10",
105
+ "pako": "^2.2.0"
92
106
  },
93
107
  "engines": {
94
108
  "node": ">=20"