vet-sdk-core-ts 0.4.0
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 +37 -0
- package/dist/animal-identity.d.ts +109 -0
- package/dist/animal-identity.js +207 -0
- package/dist/card-issuance.d.ts +20 -0
- package/dist/card-issuance.js +59 -0
- package/dist/gateway-contract.d.ts +60 -0
- package/dist/gateway-contract.js +73 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/species.d.ts +484 -0
- package/dist/species.js +76 -0
- package/node_modules/vet-data-utils-ts/README.md +14 -0
- package/node_modules/vet-data-utils-ts/dist/animal-card.d.ts +14 -0
- package/node_modules/vet-data-utils-ts/dist/animal-card.js +44 -0
- package/node_modules/vet-data-utils-ts/dist/assistant.d.ts +7 -0
- package/node_modules/vet-data-utils-ts/dist/assistant.js +6 -0
- package/node_modules/vet-data-utils-ts/dist/digital-twin.d.ts +13 -0
- package/node_modules/vet-data-utils-ts/dist/digital-twin.js +24 -0
- package/node_modules/vet-data-utils-ts/dist/emergency.d.ts +17 -0
- package/node_modules/vet-data-utils-ts/dist/emergency.js +25 -0
- package/node_modules/vet-data-utils-ts/dist/index.d.ts +5 -0
- package/node_modules/vet-data-utils-ts/dist/index.js +5 -0
- package/node_modules/vet-data-utils-ts/dist/veterinary-sections.d.ts +101 -0
- package/node_modules/vet-data-utils-ts/dist/veterinary-sections.js +25 -0
- package/node_modules/vet-data-utils-ts/package.json +29 -0
- package/package.json +59 -0
- package/vendor/vet-data-utils-ts-0.3.0.tgz +0 -0
package/README.md
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# VetChain Core SDK
|
|
2
|
+
|
|
3
|
+
Browser-safe VetChain domain contracts. This repository is independent from
|
|
4
|
+
UHC SDK packages and must not import them.
|
|
5
|
+
|
|
6
|
+
The SDK consumes governed browser-safe values from `vet-data-utils-ts` and
|
|
7
|
+
owns gateway request construction. GW VET remains the policy authority.
|
|
8
|
+
|
|
9
|
+
The numeric animal identity and external evidence contract is documented in
|
|
10
|
+
[`docs/ANIMAL_IDENTITY.md`](docs/ANIMAL_IDENTITY.md).
|
|
11
|
+
|
|
12
|
+
## Animal species
|
|
13
|
+
|
|
14
|
+
`VetChainDomesticAnimalSpecies` provides stable application keys, official
|
|
15
|
+
NCBI Taxonomy identifiers, scientific names and broad animal groups for common
|
|
16
|
+
domesticated, farmed and companion animals. User-facing common names remain in
|
|
17
|
+
each portal's i18n catalogue.
|
|
18
|
+
|
|
19
|
+
The catalogue is a governed starter set, not a closed biological universe.
|
|
20
|
+
Callers may retain another verified positive non-human NCBI Taxonomy identifier.
|
|
21
|
+
This matters because animal keeping varies by jurisdiction and taxonomy evolves.
|
|
22
|
+
|
|
23
|
+
Current cards are issued with `issueVetChainAnimalCard`: five jurisdiction
|
|
24
|
+
digits plus `animalNumericId15 + checkDigit1`, and the matching
|
|
25
|
+
`did:web:{host}:card:vetchain:{jurisdiction5}:{animal16}`. Species is separate
|
|
26
|
+
card context and participates in the Damm calculation as `speciesId7` without
|
|
27
|
+
being repeated in the 21 printed digits.
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
import {
|
|
31
|
+
VetChainDomesticAnimalSpecies,
|
|
32
|
+
findVetChainSpeciesByTaxonomyId,
|
|
33
|
+
} from "vet-sdk-core-ts/species";
|
|
34
|
+
|
|
35
|
+
VetChainDomesticAnimalSpecies.DomesticFerret.ncbiTaxonomyId; // "9669"
|
|
36
|
+
findVetChainSpeciesByTaxonomyId("9685")?.key; // "Cat"
|
|
37
|
+
```
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
export type SpeciesId7 = string;
|
|
2
|
+
export type JurisdictionCode5 = string;
|
|
3
|
+
export type AnimalNumericId15 = string;
|
|
4
|
+
export type AnimalIdentifier16 = string;
|
|
5
|
+
export type VetChainAnimalCardNumber21 = string;
|
|
6
|
+
export type AnimalIndexCanonicalNumericId27 = string;
|
|
7
|
+
export type AnimalIndexAssetId = string;
|
|
8
|
+
export declare const AnimalIdentityEvidenceSystems: Readonly<{
|
|
9
|
+
readonly UsdaAin: "usda-ain";
|
|
10
|
+
readonly CfiaIso11784: "cfia-iso-11784";
|
|
11
|
+
readonly Iso11784_11785Microchip: "iso-11784-11785-microchip";
|
|
12
|
+
readonly HerdMark: "herd-mark";
|
|
13
|
+
readonly SlapTattoo: "slap-tattoo";
|
|
14
|
+
readonly Tattoo: "tattoo";
|
|
15
|
+
readonly Other: "other";
|
|
16
|
+
}>;
|
|
17
|
+
export type AnimalIdentityEvidenceSystem = typeof AnimalIdentityEvidenceSystems[keyof typeof AnimalIdentityEvidenceSystems];
|
|
18
|
+
export type AnimalIdentityEvidenceScope = "individual" | "group" | "other";
|
|
19
|
+
export type AnimalIdentityEvidence = Readonly<{
|
|
20
|
+
system: AnimalIdentityEvidenceSystem;
|
|
21
|
+
value: string;
|
|
22
|
+
scope: AnimalIdentityEvidenceScope;
|
|
23
|
+
/** True only when the evidence itself carries one individual 15-digit number. */
|
|
24
|
+
canSeedAnimalNumericId15: boolean;
|
|
25
|
+
}>;
|
|
26
|
+
/**
|
|
27
|
+
* Converts the canonical NCBI Taxonomy identifier to the seven telephone-safe
|
|
28
|
+
* digits used by the VetChain identity checksum. This is an NCBI taxonomy
|
|
29
|
+
* projection, never an organization fiscal identifier.
|
|
30
|
+
*/
|
|
31
|
+
export declare function buildSpeciesId7(ncbiTaxonomyId: string): SpeciesId7;
|
|
32
|
+
/** Country3 plus immutable birth/initial-registration region2. */
|
|
33
|
+
export declare function normalizeJurisdictionCode5(value: string): JurisdictionCode5;
|
|
34
|
+
/** Exact telephone-safe individual number; never accepts letters or truncation. */
|
|
35
|
+
export declare function normalizeAnimalNumericId15(value: string): AnimalNumericId15;
|
|
36
|
+
/** Returns the Damm check digit that makes the complete sequence validate to zero. */
|
|
37
|
+
export declare function computeDammCheckDigit(value: string): string;
|
|
38
|
+
/** Validates a Damm-protected sequence including its final check digit. */
|
|
39
|
+
export declare function validateDamm(value: string): boolean;
|
|
40
|
+
/**
|
|
41
|
+
* Builds the spoken/printed 15+1 identifier.
|
|
42
|
+
*
|
|
43
|
+
* The check is deliberately calculated in the same order used by the passport
|
|
44
|
+
* and telephone assistant: species, initial jurisdiction, animal number.
|
|
45
|
+
* `speciesId7` and `jurisdictionCode5` are required validation context even
|
|
46
|
+
* though only `animalNumericId15 + checkDigit1` forms this 16-digit segment.
|
|
47
|
+
* The check digit detects inconsistent input; it does not create uniqueness.
|
|
48
|
+
*/
|
|
49
|
+
export declare function buildAnimalIdentifier16(input: Readonly<{
|
|
50
|
+
speciesId7: string;
|
|
51
|
+
jurisdictionCode5: string;
|
|
52
|
+
animalNumericId15: string;
|
|
53
|
+
}>): AnimalIdentifier16;
|
|
54
|
+
export declare function validateAnimalIdentifier16(input: Readonly<{
|
|
55
|
+
speciesId7: string;
|
|
56
|
+
jurisdictionCode5: string;
|
|
57
|
+
animalNumericId15: string;
|
|
58
|
+
animalIdentifier16: string;
|
|
59
|
+
}>): boolean;
|
|
60
|
+
/** Builds the 21 visible digits: jurisdiction5 + animalNumericId15 + check1. */
|
|
61
|
+
export declare function buildVetChainAnimalCardNumber21(input: Readonly<{
|
|
62
|
+
speciesId7: string;
|
|
63
|
+
jurisdictionCode5: string;
|
|
64
|
+
animalNumericId15: string;
|
|
65
|
+
}>): VetChainAnimalCardNumber21;
|
|
66
|
+
export declare function validateVetChainAnimalCardNumber21(input: Readonly<{
|
|
67
|
+
speciesId7: string;
|
|
68
|
+
cardNumber21: string;
|
|
69
|
+
}>): boolean;
|
|
70
|
+
/**
|
|
71
|
+
* Builds the exact 27-digit Animal Index lookup preimage.
|
|
72
|
+
*
|
|
73
|
+
* The printed/telephone Damm digit is deliberately excluded: it detects input
|
|
74
|
+
* errors but is not identity material. Species and initial jurisdiction remain
|
|
75
|
+
* included so equal 15-digit animal numbers in different contexts do not map
|
|
76
|
+
* to the same index asset.
|
|
77
|
+
*/
|
|
78
|
+
export declare function buildAnimalIndexCanonicalNumericId27(input: Readonly<{
|
|
79
|
+
speciesId7: string;
|
|
80
|
+
jurisdictionCode5: string;
|
|
81
|
+
animalNumericId15: string;
|
|
82
|
+
}>): AnimalIndexCanonicalNumericId27;
|
|
83
|
+
/**
|
|
84
|
+
* Produces the lowercase SHA3-256 `assetId` stored by the permissioned Animal
|
|
85
|
+
* Index. Its asset may reveal only the currently active index-provider routing
|
|
86
|
+
* record; resolving it never authorizes access to the animal's data.
|
|
87
|
+
*
|
|
88
|
+
* This deterministic digest is a pseudonymous lookup key, not encryption. Do
|
|
89
|
+
* not publish the canonical 27 digits or treat the digest as confidential.
|
|
90
|
+
*/
|
|
91
|
+
export declare function buildAnimalIndexAssetId(input: Readonly<{
|
|
92
|
+
speciesId7: string;
|
|
93
|
+
jurisdictionCode5: string;
|
|
94
|
+
animalNumericId15: string;
|
|
95
|
+
}>): Promise<AnimalIndexAssetId>;
|
|
96
|
+
export declare function isAnimalIndexAssetId(value: string): value is AnimalIndexAssetId;
|
|
97
|
+
/**
|
|
98
|
+
* Normalizes evidence without claiming verification.
|
|
99
|
+
*
|
|
100
|
+
* USDA AIN is a 15-digit individual number beginning `840`. Canadian CFIA
|
|
101
|
+
* individual indicators use ISO 11784 and begin `124`. ISO 11784/11785 animal
|
|
102
|
+
* transponders are global and are never classified by the animal's country.
|
|
103
|
+
* Herd marks and slaughter tattoos can identify a group/location rather than
|
|
104
|
+
* one animal and therefore never seed `animalNumericId15`.
|
|
105
|
+
*/
|
|
106
|
+
export declare function normalizeAnimalIdentityEvidence(input: Readonly<{
|
|
107
|
+
system: AnimalIdentityEvidenceSystem;
|
|
108
|
+
value: string;
|
|
109
|
+
}>): AnimalIdentityEvidence;
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
// Copyright 2026 Connecting Solution & Applications Ltd under the Apache License, Version 2.0.
|
|
2
|
+
import { sha3_256 } from "@noble/hashes/sha3.js";
|
|
3
|
+
import { bytesToHex, utf8ToBytes } from "@noble/hashes/utils.js";
|
|
4
|
+
export const AnimalIdentityEvidenceSystems = Object.freeze({
|
|
5
|
+
UsdaAin: "usda-ain",
|
|
6
|
+
CfiaIso11784: "cfia-iso-11784",
|
|
7
|
+
Iso11784_11785Microchip: "iso-11784-11785-microchip",
|
|
8
|
+
HerdMark: "herd-mark",
|
|
9
|
+
SlapTattoo: "slap-tattoo",
|
|
10
|
+
Tattoo: "tattoo",
|
|
11
|
+
Other: "other",
|
|
12
|
+
});
|
|
13
|
+
const DAMM_TABLE = Object.freeze([
|
|
14
|
+
Object.freeze([0, 3, 1, 7, 5, 9, 8, 6, 4, 2]),
|
|
15
|
+
Object.freeze([7, 0, 9, 2, 1, 5, 4, 8, 6, 3]),
|
|
16
|
+
Object.freeze([4, 2, 0, 6, 8, 7, 1, 3, 5, 9]),
|
|
17
|
+
Object.freeze([1, 7, 5, 0, 9, 8, 3, 4, 2, 6]),
|
|
18
|
+
Object.freeze([6, 1, 2, 3, 0, 4, 5, 9, 7, 8]),
|
|
19
|
+
Object.freeze([3, 6, 7, 4, 2, 0, 9, 5, 8, 1]),
|
|
20
|
+
Object.freeze([5, 8, 6, 9, 7, 2, 0, 1, 3, 4]),
|
|
21
|
+
Object.freeze([8, 9, 4, 5, 3, 6, 2, 0, 1, 7]),
|
|
22
|
+
Object.freeze([9, 4, 3, 8, 6, 1, 7, 2, 0, 5]),
|
|
23
|
+
Object.freeze([2, 5, 8, 1, 4, 3, 6, 7, 9, 0]),
|
|
24
|
+
]);
|
|
25
|
+
/**
|
|
26
|
+
* Converts the canonical NCBI Taxonomy identifier to the seven telephone-safe
|
|
27
|
+
* digits used by the VetChain identity checksum. This is an NCBI taxonomy
|
|
28
|
+
* projection, never an organization fiscal identifier.
|
|
29
|
+
*/
|
|
30
|
+
export function buildSpeciesId7(ncbiTaxonomyId) {
|
|
31
|
+
const source = String(ncbiTaxonomyId || "").trim();
|
|
32
|
+
if (!/^\d{1,7}$/.test(source)) {
|
|
33
|
+
throw new TypeError("NCBI Taxonomy identifier must contain at most seven digits.");
|
|
34
|
+
}
|
|
35
|
+
const normalized = source.replace(/^0+/, "") || "0";
|
|
36
|
+
if (normalized === "9606") {
|
|
37
|
+
throw new TypeError("Human NCBI Taxonomy identifier 9606 is not valid for a VetChain animal.");
|
|
38
|
+
}
|
|
39
|
+
if (normalized === "0")
|
|
40
|
+
throw new TypeError("NCBI Taxonomy identifier must be positive.");
|
|
41
|
+
return normalized.padStart(7, "0");
|
|
42
|
+
}
|
|
43
|
+
/** Country3 plus immutable birth/initial-registration region2. */
|
|
44
|
+
export function normalizeJurisdictionCode5(value) {
|
|
45
|
+
return normalizeFixedDigits(value, 5, "jurisdictionCode5");
|
|
46
|
+
}
|
|
47
|
+
/** Exact telephone-safe individual number; never accepts letters or truncation. */
|
|
48
|
+
export function normalizeAnimalNumericId15(value) {
|
|
49
|
+
return normalizeFixedDigits(value, 15, "animalNumericId15");
|
|
50
|
+
}
|
|
51
|
+
/** Returns the Damm check digit that makes the complete sequence validate to zero. */
|
|
52
|
+
export function computeDammCheckDigit(value) {
|
|
53
|
+
const digits = normalizeDigits(value, "Damm input");
|
|
54
|
+
let interim = 0;
|
|
55
|
+
for (const character of digits)
|
|
56
|
+
interim = DAMM_TABLE[interim][Number(character)];
|
|
57
|
+
return String(interim);
|
|
58
|
+
}
|
|
59
|
+
/** Validates a Damm-protected sequence including its final check digit. */
|
|
60
|
+
export function validateDamm(value) {
|
|
61
|
+
try {
|
|
62
|
+
const digits = normalizeDigits(value, "Damm input");
|
|
63
|
+
let interim = 0;
|
|
64
|
+
for (const character of digits)
|
|
65
|
+
interim = DAMM_TABLE[interim][Number(character)];
|
|
66
|
+
return interim === 0;
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Builds the spoken/printed 15+1 identifier.
|
|
74
|
+
*
|
|
75
|
+
* The check is deliberately calculated in the same order used by the passport
|
|
76
|
+
* and telephone assistant: species, initial jurisdiction, animal number.
|
|
77
|
+
* `speciesId7` and `jurisdictionCode5` are required validation context even
|
|
78
|
+
* though only `animalNumericId15 + checkDigit1` forms this 16-digit segment.
|
|
79
|
+
* The check digit detects inconsistent input; it does not create uniqueness.
|
|
80
|
+
*/
|
|
81
|
+
export function buildAnimalIdentifier16(input) {
|
|
82
|
+
const speciesId7 = buildSpeciesId7(input.speciesId7);
|
|
83
|
+
const jurisdictionCode5 = normalizeJurisdictionCode5(input.jurisdictionCode5);
|
|
84
|
+
const animalNumericId15 = normalizeAnimalNumericId15(input.animalNumericId15);
|
|
85
|
+
const checkDigit1 = computeDammCheckDigit(`${speciesId7}${jurisdictionCode5}${animalNumericId15}`);
|
|
86
|
+
return `${animalNumericId15}${checkDigit1}`;
|
|
87
|
+
}
|
|
88
|
+
export function validateAnimalIdentifier16(input) {
|
|
89
|
+
try {
|
|
90
|
+
const animalNumericId15 = normalizeAnimalNumericId15(input.animalNumericId15);
|
|
91
|
+
const animalIdentifier16 = normalizeFixedDigits(input.animalIdentifier16, 16, "animalIdentifier16");
|
|
92
|
+
return animalIdentifier16.startsWith(animalNumericId15)
|
|
93
|
+
&& animalIdentifier16 === buildAnimalIdentifier16({ ...input, animalNumericId15 });
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/** Builds the 21 visible digits: jurisdiction5 + animalNumericId15 + check1. */
|
|
100
|
+
export function buildVetChainAnimalCardNumber21(input) {
|
|
101
|
+
const jurisdictionCode5 = normalizeJurisdictionCode5(input.jurisdictionCode5);
|
|
102
|
+
return `${jurisdictionCode5}${buildAnimalIdentifier16({ ...input, jurisdictionCode5 })}`;
|
|
103
|
+
}
|
|
104
|
+
export function validateVetChainAnimalCardNumber21(input) {
|
|
105
|
+
try {
|
|
106
|
+
const cardNumber21 = normalizeFixedDigits(input.cardNumber21, 21, "cardNumber21");
|
|
107
|
+
const jurisdictionCode5 = cardNumber21.slice(0, 5);
|
|
108
|
+
const animalIdentifier16 = cardNumber21.slice(5);
|
|
109
|
+
const animalNumericId15 = animalIdentifier16.slice(0, 15);
|
|
110
|
+
return validateAnimalIdentifier16({
|
|
111
|
+
speciesId7: input.speciesId7,
|
|
112
|
+
jurisdictionCode5,
|
|
113
|
+
animalNumericId15,
|
|
114
|
+
animalIdentifier16,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Builds the exact 27-digit Animal Index lookup preimage.
|
|
123
|
+
*
|
|
124
|
+
* The printed/telephone Damm digit is deliberately excluded: it detects input
|
|
125
|
+
* errors but is not identity material. Species and initial jurisdiction remain
|
|
126
|
+
* included so equal 15-digit animal numbers in different contexts do not map
|
|
127
|
+
* to the same index asset.
|
|
128
|
+
*/
|
|
129
|
+
export function buildAnimalIndexCanonicalNumericId27(input) {
|
|
130
|
+
return `${buildSpeciesId7(input.speciesId7)}${normalizeJurisdictionCode5(input.jurisdictionCode5)}${normalizeAnimalNumericId15(input.animalNumericId15)}`;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Produces the lowercase SHA3-256 `assetId` stored by the permissioned Animal
|
|
134
|
+
* Index. Its asset may reveal only the currently active index-provider routing
|
|
135
|
+
* record; resolving it never authorizes access to the animal's data.
|
|
136
|
+
*
|
|
137
|
+
* This deterministic digest is a pseudonymous lookup key, not encryption. Do
|
|
138
|
+
* not publish the canonical 27 digits or treat the digest as confidential.
|
|
139
|
+
*/
|
|
140
|
+
export async function buildAnimalIndexAssetId(input) {
|
|
141
|
+
const canonicalId27 = buildAnimalIndexCanonicalNumericId27(input);
|
|
142
|
+
return bytesToHex(sha3_256(utf8ToBytes(canonicalId27)));
|
|
143
|
+
}
|
|
144
|
+
export function isAnimalIndexAssetId(value) {
|
|
145
|
+
return /^[a-f0-9]{64}$/.test(String(value || ""));
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Normalizes evidence without claiming verification.
|
|
149
|
+
*
|
|
150
|
+
* USDA AIN is a 15-digit individual number beginning `840`. Canadian CFIA
|
|
151
|
+
* individual indicators use ISO 11784 and begin `124`. ISO 11784/11785 animal
|
|
152
|
+
* transponders are global and are never classified by the animal's country.
|
|
153
|
+
* Herd marks and slaughter tattoos can identify a group/location rather than
|
|
154
|
+
* one animal and therefore never seed `animalNumericId15`.
|
|
155
|
+
*/
|
|
156
|
+
export function normalizeAnimalIdentityEvidence(input) {
|
|
157
|
+
const system = input.system;
|
|
158
|
+
if (!Object.values(AnimalIdentityEvidenceSystems).includes(system)) {
|
|
159
|
+
throw new TypeError("Unsupported animal identity evidence system.");
|
|
160
|
+
}
|
|
161
|
+
if (system === AnimalIdentityEvidenceSystems.UsdaAin) {
|
|
162
|
+
const value = normalizeAnimalNumericId15(input.value);
|
|
163
|
+
if (!value.startsWith("840"))
|
|
164
|
+
throw new TypeError("USDA AIN must begin with country code 840.");
|
|
165
|
+
return Object.freeze({ system, value, scope: "individual", canSeedAnimalNumericId15: true });
|
|
166
|
+
}
|
|
167
|
+
if (system === AnimalIdentityEvidenceSystems.CfiaIso11784) {
|
|
168
|
+
const value = normalizeAnimalNumericId15(input.value);
|
|
169
|
+
if (!value.startsWith("124"))
|
|
170
|
+
throw new TypeError("Canadian CFIA ISO 11784 identifier must begin with country code 124.");
|
|
171
|
+
return Object.freeze({ system, value, scope: "individual", canSeedAnimalNumericId15: true });
|
|
172
|
+
}
|
|
173
|
+
if (system === AnimalIdentityEvidenceSystems.Iso11784_11785Microchip) {
|
|
174
|
+
const value = normalizeAnimalNumericId15(input.value);
|
|
175
|
+
return Object.freeze({ system, value, scope: "individual", canSeedAnimalNumericId15: true });
|
|
176
|
+
}
|
|
177
|
+
if (system === AnimalIdentityEvidenceSystems.HerdMark) {
|
|
178
|
+
const value = normalizeEvidenceText(input.value);
|
|
179
|
+
return Object.freeze({ system, value, scope: "group", canSeedAnimalNumericId15: false });
|
|
180
|
+
}
|
|
181
|
+
if (system === AnimalIdentityEvidenceSystems.SlapTattoo) {
|
|
182
|
+
const value = normalizeEvidenceText(input.value);
|
|
183
|
+
return Object.freeze({ system, value, scope: "group", canSeedAnimalNumericId15: false });
|
|
184
|
+
}
|
|
185
|
+
const value = normalizeEvidenceText(input.value);
|
|
186
|
+
return Object.freeze({ system, value, scope: system === AnimalIdentityEvidenceSystems.Tattoo ? "individual" : "other", canSeedAnimalNumericId15: false });
|
|
187
|
+
}
|
|
188
|
+
function normalizeFixedDigits(value, length, name) {
|
|
189
|
+
const digits = normalizeDigits(value, name);
|
|
190
|
+
if (digits.length !== length)
|
|
191
|
+
throw new TypeError(`${name} must contain exactly ${length} digits.`);
|
|
192
|
+
return digits;
|
|
193
|
+
}
|
|
194
|
+
function normalizeDigits(value, name) {
|
|
195
|
+
const source = String(value || "").trim();
|
|
196
|
+
if (!source || !/^[0-9\s-]+$/.test(source))
|
|
197
|
+
throw new TypeError(`${name} must contain decimal digits only.`);
|
|
198
|
+
return source.replace(/[\s-]/g, "");
|
|
199
|
+
}
|
|
200
|
+
function normalizeEvidenceText(value) {
|
|
201
|
+
const normalized = String(value || "").trim().replace(/\s+/g, " ").toUpperCase();
|
|
202
|
+
if (!normalized)
|
|
203
|
+
throw new TypeError("Animal identity evidence value is required.");
|
|
204
|
+
if (normalized.length > 120)
|
|
205
|
+
throw new TypeError("Animal identity evidence value is too long.");
|
|
206
|
+
return normalized;
|
|
207
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export type VetChainIssuedAnimalCard = Readonly<{
|
|
2
|
+
providerCode9: string;
|
|
3
|
+
jurisdictionCode5: string;
|
|
4
|
+
ncbiTaxonomyId: string;
|
|
5
|
+
speciesCode5: string;
|
|
6
|
+
animalNumericId15: string;
|
|
7
|
+
animalId16: string;
|
|
8
|
+
cardNumber21: string;
|
|
9
|
+
cardDidWeb: string;
|
|
10
|
+
}>;
|
|
11
|
+
/** Issues the current jurisdiction5 + animal15 + check1 card from server-held HMAC custody. */
|
|
12
|
+
export declare function issueVetChainAnimalCard(input: Readonly<{
|
|
13
|
+
host: string;
|
|
14
|
+
providerCode9: string;
|
|
15
|
+
ncbiTaxonomyId: string;
|
|
16
|
+
subjectUuid: string;
|
|
17
|
+
hmacKeyBytes: Uint8Array;
|
|
18
|
+
collisionCounter?: number;
|
|
19
|
+
}>): VetChainIssuedAnimalCard;
|
|
20
|
+
export declare function normalizeVetChainNcbiTaxonomyId(value: string): string;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { hmac } from '@noble/hashes/hmac.js';
|
|
2
|
+
import { sha256 } from '@noble/hashes/sha2.js';
|
|
3
|
+
import { utf8ToBytes } from '@noble/hashes/utils.js';
|
|
4
|
+
import { parseVetChainAnimalCardDid } from 'vet-data-utils-ts';
|
|
5
|
+
import { buildAnimalIdentifier16, buildSpeciesId7 } from './animal-identity.js';
|
|
6
|
+
/** Issues the current jurisdiction5 + animal15 + check1 card from server-held HMAC custody. */
|
|
7
|
+
export function issueVetChainAnimalCard(input) {
|
|
8
|
+
const providerCode9 = digits(input.providerCode9, 9, 'vet_card_provider_code_invalid');
|
|
9
|
+
const jurisdictionCode5 = providerCode9.slice(0, 5);
|
|
10
|
+
const ncbiTaxonomyId = taxonomy(input.ncbiTaxonomyId);
|
|
11
|
+
const speciesCode5 = ncbiTaxonomyId.padStart(5, '0');
|
|
12
|
+
const subjectUuid = String(input.subjectUuid || '').trim().toLowerCase().replace(/[{}]/g, '');
|
|
13
|
+
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(subjectUuid)) {
|
|
14
|
+
throw new TypeError('vet_card_subject_uuid_invalid');
|
|
15
|
+
}
|
|
16
|
+
if (!(input.hmacKeyBytes instanceof Uint8Array) || input.hmacKeyBytes.length < 32) {
|
|
17
|
+
throw new TypeError('vet_card_hmac_key_invalid');
|
|
18
|
+
}
|
|
19
|
+
const collisionCounter = input.collisionCounter ?? 0;
|
|
20
|
+
if (!Number.isInteger(collisionCounter) || collisionCounter < 0)
|
|
21
|
+
throw new TypeError('vet_card_collision_counter_invalid');
|
|
22
|
+
const digest = hmac(sha256, input.hmacKeyBytes, utf8ToBytes([
|
|
23
|
+
'vetchain-animal-card:v3', providerCode9, ncbiTaxonomyId, subjectUuid, `cc:${collisionCounter}`,
|
|
24
|
+
].join(':')));
|
|
25
|
+
const animalNumericId15 = `000${reduceDigestToDecimalDigits(digest, 12)}`;
|
|
26
|
+
const animalId16 = buildAnimalIdentifier16({
|
|
27
|
+
speciesId7: buildSpeciesId7(ncbiTaxonomyId),
|
|
28
|
+
jurisdictionCode5,
|
|
29
|
+
animalNumericId15,
|
|
30
|
+
});
|
|
31
|
+
const cardNumber21 = `${jurisdictionCode5}${animalId16}`;
|
|
32
|
+
const host = String(input.host || '').trim().toLowerCase();
|
|
33
|
+
if (!/^[a-z0-9.-]+(?::\d+)?$/.test(host))
|
|
34
|
+
throw new TypeError('vet_card_host_invalid');
|
|
35
|
+
const cardDidWeb = `did:web:${host.replace(':', '%3A')}:card:vetchain:${jurisdictionCode5}:${animalId16}`;
|
|
36
|
+
parseVetChainAnimalCardDid(cardDidWeb);
|
|
37
|
+
return Object.freeze({ providerCode9, jurisdictionCode5, ncbiTaxonomyId, speciesCode5, animalNumericId15, animalId16, cardNumber21, cardDidWeb });
|
|
38
|
+
}
|
|
39
|
+
export function normalizeVetChainNcbiTaxonomyId(value) {
|
|
40
|
+
return taxonomy(value);
|
|
41
|
+
}
|
|
42
|
+
function taxonomy(value) {
|
|
43
|
+
const normalized = String(value || '').trim();
|
|
44
|
+
if (!/^[1-9]\d{0,4}$/.test(normalized) || normalized === '9606')
|
|
45
|
+
throw new TypeError('vet_card_taxonomy_invalid');
|
|
46
|
+
return normalized;
|
|
47
|
+
}
|
|
48
|
+
function digits(value, width, error) {
|
|
49
|
+
const normalized = String(value || '').trim().replace(/[\s-]/g, '');
|
|
50
|
+
if (!new RegExp(`^\\d{${width}}$`).test(normalized))
|
|
51
|
+
throw new TypeError(error);
|
|
52
|
+
return normalized;
|
|
53
|
+
}
|
|
54
|
+
function reduceDigestToDecimalDigits(bytes, width) {
|
|
55
|
+
let value = 0n;
|
|
56
|
+
for (const byte of bytes)
|
|
57
|
+
value = (value << 8n) + BigInt(byte);
|
|
58
|
+
return (value % (10n ** BigInt(width))).toString().padStart(width, '0');
|
|
59
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { type VeterinaryDigitalTwinFormat } from 'vet-data-utils-ts';
|
|
2
|
+
export type VetGatewayRouteContext = Readonly<{
|
|
3
|
+
tenantId: string;
|
|
4
|
+
jurisdiction: string;
|
|
5
|
+
sector: 'animal-care' | 'animal-research';
|
|
6
|
+
}>;
|
|
7
|
+
export type VeterinaryBreakGlassSmartRequest = Readonly<{
|
|
8
|
+
thid: string;
|
|
9
|
+
iss: string;
|
|
10
|
+
aud: string;
|
|
11
|
+
body: Readonly<{
|
|
12
|
+
sub: string;
|
|
13
|
+
client_id: string;
|
|
14
|
+
client_assertion: string;
|
|
15
|
+
client_assertion_type: 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer';
|
|
16
|
+
vp_token: string;
|
|
17
|
+
acr_values: 'urn:antifraud:acr:openid4vp:employee';
|
|
18
|
+
purpose: 'EMERGENCY';
|
|
19
|
+
scope: string;
|
|
20
|
+
expires_in: number;
|
|
21
|
+
break_glass: Readonly<{
|
|
22
|
+
incidentId: string;
|
|
23
|
+
subjectKind: 'animal';
|
|
24
|
+
reasonCode: 'animal-emergency';
|
|
25
|
+
justification: string;
|
|
26
|
+
}>;
|
|
27
|
+
}>;
|
|
28
|
+
}>;
|
|
29
|
+
export type VeterinaryDigitalTwinSearchRequest = Readonly<{
|
|
30
|
+
format: VeterinaryDigitalTwinFormat;
|
|
31
|
+
resourceType: 'Composition';
|
|
32
|
+
payload: Readonly<{
|
|
33
|
+
thid: string;
|
|
34
|
+
body: Readonly<Record<string, unknown>>;
|
|
35
|
+
}>;
|
|
36
|
+
}>;
|
|
37
|
+
export declare function buildVetGatewayRoutePrefix(context: VetGatewayRouteContext): string;
|
|
38
|
+
export declare function buildVeterinaryBreakGlassSmartRequest(input: Readonly<{
|
|
39
|
+
thid: string;
|
|
40
|
+
actorDid: string;
|
|
41
|
+
subjectDid: string;
|
|
42
|
+
requestedSections: readonly string[];
|
|
43
|
+
incidentId: string;
|
|
44
|
+
justification: string;
|
|
45
|
+
clientId: string;
|
|
46
|
+
clientAssertion: string;
|
|
47
|
+
vpToken: string;
|
|
48
|
+
audience: string;
|
|
49
|
+
}>): VeterinaryBreakGlassSmartRequest;
|
|
50
|
+
export declare function buildVeterinaryDigitalTwinSearchRequest(input: Readonly<{
|
|
51
|
+
thid: string;
|
|
52
|
+
format?: VeterinaryDigitalTwinFormat;
|
|
53
|
+
search: unknown;
|
|
54
|
+
}>): VeterinaryDigitalTwinSearchRequest;
|
|
55
|
+
export declare function buildExactAnimalResolution(input: Readonly<{
|
|
56
|
+
subjectDid: string;
|
|
57
|
+
}>): Readonly<{
|
|
58
|
+
subjectDid: string;
|
|
59
|
+
cardNumber21: string;
|
|
60
|
+
}>;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { VeterinaryBreakGlassMaxLifetimeSeconds, VeterinaryDigitalTwinFormats, parseVetChainAnimalCardDid, parseVeterinaryBreakGlassRequestData, parseVeterinaryDigitalTwinSearch, } from 'vet-data-utils-ts';
|
|
2
|
+
export function buildVetGatewayRoutePrefix(context) {
|
|
3
|
+
const tenantId = bounded(context.tenantId, 'tenantId');
|
|
4
|
+
const jurisdiction = bounded(context.jurisdiction, 'jurisdiction').toUpperCase();
|
|
5
|
+
if (!/^[A-Z]{2}(?:-[A-Z0-9]{1,3})?$/.test(jurisdiction))
|
|
6
|
+
throw new TypeError('vet_gateway_jurisdiction_invalid');
|
|
7
|
+
return `/${tenantId}/cds-${jurisdiction}/v1/${context.sector}`;
|
|
8
|
+
}
|
|
9
|
+
export function buildVeterinaryBreakGlassSmartRequest(input) {
|
|
10
|
+
const emergency = parseVeterinaryBreakGlassRequestData(input);
|
|
11
|
+
const actorDid = did(input.actorDid, 'vet_break_glass_actor_invalid');
|
|
12
|
+
const section = emergency.requestedSections.join(',');
|
|
13
|
+
const scope = `organization/Composition.rs?subject=${encodeURIComponent(emergency.subjectDid)}§ion=${encodeURIComponent(section)}`;
|
|
14
|
+
return Object.freeze({
|
|
15
|
+
thid: bounded(input.thid, 'thid'),
|
|
16
|
+
iss: actorDid,
|
|
17
|
+
aud: did(input.audience, 'vet_break_glass_audience_invalid'),
|
|
18
|
+
body: {
|
|
19
|
+
sub: actorDid,
|
|
20
|
+
client_id: did(input.clientId, 'vet_break_glass_client_invalid'),
|
|
21
|
+
client_assertion: bounded(input.clientAssertion, 'clientAssertion'),
|
|
22
|
+
client_assertion_type: 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',
|
|
23
|
+
vp_token: bounded(input.vpToken, 'vpToken'),
|
|
24
|
+
acr_values: 'urn:antifraud:acr:openid4vp:employee',
|
|
25
|
+
purpose: 'EMERGENCY',
|
|
26
|
+
scope,
|
|
27
|
+
expires_in: VeterinaryBreakGlassMaxLifetimeSeconds,
|
|
28
|
+
break_glass: {
|
|
29
|
+
incidentId: emergency.incidentId,
|
|
30
|
+
subjectKind: emergency.subjectKind,
|
|
31
|
+
reasonCode: emergency.reasonCode,
|
|
32
|
+
justification: emergency.justification,
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
export function buildVeterinaryDigitalTwinSearchRequest(input) {
|
|
38
|
+
const search = parseVeterinaryDigitalTwinSearch(input.search);
|
|
39
|
+
const format = input.format || VeterinaryDigitalTwinFormats.R4;
|
|
40
|
+
const parameter = [
|
|
41
|
+
{ name: 'section', valueString: search.section },
|
|
42
|
+
{ name: search.claim, valueString: search.value },
|
|
43
|
+
];
|
|
44
|
+
return Object.freeze({
|
|
45
|
+
format,
|
|
46
|
+
// GW stores the pseudonymous twin as a Composition. `search.resourceType`
|
|
47
|
+
// names the nested clinical resource whose claim is being filtered; it is
|
|
48
|
+
// never the asynchronous route resource type.
|
|
49
|
+
resourceType: 'Composition',
|
|
50
|
+
payload: {
|
|
51
|
+
thid: bounded(input.thid, 'thid'),
|
|
52
|
+
body: format === VeterinaryDigitalTwinFormats.Api
|
|
53
|
+
? { data: [{ type: 'Composition-search-request-v1.0', resource: { resourceType: 'Parameters', parameter } }] }
|
|
54
|
+
: { resourceType: 'Parameters', parameter },
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
export function buildExactAnimalResolution(input) {
|
|
59
|
+
const card = parseVetChainAnimalCardDid(input.subjectDid);
|
|
60
|
+
return Object.freeze({ subjectDid: card.did, cardNumber21: card.cardNumber21 });
|
|
61
|
+
}
|
|
62
|
+
function bounded(value, name) {
|
|
63
|
+
const normalized = String(value || '').trim();
|
|
64
|
+
if (!normalized || normalized.length > 4096)
|
|
65
|
+
throw new TypeError(`vet_gateway_${name}_invalid`);
|
|
66
|
+
return normalized;
|
|
67
|
+
}
|
|
68
|
+
function did(value, error) {
|
|
69
|
+
const normalized = bounded(value, 'did');
|
|
70
|
+
if (!normalized.startsWith('did:'))
|
|
71
|
+
throw new TypeError(error);
|
|
72
|
+
return normalized;
|
|
73
|
+
}
|
package/dist/index.d.ts
ADDED