vet-sdk-core-ts 0.4.6 → 0.4.7
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 +12 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/reusable-bff.d.ts +97 -0
- package/dist/reusable-bff.js +138 -0
- package/docs/101-REUSABLE_PROFESSIONAL_BFF.md +89 -0
- package/docs/ANIMAL_IDENTITY.md +76 -0
- package/docs/snippets/reusable-professional-bff.ts +42 -0
- package/package.json +8 -3
package/README.md
CHANGED
|
@@ -6,6 +6,18 @@ UHC SDK packages and must not import them.
|
|
|
6
6
|
The SDK consumes governed browser-safe values from `vet-data-utils-ts` and
|
|
7
7
|
owns gateway request construction. GW VET remains the policy authority.
|
|
8
8
|
|
|
9
|
+
## Reusable professional BFF
|
|
10
|
+
|
|
11
|
+
`ReusableProfessionalBffClient` exposes the complete business-level portal
|
|
12
|
+
journey for UHC UNID, VetChain and SOSChain adapters: post-login profile gate,
|
|
13
|
+
organization admission and review, activation, licences, employees, individual
|
|
14
|
+
creation, clinical data, secondary-use consent, Digital Twin search and
|
|
15
|
+
break-glass. Product adapters inject sectors, translations, routes and policy.
|
|
16
|
+
The client rejects GW, wallet, DCR, SMART and VP plumbing from browser payloads.
|
|
17
|
+
|
|
18
|
+
Start with [`docs/101-REUSABLE_PROFESSIONAL_BFF.md`](docs/101-REUSABLE_PROFESSIONAL_BFF.md)
|
|
19
|
+
and its commented snippet.
|
|
20
|
+
|
|
9
21
|
## Professional break-glass authorization
|
|
10
22
|
|
|
11
23
|
Node BFFs call `buildVeterinaryBreakGlassSmartAuthorization` with only the
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
export type ReusableBffProductExtension = Readonly<{
|
|
2
|
+
/** Stable adapter id used for diagnostics; never an authorization claim. */
|
|
3
|
+
productId: string;
|
|
4
|
+
/** Exact governed sectors rendered by this product. Empty means render none. */
|
|
5
|
+
allowedSectors: readonly string[];
|
|
6
|
+
baseUrl?: string;
|
|
7
|
+
routes?: Partial<ReusableProfessionalBffRoutes>;
|
|
8
|
+
fetch?: typeof globalThis.fetch;
|
|
9
|
+
}>;
|
|
10
|
+
export type ReusableProfessionalBffRoutes = Readonly<{
|
|
11
|
+
initialPin: string;
|
|
12
|
+
initialPinUnlock: string;
|
|
13
|
+
profileActivation: string;
|
|
14
|
+
employeeActivation: string;
|
|
15
|
+
applications: string;
|
|
16
|
+
applicationReview: string;
|
|
17
|
+
licenses: string;
|
|
18
|
+
employeeInvitations: string;
|
|
19
|
+
employeeLifecycle: string;
|
|
20
|
+
subject: string;
|
|
21
|
+
digitalTwinSearch: string;
|
|
22
|
+
breakGlass: string;
|
|
23
|
+
}>;
|
|
24
|
+
export declare const DefaultReusableProfessionalBffRoutes: ReusableProfessionalBffRoutes;
|
|
25
|
+
type BusinessPayload = Readonly<Record<string, unknown>>;
|
|
26
|
+
/**
|
|
27
|
+
* Business-level client for the reusable professional BFF shared by UHC UNID,
|
|
28
|
+
* VetChain and SOSChain product adapters.
|
|
29
|
+
*
|
|
30
|
+
* Callers choose business actions only. Firebase/GW tokens, wallet seeds,
|
|
31
|
+
* activation credentials, DIDComm/JWE, DCR, SMART audiences, client assertions
|
|
32
|
+
* and VPs stay inside the BFF and its high-level Node runtime. Product-specific
|
|
33
|
+
* sectors, labels, evidence and policy are injected through the adapter.
|
|
34
|
+
*/
|
|
35
|
+
export declare class ReusableProfessionalBffClient {
|
|
36
|
+
private readonly extension;
|
|
37
|
+
readonly profileGate: Readonly<{
|
|
38
|
+
status: () => Promise<unknown>;
|
|
39
|
+
create: (input: Readonly<{
|
|
40
|
+
pin: string;
|
|
41
|
+
repeatPin: string;
|
|
42
|
+
}>) => Promise<unknown>;
|
|
43
|
+
unlock: (input: Readonly<{
|
|
44
|
+
pin: string;
|
|
45
|
+
}>) => Promise<unknown>;
|
|
46
|
+
}>;
|
|
47
|
+
readonly profiles: Readonly<{
|
|
48
|
+
activate: (input: BusinessPayload) => Promise<unknown>;
|
|
49
|
+
activateEmployee: (input: BusinessPayload) => Promise<unknown>;
|
|
50
|
+
}>;
|
|
51
|
+
readonly applications: Readonly<{
|
|
52
|
+
submit: (input: unknown) => Promise<unknown>;
|
|
53
|
+
review: (input: BusinessPayload & Readonly<{
|
|
54
|
+
applicationId: string;
|
|
55
|
+
}>) => Promise<unknown>;
|
|
56
|
+
complete: (input: Readonly<{
|
|
57
|
+
applicationId: string;
|
|
58
|
+
}>) => Promise<unknown>;
|
|
59
|
+
pdf: (input: Readonly<{
|
|
60
|
+
applicationId: string;
|
|
61
|
+
}>) => Promise<unknown>;
|
|
62
|
+
credential: (input: Readonly<{
|
|
63
|
+
applicationId: string;
|
|
64
|
+
}>) => Promise<unknown>;
|
|
65
|
+
}>;
|
|
66
|
+
readonly licenses: Readonly<{
|
|
67
|
+
purchase: (input: BusinessPayload) => Promise<unknown>;
|
|
68
|
+
}>;
|
|
69
|
+
readonly employees: Readonly<{
|
|
70
|
+
invite: (input: BusinessPayload) => Promise<unknown>;
|
|
71
|
+
lifecycle: (input: BusinessPayload) => Promise<unknown>;
|
|
72
|
+
}>;
|
|
73
|
+
readonly subjects: Readonly<{
|
|
74
|
+
create: (input: BusinessPayload) => Promise<unknown>;
|
|
75
|
+
addClinicalData: (input: BusinessPayload) => Promise<unknown>;
|
|
76
|
+
deleteClinicalData: (input: BusinessPayload) => Promise<unknown>;
|
|
77
|
+
permitDigitalTwin: (input: BusinessPayload) => Promise<unknown>;
|
|
78
|
+
}>;
|
|
79
|
+
readonly digitalTwins: Readonly<{
|
|
80
|
+
search: (input: BusinessPayload) => Promise<unknown>;
|
|
81
|
+
}>;
|
|
82
|
+
readonly breakGlass: Readonly<{
|
|
83
|
+
request: (input: BusinessPayload) => Promise<unknown>;
|
|
84
|
+
}>;
|
|
85
|
+
private readonly routes;
|
|
86
|
+
private readonly fetcher;
|
|
87
|
+
private readonly baseUrl;
|
|
88
|
+
constructor(extension: ReusableBffProductExtension);
|
|
89
|
+
private subjectAction;
|
|
90
|
+
private applicationRoute;
|
|
91
|
+
private request;
|
|
92
|
+
}
|
|
93
|
+
export declare class ReusableBffError extends Error {
|
|
94
|
+
readonly status: number;
|
|
95
|
+
constructor(status: number, message: string);
|
|
96
|
+
}
|
|
97
|
+
export {};
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { parseReusableOrganizationApplication } from 'vet-data-utils-ts';
|
|
2
|
+
export const DefaultReusableProfessionalBffRoutes = Object.freeze({
|
|
3
|
+
initialPin: '/api/profiles/initial-pin',
|
|
4
|
+
initialPinUnlock: '/api/profiles/initial-pin/unlock',
|
|
5
|
+
profileActivation: '/api/profiles/activation',
|
|
6
|
+
employeeActivation: '/api/employees/activation',
|
|
7
|
+
applications: '/api/test-network/applications',
|
|
8
|
+
applicationReview: '/api/test-network/applications/review',
|
|
9
|
+
licenses: '/api/licenses',
|
|
10
|
+
employeeInvitations: '/api/employees/invitations',
|
|
11
|
+
employeeLifecycle: '/api/employees/lifecycle',
|
|
12
|
+
subject: '/api/subject',
|
|
13
|
+
digitalTwinSearch: '/api/test-twins/search',
|
|
14
|
+
breakGlass: '/api/professional/break-glass',
|
|
15
|
+
});
|
|
16
|
+
/**
|
|
17
|
+
* Business-level client for the reusable professional BFF shared by UHC UNID,
|
|
18
|
+
* VetChain and SOSChain product adapters.
|
|
19
|
+
*
|
|
20
|
+
* Callers choose business actions only. Firebase/GW tokens, wallet seeds,
|
|
21
|
+
* activation credentials, DIDComm/JWE, DCR, SMART audiences, client assertions
|
|
22
|
+
* and VPs stay inside the BFF and its high-level Node runtime. Product-specific
|
|
23
|
+
* sectors, labels, evidence and policy are injected through the adapter.
|
|
24
|
+
*/
|
|
25
|
+
export class ReusableProfessionalBffClient {
|
|
26
|
+
constructor(extension) {
|
|
27
|
+
this.extension = extension;
|
|
28
|
+
this.routes = Object.freeze({ ...DefaultReusableProfessionalBffRoutes, ...extension.routes });
|
|
29
|
+
this.fetcher = extension.fetch ?? globalThis.fetch;
|
|
30
|
+
if (typeof this.fetcher !== 'function')
|
|
31
|
+
throw new TypeError('bff_fetch_not_available');
|
|
32
|
+
this.baseUrl = String(extension.baseUrl || '').replace(/\/$/, '');
|
|
33
|
+
required(extension.productId, 'bff_product_id_required');
|
|
34
|
+
this.profileGate = Object.freeze({
|
|
35
|
+
status: () => this.request(this.routes.initialPin, 'GET'),
|
|
36
|
+
create: input => {
|
|
37
|
+
if (input.pin !== input.repeatPin)
|
|
38
|
+
return Promise.reject(new TypeError('profile_pin_confirmation_mismatch'));
|
|
39
|
+
return this.request(this.routes.initialPin, 'POST', input, true);
|
|
40
|
+
},
|
|
41
|
+
unlock: input => this.request(this.routes.initialPinUnlock, 'POST', input, true),
|
|
42
|
+
});
|
|
43
|
+
this.profiles = Object.freeze({
|
|
44
|
+
activate: input => this.request(this.routes.profileActivation, 'POST', input),
|
|
45
|
+
activateEmployee: input => this.request(this.routes.employeeActivation, 'POST', input),
|
|
46
|
+
});
|
|
47
|
+
this.applications = Object.freeze({
|
|
48
|
+
submit: async (input) => {
|
|
49
|
+
const normalized = parseReusableOrganizationApplication({
|
|
50
|
+
...asRecord(input),
|
|
51
|
+
allowedSectors: this.extension.allowedSectors,
|
|
52
|
+
});
|
|
53
|
+
return this.request(this.routes.applications, 'POST', normalized);
|
|
54
|
+
},
|
|
55
|
+
review: ({ applicationId, ...input }) => this.request(this.routes.applicationReview, 'POST', {
|
|
56
|
+
...input,
|
|
57
|
+
applicationId: required(applicationId, 'bff_application_id_required'),
|
|
58
|
+
}),
|
|
59
|
+
complete: ({ applicationId }) => this.request(this.applicationRoute(applicationId, 'complete'), 'POST', {}),
|
|
60
|
+
pdf: ({ applicationId }) => this.request(this.applicationRoute(applicationId, 'pdf'), 'GET'),
|
|
61
|
+
credential: ({ applicationId }) => this.request(this.applicationRoute(applicationId, 'credential'), 'GET'),
|
|
62
|
+
});
|
|
63
|
+
this.licenses = Object.freeze({ purchase: input => this.request(this.routes.licenses, 'POST', input) });
|
|
64
|
+
this.employees = Object.freeze({
|
|
65
|
+
invite: input => this.request(this.routes.employeeInvitations, 'POST', input),
|
|
66
|
+
lifecycle: input => this.request(this.routes.employeeLifecycle, 'POST', input),
|
|
67
|
+
});
|
|
68
|
+
this.subjects = Object.freeze({
|
|
69
|
+
create: input => this.subjectAction('create', input),
|
|
70
|
+
addClinicalData: input => this.subjectAction('add-clinical-data', input),
|
|
71
|
+
deleteClinicalData: input => this.subjectAction('delete-clinical-data', input),
|
|
72
|
+
permitDigitalTwin: input => this.subjectAction('permit-digital-twin', input),
|
|
73
|
+
});
|
|
74
|
+
this.digitalTwins = Object.freeze({ search: input => this.request(this.routes.digitalTwinSearch, 'POST', input) });
|
|
75
|
+
this.breakGlass = Object.freeze({ request: input => this.request(this.routes.breakGlass, 'POST', input) });
|
|
76
|
+
}
|
|
77
|
+
subjectAction(action, input) {
|
|
78
|
+
return this.request(this.routes.subject, 'POST', { ...input, action });
|
|
79
|
+
}
|
|
80
|
+
applicationRoute(applicationId, suffix) {
|
|
81
|
+
return `${this.routes.applications}/${encodeURIComponent(required(applicationId, 'bff_application_id_required'))}/${suffix}`;
|
|
82
|
+
}
|
|
83
|
+
async request(path, method, body, allowPin = false) {
|
|
84
|
+
if (body !== undefined)
|
|
85
|
+
assertBusinessPayload(body, allowPin);
|
|
86
|
+
const response = await this.fetcher(`${this.baseUrl}${path}`, {
|
|
87
|
+
method,
|
|
88
|
+
credentials: 'include',
|
|
89
|
+
headers: {
|
|
90
|
+
accept: 'application/json',
|
|
91
|
+
'x-product-extension': this.extension.productId,
|
|
92
|
+
...(body === undefined ? {} : { 'content-type': 'application/json' }),
|
|
93
|
+
},
|
|
94
|
+
...(body === undefined ? {} : { body: JSON.stringify(body) }),
|
|
95
|
+
});
|
|
96
|
+
const payload = response.status === 204 ? undefined : await response.json().catch(() => undefined);
|
|
97
|
+
if (!response.ok) {
|
|
98
|
+
const error = asRecord(payload).error;
|
|
99
|
+
throw new ReusableBffError(response.status, typeof error === 'string' ? error : 'bff_operation_failed');
|
|
100
|
+
}
|
|
101
|
+
return payload;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
export class ReusableBffError extends Error {
|
|
105
|
+
constructor(status, message) {
|
|
106
|
+
super(message);
|
|
107
|
+
this.status = status;
|
|
108
|
+
this.name = 'ReusableBffError';
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const forbiddenPlumbing = new Set([
|
|
112
|
+
'access_token', 'activation_code', 'client_assertion', 'client_id', 'id_token',
|
|
113
|
+
'private_key', 'privatekey', 'seed', 'vp_token', 'wallet_seed', 'walletseed',
|
|
114
|
+
]);
|
|
115
|
+
function assertBusinessPayload(value, allowPin) {
|
|
116
|
+
if (!value || typeof value !== 'object')
|
|
117
|
+
return;
|
|
118
|
+
if (Array.isArray(value)) {
|
|
119
|
+
value.forEach(item => assertBusinessPayload(item, allowPin));
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
for (const [key, nested] of Object.entries(value)) {
|
|
123
|
+
const normalized = key.toLowerCase();
|
|
124
|
+
if (forbiddenPlumbing.has(normalized) || (!allowPin && normalized.includes('pin'))) {
|
|
125
|
+
throw new TypeError('bff_internal_plumbing_forbidden');
|
|
126
|
+
}
|
|
127
|
+
assertBusinessPayload(nested, allowPin);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
function asRecord(value) {
|
|
131
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
132
|
+
}
|
|
133
|
+
function required(value, error) {
|
|
134
|
+
const normalized = String(value ?? '').trim();
|
|
135
|
+
if (!normalized)
|
|
136
|
+
throw new TypeError(error);
|
|
137
|
+
return normalized;
|
|
138
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# Reusable professional BFF: beginner guide
|
|
2
|
+
|
|
3
|
+
This is the stable business API shared by the UHC UNID, VetChain and SOSChain
|
|
4
|
+
professional portals. The common client lives in `vet-sdk-core-ts`; each portal
|
|
5
|
+
adds an adapter with its sectors, translations, routes and policy. No `gdc-*`
|
|
6
|
+
repository needs to change when a product adds or documents an extension.
|
|
7
|
+
|
|
8
|
+
## The three layers
|
|
9
|
+
|
|
10
|
+
1. The browser calls `ReusableProfessionalBffClient` with business data.
|
|
11
|
+
2. The product BFF verifies login, profile ownership, PIN/passkey grants and
|
|
12
|
+
policy, then uses its server-only high-level runtime.
|
|
13
|
+
3. GW validates signed identity, consent and authorization and persists the
|
|
14
|
+
authoritative result.
|
|
15
|
+
|
|
16
|
+
The browser never builds GW paths, DCR requests, DIDComm/JWE envelopes, SMART
|
|
17
|
+
audiences, `client_assertion`, `vp_token`, activation credentials or wallet
|
|
18
|
+
keys. The reusable client rejects those fields.
|
|
19
|
+
|
|
20
|
+
## Complete common journey
|
|
21
|
+
|
|
22
|
+
| Stage | SDK operation | Product extension |
|
|
23
|
+
|---|---|---|
|
|
24
|
+
| Verified login | portal identity provider | issuer, translations and mail |
|
|
25
|
+
| Profile gate before navigation | `profileGate.status/create/unlock` | PIN/passkey UI |
|
|
26
|
+
| Organization application | `applications.submit` | allowed sectors and localized labels |
|
|
27
|
+
| Human review | `applications.review` | reviewer policy and optional evidence |
|
|
28
|
+
| Evidence document | `applications.pdf/credential` | PDF template and signing policy |
|
|
29
|
+
| Controller activation | `applications.complete`, `profiles.activate` | delivery channel and host route |
|
|
30
|
+
| Seats and employees | `licenses.purchase`, `employees.invite/lifecycle` | roles and payment adapter |
|
|
31
|
+
| Employee activation | `profiles.activateEmployee` | selected approved role |
|
|
32
|
+
| Individual | `subjects.create` | person demographics in UHC/SOSChain; animal demographics in VetChain |
|
|
33
|
+
| Clinical data | `subjects.addClinicalData/deleteClinicalData` | permitted FHIR resources |
|
|
34
|
+
| Secondary use | `subjects.permitDigitalTwin` | purpose/consent policy |
|
|
35
|
+
| Digital Twin | `digitalTwins.search` | governed search profile |
|
|
36
|
+
| Emergency access | `breakGlass.request` | disabled by default; human/animal policy differs |
|
|
37
|
+
|
|
38
|
+
## Organization application contract
|
|
39
|
+
|
|
40
|
+
- The product supplies the sector allowlist. The form does not hardcode
|
|
41
|
+
“animal”, “healthcare” or any other product label. An empty allowlist renders
|
|
42
|
+
no sector choice and fails closed.
|
|
43
|
+
- Country starts unselected and uses ISO 3166-1 alpha-2. A regional identifier
|
|
44
|
+
uses an ISO 3166-2 subdivision belonging to that country, for example
|
|
45
|
+
`CA-BC` or `ES-MD`. Postal region is separate.
|
|
46
|
+
- There is one official identifier with type `TAX`, `EIN` or `BN`. A second
|
|
47
|
+
`taxId` is forbidden. A later VAT/tax identifier is additional evidence only
|
|
48
|
+
when the official identifier is not already `TAX`.
|
|
49
|
+
- Official licence is separate and optional on Test Network. A later production
|
|
50
|
+
policy may require it for authorized clinical/index writers.
|
|
51
|
+
- Legal representative and technical controller are separate. If they are the
|
|
52
|
+
same person, the product checkbox reuses the verified email and disables the
|
|
53
|
+
duplicate input.
|
|
54
|
+
- The six independent roles are service provider, data provider and data
|
|
55
|
+
consumer for both the individual index and the Digital Twin index.
|
|
56
|
+
- No application contains a PIN. After verified login, the global profile gate
|
|
57
|
+
creates a PIN once for a new account or unlocks the existing profile after a
|
|
58
|
+
reload, before any redirect or application screen.
|
|
59
|
+
|
|
60
|
+
## PDF and activation licence
|
|
61
|
+
|
|
62
|
+
The PDF `docVersion` is the lowercase SHA-256 of the immutable template bytes.
|
|
63
|
+
Its portal field is the deployed web origin, not the GW hostname. Authorized
|
|
64
|
+
staff review the application, not merely the PDF.
|
|
65
|
+
|
|
66
|
+
Submission goes directly to human review. There is no generic ten-minute PDF
|
|
67
|
+
or legal-representative email OTP. A product extension may verify an official
|
|
68
|
+
registry telephone when that evidence source requires it.
|
|
69
|
+
|
|
70
|
+
After approval, GW `Order` issues the controller activation licence used once
|
|
71
|
+
by exchange and DCR. Test Network delivery may be email; production delivery is
|
|
72
|
+
planned postal verification. The production licence target is one year with a
|
|
73
|
+
renewal warning one month before expiry. Delivery and renewal remain explicit
|
|
74
|
+
TODOs until their live boundary tests exist.
|
|
75
|
+
|
|
76
|
+
## Product extensions
|
|
77
|
+
|
|
78
|
+
- **VetChain:** animal sectors, animal individual, veterinary clinical data,
|
|
79
|
+
animal Digital Twin and veterinarian-only animal break-glass.
|
|
80
|
+
- **UHC UNID:** health/other governed sectors, human individual and its own
|
|
81
|
+
consent/emergency policy.
|
|
82
|
+
- **SOSChain:** emergency-response sectors and associations; capabilities come
|
|
83
|
+
from its BFF/GW and fail closed when not enabled.
|
|
84
|
+
|
|
85
|
+
Extensions may narrow or disable a common capability. They must never obtain
|
|
86
|
+
authority from a browser-selected role, product id, sector or checkbox.
|
|
87
|
+
|
|
88
|
+
See [`snippets/reusable-professional-bff.ts`](snippets/reusable-professional-bff.ts)
|
|
89
|
+
for a commented integration example.
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# VetChain animal identity and evidence
|
|
2
|
+
|
|
3
|
+
## Canonical numeric contract
|
|
4
|
+
|
|
5
|
+
The personal portal, professional portal, PETD and telephone assistant use the
|
|
6
|
+
same ordered identity tuple:
|
|
7
|
+
|
|
8
|
+
```text
|
|
9
|
+
speciesId7 + jurisdictionCode5 + animalNumericId15
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
The check digit follows that same passport and voice order:
|
|
13
|
+
|
|
14
|
+
```text
|
|
15
|
+
checkDigit1 = Damm(speciesId7 + jurisdictionCode5 + animalNumericId15)
|
|
16
|
+
animalIdentifier16 = animalNumericId15 + checkDigit1
|
|
17
|
+
cardNumber21 = jurisdictionCode5 + animalIdentifier16
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
`speciesId7` is the zero-padded seven-digit projection of the canonical
|
|
21
|
+
`ncbiTaxonomyId`. `jurisdictionCode5` is country3 plus the immutable region2 of
|
|
22
|
+
birth or initial registration. It is not current residence, current location
|
|
23
|
+
or provider. The animal number contains exactly 15 decimal digits for cards,
|
|
24
|
+
DTMF and voice readback.
|
|
25
|
+
|
|
26
|
+
Species is shown separately in PETD/card/QR and is required to validate the
|
|
27
|
+
check digit. The digit detects transcription or inconsistent context; with
|
|
28
|
+
only ten possible results it cannot create uniqueness. An authoritative
|
|
29
|
+
registry must reject two different animals using the same 15-digit number in
|
|
30
|
+
the same jurisdiction and evidence-system namespace.
|
|
31
|
+
|
|
32
|
+
## Animal Index blockchain asset
|
|
33
|
+
|
|
34
|
+
The permissioned Animal Index uses the exact fixed-width decimal string, not a
|
|
35
|
+
BigInt, hexadecimal conversion or packed representation:
|
|
36
|
+
|
|
37
|
+
```text
|
|
38
|
+
canonicalNumericId27 = speciesId7 + jurisdictionCode5 + animalNumericId15
|
|
39
|
+
assetId = lowercaseHex(SHA3-256(ASCII(canonicalNumericId27)))
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
ASCII and UTF-8 produce the same 27 bytes for decimal digits. Keeping the
|
|
43
|
+
fixed-width string preserves leading zeroes and avoids byte-order/length rules;
|
|
44
|
+
converting to a shorter integer byte array provides no meaningful hash-cost
|
|
45
|
+
benefit. SHA-256 is not part of this Animal Index contract; SHA3-256 is the
|
|
46
|
+
minimum accepted algorithm. `checkDigit1` is excluded because it detects capture errors and is not
|
|
47
|
+
identity material.
|
|
48
|
+
|
|
49
|
+
The asset resolves only the currently active index provider, if one exists. It
|
|
50
|
+
contains no animal data and grants no authorization. Provider replacement or
|
|
51
|
+
revocation remains ledger history while current resolution returns only the
|
|
52
|
+
active provider. The deterministic digest is pseudonymous, not encryption, so
|
|
53
|
+
the 27-digit preimage must not be published or logged.
|
|
54
|
+
|
|
55
|
+
## Evidence systems
|
|
56
|
+
|
|
57
|
+
| System | Individual 15-digit source | Rule |
|
|
58
|
+
| --- | --- | --- |
|
|
59
|
+
| USDA AIN | yes | Exactly 15 digits beginning `840`; `840` tags apply to animals born in the United States. |
|
|
60
|
+
| CFIA ISO 11784 | yes | Exactly 15 digits beginning `124`; Canadian livestock ranges are allocated by CFIA and responsible administrators. |
|
|
61
|
+
| ISO 11784/11785 animal microchip | yes | Global technical system: preserve all 15 digits and retain issuer/registry evidence separately, regardless of location. |
|
|
62
|
+
| Herd mark | no | May identify a group/herd; issuing authority and jurisdiction are separate metadata. |
|
|
63
|
+
| Slap tattoo | no | May identify a herd or location; issuing authority and jurisdiction are separate metadata. |
|
|
64
|
+
| Other tattoo | no | Retain as evidence; never pad, hash or reinterpret it as the numeric animal number. |
|
|
65
|
+
|
|
66
|
+
Sources:
|
|
67
|
+
|
|
68
|
+
- USDA APHIS animal identification: <https://www.aphis.usda.gov/nvap/reference-guide/animal-identification>
|
|
69
|
+
- CFIA approved animal indicators: <https://inspection.canada.ca/en/animal-health/terrestrial-animals/traceability/indicators>
|
|
70
|
+
- ICAR global ISO 11784/11785 device registry: <https://www.icar.org/icar-registry-of-rfid-devices-in-conformance-with-iso-11784-11785/>
|
|
71
|
+
- EU pet passport use of the global standard: <https://eur-lex.europa.eu/legal-content/EN/TXT/?uri=CELEX:32026R0132>
|
|
72
|
+
|
|
73
|
+
Evidence capture never means verification. Portal and telephone clients create
|
|
74
|
+
an unverified draft, preserve the evidence system and exact normalized value,
|
|
75
|
+
and wait for an authoritative registry/issuer check before presenting it as
|
|
76
|
+
verified or using it for signed PETD issuance.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { ReusableProfessionalBffClient } from 'vet-sdk-core-ts/reusable-bff'
|
|
2
|
+
|
|
3
|
+
// The product adapter owns sectors and translations. The browser cannot ask
|
|
4
|
+
// for a sector that the deployed product did not explicitly enable.
|
|
5
|
+
const bff = new ReusableProfessionalBffClient({
|
|
6
|
+
productId: 'vetchain',
|
|
7
|
+
allowedSectors: ['animal-care', 'animal-insurance', 'animal-research', 'animal-tech'],
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
// Run this immediately after verified login and before redirecting anywhere.
|
|
11
|
+
// New accounts call create once; returning/reloaded accounts call unlock.
|
|
12
|
+
const gate = await bff.profileGate.status()
|
|
13
|
+
void gate
|
|
14
|
+
|
|
15
|
+
// The application contains business evidence only: no taxId duplicate, PIN,
|
|
16
|
+
// activation code, wallet seed, client assertion, VP or SMART token.
|
|
17
|
+
await bff.applications.submit({
|
|
18
|
+
sector: 'animal-care',
|
|
19
|
+
officialId: { type: 'BN', value: '704457076' },
|
|
20
|
+
countryCode: 'CA',
|
|
21
|
+
regionalId: true,
|
|
22
|
+
subdivisionCode: 'CA-BC',
|
|
23
|
+
officialLicense: 'CVBC-12345',
|
|
24
|
+
legalRepresentative: { name: 'Legal Representative', email: 'legal@example.test' },
|
|
25
|
+
technicalController: { sameAsLegalRepresentative: true },
|
|
26
|
+
participationRoles: [
|
|
27
|
+
'individual-index-data-provider',
|
|
28
|
+
'digital-twin-data-consumer',
|
|
29
|
+
],
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
// High-level subject, clinical, Digital Twin and break-glass operations use the
|
|
33
|
+
// current authenticated BFF session. Internal GW security plumbing stays on the
|
|
34
|
+
// server and is rejected if a browser tries to include it.
|
|
35
|
+
await bff.subjects.addClinicalData({ resource: { resourceType: 'Observation' } })
|
|
36
|
+
await bff.digitalTwins.search({ section: 'results', text: 'blood', dateFrom: '2026-01-01' })
|
|
37
|
+
await bff.breakGlass.request({
|
|
38
|
+
subjectDid: 'did:web:example:card:vetchain:12409:1234567890123456',
|
|
39
|
+
requestedSections: ['results'],
|
|
40
|
+
incidentId: 'incident-1',
|
|
41
|
+
justification: 'Immediate emergency veterinary treatment is required.',
|
|
42
|
+
})
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vet-sdk-core-ts",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.7",
|
|
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",
|
|
@@ -31,11 +31,16 @@
|
|
|
31
31
|
"./animal-onboarding": {
|
|
32
32
|
"types": "./dist/animal-onboarding.d.ts",
|
|
33
33
|
"default": "./dist/animal-onboarding.js"
|
|
34
|
+
},
|
|
35
|
+
"./reusable-bff": {
|
|
36
|
+
"types": "./dist/reusable-bff.d.ts",
|
|
37
|
+
"default": "./dist/reusable-bff.js"
|
|
34
38
|
}
|
|
35
39
|
},
|
|
36
40
|
"files": [
|
|
37
41
|
"dist",
|
|
38
|
-
"README.md"
|
|
42
|
+
"README.md",
|
|
43
|
+
"docs"
|
|
39
44
|
],
|
|
40
45
|
"scripts": {
|
|
41
46
|
"clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"",
|
|
@@ -54,6 +59,6 @@
|
|
|
54
59
|
},
|
|
55
60
|
"dependencies": {
|
|
56
61
|
"@noble/hashes": "^2.2.0",
|
|
57
|
-
"vet-data-utils-ts": "0.
|
|
62
|
+
"vet-data-utils-ts": "0.4.2"
|
|
58
63
|
}
|
|
59
64
|
}
|