vet-data-utils-ts 0.3.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 ADDED
@@ -0,0 +1,14 @@
1
+ # vet-data-utils-ts
2
+
3
+ Browser-safe, transport-neutral VetChain data contracts shared by GW VET and
4
+ `vet-sdk-core-ts`. The package owns governed values and validation only; GW VET
5
+ owns authorization and the SDK owns request construction.
6
+
7
+ Current surfaces cover the canonical animal-card DID, veterinary summary
8
+ sections, animal-only emergency data, pseudonymous DigitalTwin search input and
9
+ veterinary assistant intents.
10
+
11
+ The catalogue retains longitudinal sections that also apply to animal care,
12
+ with animal-facing labels where needed. Human advance directives
13
+ (`LOINC|42348-3`) are deliberately excluded because they express the patient's
14
+ own legal/autonomy decisions, not controller preferences.
@@ -0,0 +1,14 @@
1
+ export type VetChainAnimalCardIdentity = Readonly<{
2
+ did: string;
3
+ host: string;
4
+ jurisdictionCode5: string;
5
+ animalNumericId15: string;
6
+ checkDigit1: string;
7
+ animalId16: string;
8
+ cardNumber21: string;
9
+ }>;
10
+ /** Parses only the current VetChain card DID; legacy UHC animal aliases are rejected. */
11
+ export declare function parseVetChainAnimalCardDid(value: unknown): VetChainAnimalCardIdentity;
12
+ export declare function isVetChainAnimalCardDid(value: unknown): value is string;
13
+ /** Returns the Damm digit that makes the complete decimal card validate to zero. */
14
+ export declare function computeVetChainDammCheckDigit(value: string): string;
@@ -0,0 +1,44 @@
1
+ const CARD_DID = /^did:web:([^:]+):card:vetchain:(\d{5}):(\d{16})$/i;
2
+ /** Parses only the current VetChain card DID; legacy UHC animal aliases are rejected. */
3
+ export function parseVetChainAnimalCardDid(value) {
4
+ const did = String(value || '').trim();
5
+ const match = CARD_DID.exec(did);
6
+ if (!match)
7
+ throw new TypeError('vet_animal_card_did_invalid');
8
+ const [, host, jurisdictionCode5, animalId16] = match;
9
+ const cardNumber21 = `${jurisdictionCode5}${animalId16}`;
10
+ return Object.freeze({
11
+ did,
12
+ host: host.toLowerCase(),
13
+ jurisdictionCode5,
14
+ animalNumericId15: animalId16.slice(0, 15),
15
+ checkDigit1: animalId16.slice(15),
16
+ animalId16,
17
+ cardNumber21,
18
+ });
19
+ }
20
+ export function isVetChainAnimalCardDid(value) {
21
+ try {
22
+ parseVetChainAnimalCardDid(value);
23
+ return true;
24
+ }
25
+ catch {
26
+ return false;
27
+ }
28
+ }
29
+ /** Returns the Damm digit that makes the complete decimal card validate to zero. */
30
+ export function computeVetChainDammCheckDigit(value) {
31
+ if (!/^\d+$/.test(value))
32
+ throw new TypeError('vet_animal_card_damm_input_invalid');
33
+ let interim = 0;
34
+ for (const digit of value)
35
+ interim = DAMM_TABLE[interim][Number(digit)];
36
+ return String(interim);
37
+ }
38
+ const DAMM_TABLE = [
39
+ [0, 3, 1, 7, 5, 9, 8, 6, 4, 2], [7, 0, 9, 2, 1, 5, 4, 8, 6, 3],
40
+ [4, 2, 0, 6, 8, 7, 1, 3, 5, 9], [1, 7, 5, 0, 9, 8, 3, 4, 2, 6],
41
+ [6, 1, 2, 3, 0, 4, 5, 9, 7, 8], [3, 6, 7, 4, 2, 0, 9, 5, 8, 1],
42
+ [5, 8, 6, 9, 7, 2, 0, 1, 3, 4], [8, 9, 4, 5, 3, 6, 2, 0, 1, 7],
43
+ [9, 4, 3, 8, 6, 1, 7, 2, 0, 5], [2, 5, 8, 1, 4, 3, 6, 7, 9, 0],
44
+ ];
@@ -0,0 +1,7 @@
1
+ export declare const VeterinaryAssistantIntents: Readonly<{
2
+ readonly ExplainAnimalIdentity: "explain-animal-identity";
3
+ readonly ExplainPermissions: "explain-permissions";
4
+ readonly ExplainEmergencyAccess: "explain-emergency-access";
5
+ readonly ExplainDigitalTwin: "explain-digital-twin";
6
+ }>;
7
+ export type VeterinaryAssistantIntent = typeof VeterinaryAssistantIntents[keyof typeof VeterinaryAssistantIntents];
@@ -0,0 +1,6 @@
1
+ export const VeterinaryAssistantIntents = Object.freeze({
2
+ ExplainAnimalIdentity: 'explain-animal-identity',
3
+ ExplainPermissions: 'explain-permissions',
4
+ ExplainEmergencyAccess: 'explain-emergency-access',
5
+ ExplainDigitalTwin: 'explain-digital-twin',
6
+ });
@@ -0,0 +1,13 @@
1
+ export declare const VeterinaryDigitalTwinFormats: Readonly<{
2
+ readonly Api: "org.hl7.fhir.api";
3
+ readonly R4: "org.hl7.fhir.r4";
4
+ }>;
5
+ export type VeterinaryDigitalTwinFormat = typeof VeterinaryDigitalTwinFormats[keyof typeof VeterinaryDigitalTwinFormats];
6
+ export type VeterinaryDigitalTwinSearch = Readonly<{
7
+ section: string;
8
+ resourceType: string;
9
+ claim: string;
10
+ value: string;
11
+ }>;
12
+ /** Bounds one section-first pseudonymous search and rejects direct identifiers. */
13
+ export declare function parseVeterinaryDigitalTwinSearch(input: unknown): VeterinaryDigitalTwinSearch;
@@ -0,0 +1,24 @@
1
+ import { isVeterinarySummarySection } from './veterinary-sections.js';
2
+ export const VeterinaryDigitalTwinFormats = Object.freeze({ Api: 'org.hl7.fhir.api', R4: 'org.hl7.fhir.r4' });
3
+ /** Bounds one section-first pseudonymous search and rejects direct identifiers. */
4
+ export function parseVeterinaryDigitalTwinSearch(input) {
5
+ const value = (input || {});
6
+ for (const forbidden of ['name', 'email', 'telephone', 'identifier', 'subjectDid', 'cardNumber']) {
7
+ if (String(value[forbidden] || '').trim())
8
+ throw new TypeError('veterinary_digital_twin_identifying_filter_forbidden');
9
+ }
10
+ const section = String(value.section || '').trim();
11
+ const resourceType = String(value.resourceType || '').trim();
12
+ const claim = String(value.claim || '').trim();
13
+ const filterValue = String(value.value || '').trim();
14
+ if (!isVeterinarySummarySection(section))
15
+ throw new TypeError('veterinary_digital_twin_section_invalid');
16
+ if (!/^[A-Z][A-Za-z]{1,63}$/.test(resourceType))
17
+ throw new TypeError('veterinary_digital_twin_resource_invalid');
18
+ if (!new RegExp(`^${resourceType.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\.[A-Za-z0-9][A-Za-z0-9.-]{0,127}$`).test(claim)) {
19
+ throw new TypeError('veterinary_digital_twin_claim_invalid');
20
+ }
21
+ if (!filterValue || filterValue.length > 256)
22
+ throw new TypeError('veterinary_digital_twin_value_invalid');
23
+ return Object.freeze({ section, resourceType, claim, value: filterValue });
24
+ }
@@ -0,0 +1,17 @@
1
+ export declare const VeterinaryOccupationCodes: Readonly<{
2
+ Veterinarian: "2250";
3
+ }>;
4
+ export declare const VeterinaryEmergencyReasonCodes: Readonly<{
5
+ AnimalEmergency: "animal-emergency";
6
+ }>;
7
+ export declare const VeterinaryBreakGlassMaxLifetimeSeconds: number;
8
+ export type VeterinaryBreakGlassRequestData = Readonly<{
9
+ subjectDid: string;
10
+ requestedSections: readonly string[];
11
+ incidentId: string;
12
+ justification: string;
13
+ subjectKind: 'animal';
14
+ reasonCode: 'animal-emergency';
15
+ }>;
16
+ /** Normalizes transport-neutral animal emergency data; GW remains the policy authority. */
17
+ export declare function parseVeterinaryBreakGlassRequestData(input: unknown): VeterinaryBreakGlassRequestData;
@@ -0,0 +1,25 @@
1
+ import { isVetChainAnimalCardDid } from './animal-card.js';
2
+ import { isVeterinarySummarySection } from './veterinary-sections.js';
3
+ export const VeterinaryOccupationCodes = Object.freeze({ Veterinarian: '2250' });
4
+ export const VeterinaryEmergencyReasonCodes = Object.freeze({ AnimalEmergency: 'animal-emergency' });
5
+ export const VeterinaryBreakGlassMaxLifetimeSeconds = 15 * 60;
6
+ /** Normalizes transport-neutral animal emergency data; GW remains the policy authority. */
7
+ export function parseVeterinaryBreakGlassRequestData(input) {
8
+ const value = (input || {});
9
+ const subjectDid = String(value.subjectDid || '').trim();
10
+ if (!isVetChainAnimalCardDid(subjectDid))
11
+ throw new TypeError('veterinary_break_glass_animal_required');
12
+ const incidentId = String(value.incidentId || '').trim();
13
+ if (!/^[A-Za-z0-9][A-Za-z0-9._:-]{7,127}$/.test(incidentId))
14
+ throw new TypeError('veterinary_break_glass_incident_invalid');
15
+ const justification = String(value.justification || '').trim();
16
+ if (justification.length < 20 || justification.length > 500)
17
+ throw new TypeError('veterinary_break_glass_justification_invalid');
18
+ const requestedSections = [...new Set(Array.isArray(value.requestedSections)
19
+ ? value.requestedSections.map(String).map(item => item.trim()).filter(Boolean)
20
+ : [])];
21
+ if (!requestedSections.length || requestedSections.some(section => !isVeterinarySummarySection(section))) {
22
+ throw new TypeError('veterinary_break_glass_sections_invalid');
23
+ }
24
+ return Object.freeze({ subjectDid, requestedSections, incidentId, justification, subjectKind: 'animal', reasonCode: 'animal-emergency' });
25
+ }
@@ -0,0 +1,5 @@
1
+ export * from './animal-card.js';
2
+ export * from './assistant.js';
3
+ export * from './digital-twin.js';
4
+ export * from './emergency.js';
5
+ export * from './veterinary-sections.js';
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ export * from './animal-card.js';
2
+ export * from './assistant.js';
3
+ export * from './digital-twin.js';
4
+ export * from './emergency.js';
5
+ export * from './veterinary-sections.js';
@@ -0,0 +1,101 @@
1
+ export type VeterinarySummarySection = Readonly<{
2
+ system: 'http://loinc.org';
3
+ code: string;
4
+ value: `LOINC|${string}`;
5
+ titleEn: string;
6
+ }>;
7
+ /** VetChain-owned clinical section catalog. It is not an alias of human IPS. */
8
+ export declare const VeterinarySummarySections: Readonly<{
9
+ Problems: Readonly<{
10
+ system: "http://loinc.org";
11
+ code: string;
12
+ value: `LOINC|${string}`;
13
+ titleEn: string;
14
+ }>;
15
+ Allergies: Readonly<{
16
+ system: "http://loinc.org";
17
+ code: string;
18
+ value: `LOINC|${string}`;
19
+ titleEn: string;
20
+ }>;
21
+ Medications: Readonly<{
22
+ system: "http://loinc.org";
23
+ code: string;
24
+ value: `LOINC|${string}`;
25
+ titleEn: string;
26
+ }>;
27
+ Immunizations: Readonly<{
28
+ system: "http://loinc.org";
29
+ code: string;
30
+ value: `LOINC|${string}`;
31
+ titleEn: string;
32
+ }>;
33
+ Results: Readonly<{
34
+ system: "http://loinc.org";
35
+ code: string;
36
+ value: `LOINC|${string}`;
37
+ titleEn: string;
38
+ }>;
39
+ Procedures: Readonly<{
40
+ system: "http://loinc.org";
41
+ code: string;
42
+ value: `LOINC|${string}`;
43
+ titleEn: string;
44
+ }>;
45
+ Devices: Readonly<{
46
+ system: "http://loinc.org";
47
+ code: string;
48
+ value: `LOINC|${string}`;
49
+ titleEn: string;
50
+ }>;
51
+ VitalSigns: Readonly<{
52
+ system: "http://loinc.org";
53
+ code: string;
54
+ value: `LOINC|${string}`;
55
+ titleEn: string;
56
+ }>;
57
+ EnvironmentAndLifestyle: Readonly<{
58
+ system: "http://loinc.org";
59
+ code: string;
60
+ value: `LOINC|${string}`;
61
+ titleEn: string;
62
+ }>;
63
+ Alerts: Readonly<{
64
+ system: "http://loinc.org";
65
+ code: string;
66
+ value: `LOINC|${string}`;
67
+ titleEn: string;
68
+ }>;
69
+ CareGoalsAndControllerPreferences: Readonly<{
70
+ system: "http://loinc.org";
71
+ code: string;
72
+ value: `LOINC|${string}`;
73
+ titleEn: string;
74
+ }>;
75
+ FunctionalStatus: Readonly<{
76
+ system: "http://loinc.org";
77
+ code: string;
78
+ value: `LOINC|${string}`;
79
+ titleEn: string;
80
+ }>;
81
+ PastIllness: Readonly<{
82
+ system: "http://loinc.org";
83
+ code: string;
84
+ value: `LOINC|${string}`;
85
+ titleEn: string;
86
+ }>;
87
+ ReproductiveHistory: Readonly<{
88
+ system: "http://loinc.org";
89
+ code: string;
90
+ value: `LOINC|${string}`;
91
+ titleEn: string;
92
+ }>;
93
+ CarePlan: Readonly<{
94
+ system: "http://loinc.org";
95
+ code: string;
96
+ value: `LOINC|${string}`;
97
+ titleEn: string;
98
+ }>;
99
+ }>;
100
+ export declare const VeterinarySummarySectionValues: readonly `LOINC|${string}`[];
101
+ export declare function isVeterinarySummarySection(value: unknown): value is string;
@@ -0,0 +1,25 @@
1
+ function loinc(code, titleEn) {
2
+ return Object.freeze({ system: 'http://loinc.org', code, value: `LOINC|${code}`, titleEn });
3
+ }
4
+ /** VetChain-owned clinical section catalog. It is not an alias of human IPS. */
5
+ export const VeterinarySummarySections = Object.freeze({
6
+ Problems: loinc('11450-4', 'Problems and active conditions'),
7
+ Allergies: loinc('48765-2', 'Allergies and adverse reactions'),
8
+ Medications: loinc('10160-0', 'Medication history'),
9
+ Immunizations: loinc('11369-6', 'Vaccination history'),
10
+ Results: loinc('30954-2', 'Diagnostic and laboratory results'),
11
+ Procedures: loinc('47519-4', 'Procedure history'),
12
+ Devices: loinc('46264-8', 'Implants and animal devices'),
13
+ VitalSigns: loinc('8716-3', 'Vital signs'),
14
+ EnvironmentAndLifestyle: loinc('29762-2', 'Environment, lifestyle and husbandry history'),
15
+ Alerts: loinc('104605-1', 'Clinical alerts'),
16
+ CareGoalsAndControllerPreferences: loinc('81338-6', 'Care goals and controller preferences'),
17
+ FunctionalStatus: loinc('47420-5', 'Functional status'),
18
+ PastIllness: loinc('11348-0', 'History of past illness'),
19
+ ReproductiveHistory: loinc('10162-6', 'Reproductive history'),
20
+ CarePlan: loinc('18776-5', 'Veterinary care plan'),
21
+ });
22
+ export const VeterinarySummarySectionValues = Object.freeze(Object.values(VeterinarySummarySections).map(({ value }) => value));
23
+ export function isVeterinarySummarySection(value) {
24
+ return VeterinarySummarySectionValues.includes(String(value || '').trim());
25
+ }
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "vet-data-utils-ts",
3
+ "version": "0.3.0",
4
+ "description": "Browser-safe governed VetChain data contracts",
5
+ "license": "Apache-2.0",
6
+ "author": "Connecting Solution & Applications Ltd",
7
+ "type": "module",
8
+ "main": "dist/index.js",
9
+ "types": "dist/index.d.ts",
10
+ "exports": {
11
+ ".": { "types": "./dist/index.d.ts", "default": "./dist/index.js" },
12
+ "./animal-card": { "types": "./dist/animal-card.d.ts", "default": "./dist/animal-card.js" },
13
+ "./digital-twin": { "types": "./dist/digital-twin.d.ts", "default": "./dist/digital-twin.js" },
14
+ "./emergency": { "types": "./dist/emergency.d.ts", "default": "./dist/emergency.js" },
15
+ "./veterinary-sections": { "types": "./dist/veterinary-sections.d.ts", "default": "./dist/veterinary-sections.js" },
16
+ "./assistant": { "types": "./dist/assistant.d.ts", "default": "./dist/assistant.js" }
17
+ },
18
+ "files": ["dist", "README.md"],
19
+ "scripts": {
20
+ "clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"",
21
+ "build": "npm run clean && tsc -p tsconfig.json",
22
+ "typecheck": "tsc -p tsconfig.json --noEmit",
23
+ "test": "npm run build && node --test tests/*.test.mjs",
24
+ "prepare": "npm run build",
25
+ "prepublishOnly": "npm run typecheck && npm test"
26
+ },
27
+ "devDependencies": { "@types/node": "^22.0.0", "typescript": "^5.5.4" },
28
+ "engines": { "node": ">=20" }
29
+ }