vet-sdk-core-ts 0.4.37 → 0.4.39

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,24 @@
1
1
  # VetChain Core SDK
2
2
 
3
+ ## Animal Individual Organization identity
4
+
5
+ An animal is the first member of its own Individual Organization, so
6
+ `Organization.member.role` is always the HL7 code `ONESELF`. The authenticated
7
+ human that creates it is `Organization.owner`, the controller by default;
8
+ `RESPRSN` must never be copied into the animal member.
9
+
10
+ Name, birth date, exact NCBI species and optional breed are animal
11
+ demographics. Breed remains separate descriptive text. Controller-authored
12
+ demographics remain editable until a veterinarian attests them; attestation
13
+ does not rewrite the original author, and later correction creates a new
14
+ veterinarian-authored version.
15
+
16
+ Use `buildVetChainAnimalDraftClaims(...)` while the name, exact NCBI species or
17
+ public Card is missing. Only a draft for which
18
+ `readVetChainAnimalOnboardingReadiness(...)` returns `ready: true` may be
19
+ activated and issued a Card. See
20
+ [`docs/101-ANIMAL-INDIVIDUAL-ONBOARDING.md`](docs/101-ANIMAL-INDIVIDUAL-ONBOARDING.md).
21
+
3
22
  `vet-sdk-core-ts/place-service-directory` is the VetChain-only bundle boundary
4
23
  for claims-first Schema.org `Place` and `Service` resources. It builds GW VET
5
24
  batch/search envelopes and joins authorized directory search responses for the
@@ -1,3 +1,7 @@
1
+ /** Canonical member relationship for the animal SELF subject. */
2
+ export declare const VetChainAnimalMemberRoles: Readonly<{
3
+ readonly Self: "ONESELF";
4
+ }>;
1
5
  export type VetChainAnimalOnboardingInput = Readonly<{
2
6
  subjectId: string;
3
7
  cardDidWeb: string;
@@ -7,10 +11,29 @@ export type VetChainAnimalOnboardingInput = Readonly<{
7
11
  birthYear?: number;
8
12
  gender?: 'female' | 'male' | 'other' | 'unknown';
9
13
  ncbiTaxonomyId: string;
14
+ breedLabel?: string;
10
15
  controllerEmail?: string;
11
16
  controllerTelephone?: string;
12
17
  sector?: string;
13
18
  }>;
19
+ export type VetChainAnimalDraftInput = Readonly<{
20
+ subjectId: string;
21
+ cardDidWeb?: string;
22
+ alternateName?: string;
23
+ legalName?: string;
24
+ birthDate?: string;
25
+ birthYear?: number;
26
+ gender?: 'female' | 'male' | 'other' | 'unknown';
27
+ ncbiTaxonomyId?: string;
28
+ breedLabel?: string;
29
+ controllerEmail?: string;
30
+ controllerTelephone?: string;
31
+ sector?: string;
32
+ }>;
33
+ export type VetChainAnimalOnboardingReadiness = Readonly<{
34
+ ready: boolean;
35
+ missing: ReadonlyArray<'alternateName' | 'species' | 'card'>;
36
+ }>;
14
37
  /** Canonical schema.org claim names for the VetChain animal registration. */
15
38
  export declare const VetChainAnimalOnboardingClaimNames: Readonly<{
16
39
  readonly identifierValue: "org.schema.Organization.identifier.value";
@@ -20,6 +43,7 @@ export declare const VetChainAnimalOnboardingClaimNames: Readonly<{
20
43
  readonly sameAs: "org.schema.Organization.sameAs";
21
44
  readonly memberName: "org.schema.Organization.member.name";
22
45
  readonly memberAdditionalType: "org.schema.Organization.member.additionalType";
46
+ readonly memberBreed: "org.schema.Organization.member.breed";
23
47
  readonly memberBirthDate: "org.schema.Organization.member.birthDate";
24
48
  readonly memberGender: "org.schema.Organization.member.gender";
25
49
  readonly memberRole: "org.schema.Organization.member.role";
@@ -27,9 +51,24 @@ export declare const VetChainAnimalOnboardingClaimNames: Readonly<{
27
51
  readonly ownerTelephone: "org.schema.Organization.owner.telephone";
28
52
  readonly serviceCategory: "org.schema.Service.category";
29
53
  }>;
54
+ /**
55
+ * Creates the claims-first administrative draft for one animal.
56
+ *
57
+ * A draft always identifies its private subject and the creating controller,
58
+ * but name, NCBI species and public Card DID may remain absent. The animal is
59
+ * still its own `ONESELF` member; `Organization.owner` is the controller.
60
+ */
61
+ export declare function buildVetChainAnimalDraftClaims(input: VetChainAnimalDraftInput): Readonly<Record<string, string>>;
62
+ /** Reports whether an animal draft can be activated and issued a public Card. */
63
+ export declare function readVetChainAnimalOnboardingReadiness(claims: Readonly<Record<string, unknown>>): VetChainAnimalOnboardingReadiness;
30
64
  /**
31
65
  * Projects one controller-authorized animal to the GW Organization envelope.
32
66
  *
67
+ * The animal is the envelope's first member and therefore has the HL7
68
+ * `ONESELF` relationship. The authenticated human that creates it is kept in
69
+ * `Organization.owner`; controller authority such as `RESPRSN` is never
70
+ * copied into `Organization.member.role`.
71
+ *
33
72
  * Species remains separate governed metadata. It validates the Damm digit with
34
73
  * jurisdiction and animalNumericId15 but is never decoded from cardNumber21.
35
74
  */
@@ -1,6 +1,8 @@
1
1
  import { parseVetChainAnimalCardDid } from 'vet-data-utils-ts';
2
2
  import { buildAnimalIdentifier16, buildSpeciesId7 } from './animal-identity.js';
3
3
  import { normalizeVetChainNcbiTaxonomyId } from './card-issuance.js';
4
+ /** Canonical member relationship for the animal SELF subject. */
5
+ export const VetChainAnimalMemberRoles = Object.freeze({ Self: 'ONESELF' });
4
6
  /** Canonical schema.org claim names for the VetChain animal registration. */
5
7
  export const VetChainAnimalOnboardingClaimNames = Object.freeze({
6
8
  identifierValue: 'org.schema.Organization.identifier.value',
@@ -10,6 +12,7 @@ export const VetChainAnimalOnboardingClaimNames = Object.freeze({
10
12
  sameAs: 'org.schema.Organization.sameAs',
11
13
  memberName: 'org.schema.Organization.member.name',
12
14
  memberAdditionalType: 'org.schema.Organization.member.additionalType',
15
+ memberBreed: 'org.schema.Organization.member.breed',
13
16
  memberBirthDate: 'org.schema.Organization.member.birthDate',
14
17
  memberGender: 'org.schema.Organization.member.gender',
15
18
  memberRole: 'org.schema.Organization.member.role',
@@ -18,26 +21,26 @@ export const VetChainAnimalOnboardingClaimNames = Object.freeze({
18
21
  serviceCategory: 'org.schema.Service.category',
19
22
  });
20
23
  /**
21
- * Projects one controller-authorized animal to the GW Organization envelope.
24
+ * Creates the claims-first administrative draft for one animal.
22
25
  *
23
- * Species remains separate governed metadata. It validates the Damm digit with
24
- * jurisdiction and animalNumericId15 but is never decoded from cardNumber21.
26
+ * A draft always identifies its private subject and the creating controller,
27
+ * but name, NCBI species and public Card DID may remain absent. The animal is
28
+ * still its own `ONESELF` member; `Organization.owner` is the controller.
25
29
  */
26
- export function buildVetChainAnimalOnboardingClaims(input) {
30
+ export function buildVetChainAnimalDraftClaims(input) {
27
31
  const subjectId = required(input.subjectId, 'vet_animal_subject_id_invalid');
28
- const alternateName = required(input.alternateName, 'vet_animal_name_invalid');
29
- const card = parseVetChainAnimalCardDid(input.cardDidWeb);
30
- const ncbiTaxonomyId = normalizeVetChainNcbiTaxonomyId(input.ncbiTaxonomyId);
31
- const expectedAnimalId16 = buildAnimalIdentifier16({
32
- speciesId7: buildSpeciesId7(ncbiTaxonomyId),
33
- jurisdictionCode5: card.jurisdictionCode5,
34
- animalNumericId15: card.animalNumericId15,
35
- });
36
- if (expectedAnimalId16 !== card.animalId16) {
37
- throw new TypeError('vet_animal_card_species_checksum_invalid');
38
- }
39
- const birthDate = String(input.birthDate || '').trim()
40
- || (Number.isInteger(input.birthYear) ? String(input.birthYear) : '');
32
+ const alternateName = String(input.alternateName || '').trim();
33
+ const ncbiTaxonomyId = String(input.ncbiTaxonomyId || '').trim()
34
+ ? normalizeVetChainNcbiTaxonomyId(String(input.ncbiTaxonomyId))
35
+ : '';
36
+ const cardDidWeb = String(input.cardDidWeb || '').trim();
37
+ if (cardDidWeb && !ncbiTaxonomyId)
38
+ throw new TypeError('vet_animal_card_species_required');
39
+ if (cardDidWeb)
40
+ assertCardSpecies(cardDidWeb, ncbiTaxonomyId);
41
+ const birthDate = normalizePartialBirthDate(String(input.birthDate || '').trim()
42
+ || (Number.isInteger(input.birthYear) ? String(input.birthYear) : ''));
43
+ const breedLabel = String(input.breedLabel || '').trim();
41
44
  const legalName = String(input.legalName || '').trim();
42
45
  const gender = input.gender && input.gender !== 'unknown' ? input.gender : '';
43
46
  const claim = VetChainAnimalOnboardingClaimNames;
@@ -45,12 +48,17 @@ export function buildVetChainAnimalOnboardingClaims(input) {
45
48
  '@context': 'org.schema',
46
49
  [claim.identifierValue]: subjectId,
47
50
  [claim.additionalType]: 'animal',
48
- [claim.alternateName]: alternateName,
51
+ ...(alternateName ? {
52
+ [claim.alternateName]: alternateName,
53
+ [claim.memberName]: alternateName,
54
+ } : {}),
49
55
  ...(legalName ? { [claim.legalName]: legalName } : {}),
50
- [claim.sameAs]: card.did,
51
- [claim.memberName]: alternateName,
52
- [claim.memberAdditionalType]: `http://purl.obolibrary.org/obo/NCBITaxon_${ncbiTaxonomyId}`,
53
- [claim.memberRole]: 'RESPRSN',
56
+ ...(cardDidWeb ? { [claim.sameAs]: cardDidWeb } : {}),
57
+ ...(ncbiTaxonomyId ? {
58
+ [claim.memberAdditionalType]: `http://purl.obolibrary.org/obo/NCBITaxon_${ncbiTaxonomyId}`,
59
+ } : {}),
60
+ ...(breedLabel ? { [claim.memberBreed]: breedLabel } : {}),
61
+ [claim.memberRole]: VetChainAnimalMemberRoles.Self,
54
62
  ...(birthDate ? { [claim.memberBirthDate]: birthDate } : {}),
55
63
  ...(gender ? { [claim.memberGender]: gender } : {}),
56
64
  [claim.ownerEmail]: String(input.controllerEmail || '').trim(),
@@ -58,6 +66,70 @@ export function buildVetChainAnimalOnboardingClaims(input) {
58
66
  [claim.serviceCategory]: String(input.sector || '').trim() || 'animal-care',
59
67
  });
60
68
  }
69
+ /** Normalizes ISO 8601 reduced-precision animal birth dates (year, year-month or full date). */
70
+ function normalizePartialBirthDate(value) {
71
+ if (!value)
72
+ return '';
73
+ const match = value.match(/^(\d{4})(?:-(\d{2})(?:-(\d{2}))?)?$/);
74
+ if (!match)
75
+ throw new TypeError('vet_animal_birth_date_invalid');
76
+ const year = Number(match[1]);
77
+ const month = match[2] ? Number(match[2]) : undefined;
78
+ const day = match[3] ? Number(match[3]) : undefined;
79
+ if (year < 1000 || year > 9999 || (month !== undefined && (month < 1 || month > 12))) {
80
+ throw new TypeError('vet_animal_birth_date_invalid');
81
+ }
82
+ if (day !== undefined) {
83
+ const lastDay = new Date(Date.UTC(year, month, 0)).getUTCDate();
84
+ if (day < 1 || day > lastDay)
85
+ throw new TypeError('vet_animal_birth_date_invalid');
86
+ }
87
+ return value;
88
+ }
89
+ /** Reports whether an animal draft can be activated and issued a public Card. */
90
+ export function readVetChainAnimalOnboardingReadiness(claims) {
91
+ const claim = VetChainAnimalOnboardingClaimNames;
92
+ const missing = [];
93
+ if (!String(claims[claim.alternateName] || '').trim())
94
+ missing.push('alternateName');
95
+ if (!/^http:\/\/purl\.obolibrary\.org\/obo\/NCBITaxon_[1-9]\d*$/.test(String(claims[claim.memberAdditionalType] || '').trim())) {
96
+ missing.push('species');
97
+ }
98
+ if (!String(claims[claim.sameAs] || '').trim())
99
+ missing.push('card');
100
+ return Object.freeze({ ready: missing.length === 0, missing: Object.freeze(missing) });
101
+ }
102
+ /**
103
+ * Projects one controller-authorized animal to the GW Organization envelope.
104
+ *
105
+ * The animal is the envelope's first member and therefore has the HL7
106
+ * `ONESELF` relationship. The authenticated human that creates it is kept in
107
+ * `Organization.owner`; controller authority such as `RESPRSN` is never
108
+ * copied into `Organization.member.role`.
109
+ *
110
+ * Species remains separate governed metadata. It validates the Damm digit with
111
+ * jurisdiction and animalNumericId15 but is never decoded from cardNumber21.
112
+ */
113
+ export function buildVetChainAnimalOnboardingClaims(input) {
114
+ const alternateName = required(input.alternateName, 'vet_animal_name_invalid');
115
+ const ncbiTaxonomyId = normalizeVetChainNcbiTaxonomyId(input.ncbiTaxonomyId);
116
+ const claims = buildVetChainAnimalDraftClaims({ ...input, alternateName, ncbiTaxonomyId });
117
+ const readiness = readVetChainAnimalOnboardingReadiness(claims);
118
+ if (!readiness.ready)
119
+ throw new TypeError(`vet_animal_onboarding_incomplete:${readiness.missing.join(',')}`);
120
+ return claims;
121
+ }
122
+ function assertCardSpecies(cardDidWeb, ncbiTaxonomyId) {
123
+ const card = parseVetChainAnimalCardDid(cardDidWeb);
124
+ const expectedAnimalId16 = buildAnimalIdentifier16({
125
+ speciesId7: buildSpeciesId7(ncbiTaxonomyId),
126
+ jurisdictionCode5: card.jurisdictionCode5,
127
+ animalNumericId15: card.animalNumericId15,
128
+ });
129
+ if (expectedAnimalId16 !== card.animalId16) {
130
+ throw new TypeError('vet_animal_card_species_checksum_invalid');
131
+ }
132
+ }
61
133
  function required(value, error) {
62
134
  const normalized = String(value || '').trim();
63
135
  if (!normalized)
@@ -0,0 +1,54 @@
1
+ # Animal Individual Organization onboarding 101
2
+
3
+ The animal and the human controller are two different identities:
4
+
5
+ - `Organization.member` is the indexed animal itself. Its relationship is
6
+ `ONESELF`.
7
+ - `Organization.owner` is the authenticated human that creates and controls
8
+ the Individual Organization by default. Controller authority may be
9
+ represented as `RESPRSN`, but that value never becomes the animal's member
10
+ role.
11
+
12
+ ```ts
13
+ import {
14
+ buildVetChainAnimalDraftClaims,
15
+ readVetChainAnimalOnboardingReadiness,
16
+ } from 'vet-sdk-core-ts/animal-onboarding'
17
+
18
+ // Step 1. The controller starts a private draft using a verified contact.
19
+ const draft = buildVetChainAnimalDraftClaims({
20
+ subjectId: crypto.randomUUID(),
21
+ controllerTelephone: '+12365162385',
22
+ })
23
+
24
+ // Optional breed text stays separate from the governed NCBI species. The
25
+ // birth date may safely retain reduced year-month precision.
26
+ buildVetChainAnimalDraftClaims({
27
+ subjectId: crypto.randomUUID(),
28
+ controllerTelephone: '+12365162385',
29
+ ncbiTaxonomyId: '9615',
30
+ breedLabel: 'Beagle',
31
+ birthDate: '2022-01',
32
+ })
33
+
34
+ // Step 2. The draft has member=SELF/ONESELF and owner=controller.
35
+ draft['org.schema.Organization.member.role'] // ONESELF
36
+ draft['org.schema.Organization.owner.telephone'] // verified controller
37
+
38
+ // Step 3. Do not issue or display a Card while required data is missing.
39
+ readVetChainAnimalOnboardingReadiness(draft)
40
+ // { ready: false, missing: ['alternateName', 'species', 'card'] }
41
+ ```
42
+
43
+ The BFF later rebuilds the same subject draft with its name and exact NCBI
44
+ taxonomy, issues the public Card, and resubmits it under the same private
45
+ `subjectId`. GW VET alone performs the `pending -> active` transition. A draft
46
+ does not need a clinical `Composition`; the active clinical index is a
47
+ separate document lifecycle.
48
+
49
+ The controller is the author of demographics supplied during this draft. A
50
+ veterinarian later verifies them as attester. That verification locks the
51
+ controller-facing edit path; a correction is a new version authored and
52
+ attested by an authorized veterinarian, never a rewrite of the earlier author.
53
+ Veterinary identity evidence publishes only protected proof and lifecycle
54
+ metadata to the ledger, not raw identity documents or private demographics.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vet-sdk-core-ts",
3
- "version": "0.4.37",
3
+ "version": "0.4.39",
4
4
  "description": "Browser-safe VetChain core contracts and governed animal species identifiers",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Connecting Solution & Applications Ltd",