sos-sdk-node-ts 0.1.7 → 0.1.9
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/CHANGELOG.md +83 -0
- package/NOTICE +2 -0
- package/README.md +19 -4
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/subject-card-directory-client.d.ts +51 -0
- package/dist/subject-card-directory-client.js +291 -0
- package/docs/AUTHORIZED_SUBJECT_DIRECTORY.md +48 -0
- package/docs/PROMPT_AUDIO_RUNTIME.md +4 -4
- package/docs/SDK_LAYERING.md +3 -7
- package/package.json +8 -1
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
- Send authorized-subject directory submit and poll envelopes with the
|
|
6
|
+
canonical `application/didcomm-plain+json` media type so compat-mode SOS
|
|
7
|
+
gateways do not reject card discovery as legacy JSON.
|
|
8
|
+
- Add the shared trusted-BFF authorized-subject directory client with canonical
|
|
9
|
+
SOS GW Organization/License routes, real submit/poll HTTP coverage,
|
|
10
|
+
independent `sameAs` alias matching and fail-closed animal-card DID
|
|
11
|
+
validation.
|
|
12
|
+
- Document multiple-controller ownership and future explicit principal
|
|
13
|
+
controller promotion without changing the child or animal subject identity.
|
|
14
|
+
- Add the 2026 development-owner notice and re-establish the public repository
|
|
15
|
+
from a clean source snapshot while
|
|
16
|
+
retaining this changelog as the package release record.
|
|
17
|
+
|
|
18
|
+
## 0.1.7 - 2026-10-05
|
|
19
|
+
|
|
20
|
+
- Add a short-lived HMAC-protected email assertion for exact BFF-to-BFF
|
|
21
|
+
audiences and purposes. It normalizes the authenticated email and fails
|
|
22
|
+
closed for weak secrets, altered payloads, expiry and cross-audience reuse;
|
|
23
|
+
it is not a browser-selected mode or an authorization capability.
|
|
24
|
+
|
|
25
|
+
## 0.1.6 - 2026-10-04
|
|
26
|
+
|
|
27
|
+
- Add one atomic accepted-handover persistence boundary for the
|
|
28
|
+
Encounter/EncounterHistory projection, future professional Group audience
|
|
29
|
+
revision and family-safe journey event.
|
|
30
|
+
|
|
31
|
+
## 0.1.5 - 2026-10-04
|
|
32
|
+
|
|
33
|
+
- Add the product-neutral confidential channel cache shared by SOSChain,
|
|
34
|
+
VetChain and UHC Next.js BFFs. One profile-and-channel document stores
|
|
35
|
+
materialized FHIR Communication messages behind the unlocked profile's
|
|
36
|
+
confidential-storage adapter, skips already opened source JWEs and keeps
|
|
37
|
+
subject, channel, recipients and payload out of cleartext persistence.
|
|
38
|
+
|
|
39
|
+
## 0.1.4 - 2026-10-04
|
|
40
|
+
|
|
41
|
+
- Add the injectable protected Bundle-sync service used by trusted runtimes for
|
|
42
|
+
clinical documents, Consent, emergency contacts and Encounter transfers.
|
|
43
|
+
It persists the exact retryable draft, enforces idempotency and confirms a
|
|
44
|
+
mutation only after authoritative provider readback.
|
|
45
|
+
- Pin `sos-sdk-core-ts@0.5.1`, which owns the portable synchronization state
|
|
46
|
+
and response reconciliation contract.
|
|
47
|
+
|
|
48
|
+
- Link the coordinated clinical accessor adoption boundary and keep optional
|
|
49
|
+
portal/assistant editor work out of the Node adapter until runtime behavior
|
|
50
|
+
is actually required.
|
|
51
|
+
|
|
52
|
+
## 0.1.3 - 2026-09-27
|
|
53
|
+
|
|
54
|
+
- Serialize Node test files so package and publication gates cannot exhaust a
|
|
55
|
+
shared developer workstation with parallel workers.
|
|
56
|
+
- Require one version change only after grouped changes are merged and tested,
|
|
57
|
+
and preserve every exact published version on an immutable stable branch.
|
|
58
|
+
|
|
59
|
+
- Add the product-neutral immutable prompt-audio Node runtime with stable
|
|
60
|
+
SHA-256 object keys, durable filesystem storage, concurrent-miss coalescing,
|
|
61
|
+
sequential prewarming and an ElevenLabs multilingual-v2 adapter.
|
|
62
|
+
- Correct the package boundary: SOSChain, Vet and UHC may reuse this Node
|
|
63
|
+
runtime while keeping their sector policy in their own SDKs.
|
|
64
|
+
- Add a product-neutral, create-only GCS prompt store so identical reviewed
|
|
65
|
+
assets can be reused across Vet, UHC and Connect ICA without sharing live
|
|
66
|
+
OTPs, personalized speech or clinical data.
|
|
67
|
+
|
|
68
|
+
## 0.1.2
|
|
69
|
+
|
|
70
|
+
- Add the Node CSPRNG adapter and factory for the shared multi-recipient ML-KEM
|
|
71
|
+
secure-message core used by written and binary-audio payloads.
|
|
72
|
+
- Keep Node limited to validated `node:crypto.randomBytes` output; ML-KEM, KDF,
|
|
73
|
+
CEK wrapping and AES content protection remain in runtime-neutral core.
|
|
74
|
+
|
|
75
|
+
## 0.1.1
|
|
76
|
+
|
|
77
|
+
- Apply the blocking main-before-publish release contract to the SOSChain Node SDK documentation and repository skills.
|
|
78
|
+
|
|
79
|
+
## 0.1.0
|
|
80
|
+
|
|
81
|
+
- Initialize the SOSChain-specific Node SDK above `sos-sdk-core-ts@0.2.0`.
|
|
82
|
+
- Add fail-closed authenticated emergency dispatch with HTTPS enforcement,
|
|
83
|
+
idempotency and acknowledgement validation.
|
package/NOTICE
ADDED
package/README.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# sos-sdk-node-ts
|
|
2
2
|
|
|
3
|
+
Copyright 2026 Connecting Solution & Applications Ltd.
|
|
4
|
+
Licensed under the Apache License, Version 2.0; see [`NOTICE`](NOTICE).
|
|
5
|
+
|
|
6
|
+
## Public source history
|
|
7
|
+
|
|
8
|
+
The public repository begins from a clean 2026 source snapshot. The retained
|
|
9
|
+
[`CHANGELOG.md`](CHANGELOG.md) records package releases without republishing
|
|
10
|
+
the former Git push history.
|
|
11
|
+
|
|
3
12
|
Accepted Encounter handover persistence and future channel-audience
|
|
4
13
|
reconciliation are documented in
|
|
5
14
|
[`docs/ENCOUNTER_TRANSFER_AUDIENCE_PERSISTENCE.md`](docs/ENCOUNTER_TRANSFER_AUDIENCE_PERSISTENCE.md).
|
|
@@ -13,7 +22,7 @@ deterministic failures and never commit a local package reference. Staging
|
|
|
13
22
|
begins only after exact registry packages are installed and verified.
|
|
14
23
|
|
|
15
24
|
|
|
16
|
-
Product-neutral Node adapters
|
|
25
|
+
Product-neutral Node adapters for trusted SOSCHAIN runtimes.
|
|
17
26
|
|
|
18
27
|
```text
|
|
19
28
|
gdc-* frozen
|
|
@@ -27,8 +36,8 @@ sos-sdk-node-ts
|
|
|
27
36
|
|
|
28
37
|
This package owns reusable server-side Node transport, credentials,
|
|
29
38
|
persistence, jobs and prompt-audio runtime behavior. It does not own
|
|
30
|
-
deterministic data contracts, common authorization or sector policy.
|
|
31
|
-
|
|
39
|
+
deterministic data contracts, common authorization or sector policy. Downstream
|
|
40
|
+
applications consume it without creating dependencies between one another.
|
|
32
41
|
|
|
33
42
|
`BundleSyncDraftService` is the trusted-runtime adapter for the shared durable
|
|
34
43
|
editor outbox. The application injects protected storage, payload protection
|
|
@@ -40,7 +49,7 @@ readback.
|
|
|
40
49
|
It also does not duplicate the typed clinical Bundle editors exposed by the
|
|
41
50
|
GDC Node SDK. Add a SOS Node adapter only when a real shared channel workflow
|
|
42
51
|
requires runtime behavior, following the
|
|
43
|
-
[
|
|
52
|
+
[clinical accessor adoption matrix](https://github.com/Fundacion-UNID/sos-data-utils-ts/blob/main/docs/CLINICAL_TYPED_ACCESSOR_ADOPTION.md).
|
|
44
53
|
Portals and assistants without that editor remain on their verified versions.
|
|
45
54
|
|
|
46
55
|
The first adapter, `SosEmergencyDispatchClient`, sends an already-authorized
|
|
@@ -54,6 +63,12 @@ The receiving application still authorizes the operation and should reject
|
|
|
54
63
|
replay where it matters. Never expose the shared secret or let a browser set
|
|
55
64
|
the trusted issuer, audience, purpose or identity claims.
|
|
56
65
|
|
|
66
|
+
`sos-sdk-node-ts/subject-card-directory-client` owns the complete asynchronous
|
|
67
|
+
SOS GW Organization/License search and polling protocol used by every portal
|
|
68
|
+
BFF. It matches exact aliases inside `Organization.sameAs` CSV values and
|
|
69
|
+
rejects removed animal-card DID layouts before UI mapping. See
|
|
70
|
+
[the authorized subject directory contract](docs/AUTHORIZED_SUBJECT_DIRECTORY.md).
|
|
71
|
+
|
|
57
72
|
## Secure-message CSPRNG adapter
|
|
58
73
|
|
|
59
74
|
`sos-sdk-node-ts/secure-message-crypto` exports `NodeSecureRandomSource` and
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export type SosGatewayRouteContext = Readonly<{
|
|
2
|
+
tenantId: string;
|
|
3
|
+
jurisdiction: string;
|
|
4
|
+
sector: string;
|
|
5
|
+
}>;
|
|
6
|
+
export type AuthorizedSubjectVerifiedContact = Readonly<{
|
|
7
|
+
email?: string;
|
|
8
|
+
telephone?: string;
|
|
9
|
+
}>;
|
|
10
|
+
export type AuthorizedIndividualSubjectDirectoryInput = Readonly<{
|
|
11
|
+
verifiedContact: AuthorizedSubjectVerifiedContact;
|
|
12
|
+
requestThid?: string;
|
|
13
|
+
pollOptions?: Readonly<{
|
|
14
|
+
timeoutMs?: number;
|
|
15
|
+
intervalMs?: number;
|
|
16
|
+
}>;
|
|
17
|
+
}>;
|
|
18
|
+
export type AuthorizedIndividualSubject = Readonly<{
|
|
19
|
+
subjectDid: string;
|
|
20
|
+
organizationId?: string;
|
|
21
|
+
role?: string;
|
|
22
|
+
grantClaims: Readonly<Record<string, unknown>>;
|
|
23
|
+
authorizationEvidenceId?: string;
|
|
24
|
+
relatedPersonId?: string;
|
|
25
|
+
issuerDid?: string;
|
|
26
|
+
subjectClaims: Readonly<Record<string, unknown>>;
|
|
27
|
+
}>;
|
|
28
|
+
export type SosGatewaySubjectDirectoryClientOptions = Readonly<{
|
|
29
|
+
baseUrl: string;
|
|
30
|
+
bearerToken?: string;
|
|
31
|
+
appId?: string;
|
|
32
|
+
appVersion?: string;
|
|
33
|
+
defaultHeaders?: Readonly<Record<string, string>>;
|
|
34
|
+
requestTimeoutMs?: number;
|
|
35
|
+
fetchImpl?: typeof fetch;
|
|
36
|
+
}>;
|
|
37
|
+
/**
|
|
38
|
+
* Shared trusted-BFF client for the canonical asynchronous SOS GW subject
|
|
39
|
+
* directory. The verified contact is discovery evidence only; results never
|
|
40
|
+
* grant a protected clinical or emergency action.
|
|
41
|
+
*/
|
|
42
|
+
export declare class SosGatewaySubjectDirectoryClient {
|
|
43
|
+
private readonly baseUrl;
|
|
44
|
+
private readonly headers;
|
|
45
|
+
private readonly requestTimeoutMs;
|
|
46
|
+
private readonly fetchImpl;
|
|
47
|
+
constructor(options: SosGatewaySubjectDirectoryClientOptions);
|
|
48
|
+
listAuthorizedIndividualSubjects(routeContext: SosGatewayRouteContext, input: AuthorizedIndividualSubjectDirectoryInput): Promise<AuthorizedIndividualSubject[]>;
|
|
49
|
+
private submitAndPoll;
|
|
50
|
+
private post;
|
|
51
|
+
}
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { parseAnimalCardDidWeb } from 'sos-data-utils-ts/animal-card-interoperability';
|
|
3
|
+
import { buildGatewayApplicationHeaders } from 'sos-data-utils-ts/gateway-application-identity';
|
|
4
|
+
const CLAIM_AUDIENCE_TYPE = 'org.schema.IndividualProduct.audience.audienceType';
|
|
5
|
+
const CLAIM_ITEM_CONDITION = 'org.schema.IndividualProduct.itemCondition';
|
|
6
|
+
const CLAIM_PERSON_EMAIL = 'org.schema.Person.email';
|
|
7
|
+
const CLAIM_PERSON_TELEPHONE = 'org.schema.Person.telephone';
|
|
8
|
+
const CLAIM_ORGANIZATION_SAME_AS = 'org.schema.Organization.sameAs';
|
|
9
|
+
const CLAIM_ORGANIZATION_OWNER_EMAIL = 'org.schema.Organization.owner.email';
|
|
10
|
+
const CLAIM_ORGANIZATION_OWNER_TELEPHONE = 'org.schema.Organization.owner.telephone';
|
|
11
|
+
const CLAIM_ORGANIZATION_OWNER_IDENTIFIER = 'org.schema.Organization.owner.identifier.value';
|
|
12
|
+
const CLAIM_ORGANIZATION_MEMBER_ROLE = 'org.schema.Organization.member.role';
|
|
13
|
+
const CLAIM_RELATED_PERSON_ROLE = 'RelatedPerson.role';
|
|
14
|
+
const CLAIM_OCCUPATION_IDENTIFIER = 'org.schema.Person.hasOccupation.identifier.value';
|
|
15
|
+
/**
|
|
16
|
+
* Shared trusted-BFF client for the canonical asynchronous SOS GW subject
|
|
17
|
+
* directory. The verified contact is discovery evidence only; results never
|
|
18
|
+
* grant a protected clinical or emergency action.
|
|
19
|
+
*/
|
|
20
|
+
export class SosGatewaySubjectDirectoryClient {
|
|
21
|
+
baseUrl;
|
|
22
|
+
headers;
|
|
23
|
+
requestTimeoutMs;
|
|
24
|
+
fetchImpl;
|
|
25
|
+
constructor(options) {
|
|
26
|
+
this.baseUrl = normalizeBaseUrl(options.baseUrl);
|
|
27
|
+
this.requestTimeoutMs = positiveInteger(options.requestTimeoutMs ?? 15_000, 'request_timeout_invalid');
|
|
28
|
+
this.fetchImpl = options.fetchImpl ?? fetch;
|
|
29
|
+
this.headers = Object.freeze({
|
|
30
|
+
// The shared GW contract identifies this existing thid/body envelope as
|
|
31
|
+
// DIDComm plaintext. Compat and strict deployments must never receive it
|
|
32
|
+
// under the ambiguous legacy application/json media type.
|
|
33
|
+
'Content-Type': 'application/didcomm-plain+json',
|
|
34
|
+
Accept: 'application/didcomm-plain+json',
|
|
35
|
+
...(requiredOptional(options.bearerToken) ? { Authorization: `Bearer ${requiredOptional(options.bearerToken)}` } : {}),
|
|
36
|
+
...(requiredOptional(options.appId)
|
|
37
|
+
? buildGatewayApplicationHeaders({
|
|
38
|
+
appId: requiredOptional(options.appId),
|
|
39
|
+
appVersion: options.appVersion,
|
|
40
|
+
})
|
|
41
|
+
: {}),
|
|
42
|
+
...(options.defaultHeaders ?? {}),
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
async listAuthorizedIndividualSubjects(routeContext, input) {
|
|
46
|
+
const context = normalizeRouteContext(routeContext);
|
|
47
|
+
const email = String(input.verifiedContact?.email || '').trim().toLowerCase();
|
|
48
|
+
const telephone = String(input.verifiedContact?.telephone || '').trim();
|
|
49
|
+
if (!email && !telephone)
|
|
50
|
+
throw new Error('verified_contact_required');
|
|
51
|
+
const organizationPath = gatewayPath(context, 'Organization', '_search');
|
|
52
|
+
const organizationPollPath = gatewayPath(context, 'Organization', '_search-response');
|
|
53
|
+
const ownerResult = await this.submitAndPoll(organizationPath, organizationPollPath, {
|
|
54
|
+
thid: input.requestThid || threadId('authorized-subject-owner'),
|
|
55
|
+
body: searchBundle('Family-search-v1.0', {
|
|
56
|
+
'@context': 'org.schema',
|
|
57
|
+
...(email ? { [CLAIM_ORGANIZATION_OWNER_EMAIL]: email } : {}),
|
|
58
|
+
...(telephone ? { [CLAIM_ORGANIZATION_OWNER_TELEPHONE]: telephone } : {}),
|
|
59
|
+
}),
|
|
60
|
+
}, input.pollOptions);
|
|
61
|
+
const ownedSubjects = readOwnedSubjects(ownerResult.body, context, { email, telephone });
|
|
62
|
+
const subjectsByDid = new Map(ownedSubjects.map((subject) => [subject.subjectDid, subject]));
|
|
63
|
+
const licensePath = gatewayPath(context, 'License', '_search');
|
|
64
|
+
const licensePollPath = gatewayPath(context, 'License', '_search-response');
|
|
65
|
+
const licenseResult = await this.submitAndPoll(licensePath, licensePollPath, {
|
|
66
|
+
thid: input.requestThid || threadId('authorized-subject-license'),
|
|
67
|
+
body: {
|
|
68
|
+
resourceType: 'Bundle',
|
|
69
|
+
type: 'batch',
|
|
70
|
+
data: [{
|
|
71
|
+
request: { method: 'POST' },
|
|
72
|
+
resource: { meta: { status: 'active', claims: {
|
|
73
|
+
'@context': 'org.schema',
|
|
74
|
+
[CLAIM_AUDIENCE_TYPE]: 'Individual',
|
|
75
|
+
[CLAIM_ITEM_CONDITION]: 'https://schema.org/UsedCondition',
|
|
76
|
+
...(email ? { [CLAIM_PERSON_EMAIL]: email } : {}),
|
|
77
|
+
...(telephone ? { [CLAIM_PERSON_TELEPHONE]: telephone } : {}),
|
|
78
|
+
} } },
|
|
79
|
+
}],
|
|
80
|
+
},
|
|
81
|
+
}, input.pollOptions);
|
|
82
|
+
const licenses = readAcceptedLicenses(licenseResult.body, context);
|
|
83
|
+
for (const license of licenses) {
|
|
84
|
+
if (subjectsByDid.has(license.subjectDid))
|
|
85
|
+
continue;
|
|
86
|
+
const organizationResult = await this.submitAndPoll(organizationPath, organizationPollPath, {
|
|
87
|
+
thid: threadId('authorized-subject-record'),
|
|
88
|
+
body: searchBundle('Family-search-v1.0', {
|
|
89
|
+
'@context': 'org.schema',
|
|
90
|
+
[CLAIM_ORGANIZATION_SAME_AS]: license.subjectDid,
|
|
91
|
+
}),
|
|
92
|
+
}, input.pollOptions);
|
|
93
|
+
const organization = readFirstOrganization(organizationResult.body);
|
|
94
|
+
if (!publicAliases(organization.claims[CLAIM_ORGANIZATION_SAME_AS]).includes(license.subjectDid))
|
|
95
|
+
continue;
|
|
96
|
+
subjectsByDid.set(license.subjectDid, Object.freeze({
|
|
97
|
+
subjectDid: license.subjectDid,
|
|
98
|
+
...(organization.organizationId ? { organizationId: organization.organizationId } : {}),
|
|
99
|
+
...(license.role ? { role: license.role } : {}),
|
|
100
|
+
grantClaims: license.grantClaims,
|
|
101
|
+
...(license.authorizationEvidenceId ? { authorizationEvidenceId: license.authorizationEvidenceId } : {}),
|
|
102
|
+
...(license.relatedPersonId ? { relatedPersonId: license.relatedPersonId } : {}),
|
|
103
|
+
...(license.issuerDid ? { issuerDid: license.issuerDid } : {}),
|
|
104
|
+
subjectClaims: organization.claims,
|
|
105
|
+
}));
|
|
106
|
+
}
|
|
107
|
+
return [...subjectsByDid.values()];
|
|
108
|
+
}
|
|
109
|
+
async submitAndPoll(submitPath, pollPath, payload, options) {
|
|
110
|
+
const submit = await this.post(submitPath, payload);
|
|
111
|
+
requireSuccessfulResponse(submit, 'gateway_submit_failed');
|
|
112
|
+
const timeoutMs = positiveInteger(options?.timeoutMs ?? 30_000, 'poll_timeout_invalid');
|
|
113
|
+
const intervalMs = positiveInteger(options?.intervalMs ?? 250, 'poll_interval_invalid');
|
|
114
|
+
const started = Date.now();
|
|
115
|
+
for (;;) {
|
|
116
|
+
const poll = await this.post(pollPath, { thid: payload.thid });
|
|
117
|
+
if (poll.status !== 202) {
|
|
118
|
+
requireSuccessfulResponse(poll, 'gateway_poll_failed');
|
|
119
|
+
return poll;
|
|
120
|
+
}
|
|
121
|
+
if (Date.now() - started >= timeoutMs)
|
|
122
|
+
throw new Error('gateway_poll_timeout');
|
|
123
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
async post(path, payload) {
|
|
127
|
+
const controller = new AbortController();
|
|
128
|
+
const timeout = setTimeout(() => controller.abort(), this.requestTimeoutMs);
|
|
129
|
+
try {
|
|
130
|
+
const response = await this.fetchImpl(`${this.baseUrl}${path}`, {
|
|
131
|
+
method: 'POST',
|
|
132
|
+
headers: this.headers,
|
|
133
|
+
body: JSON.stringify(payload),
|
|
134
|
+
signal: controller.signal,
|
|
135
|
+
});
|
|
136
|
+
const raw = await response.text();
|
|
137
|
+
let body = {};
|
|
138
|
+
if (raw) {
|
|
139
|
+
try {
|
|
140
|
+
body = JSON.parse(raw);
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
body = raw;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return { status: response.status, body };
|
|
147
|
+
}
|
|
148
|
+
finally {
|
|
149
|
+
clearTimeout(timeout);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
function searchBundle(type, claims) {
|
|
154
|
+
return {
|
|
155
|
+
resourceType: 'Bundle',
|
|
156
|
+
type: 'batch',
|
|
157
|
+
data: [{ type, request: { method: 'POST' }, resource: { meta: { claims } } }],
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
function readOwnedSubjects(body, context, verified) {
|
|
161
|
+
const resources = readData(body).flatMap((entry) => entry?.resource?.resourceType === 'Bundle' && Array.isArray(entry.resource.entry)
|
|
162
|
+
? entry.resource.entry.map((item) => item?.resource).filter(Boolean)
|
|
163
|
+
: []);
|
|
164
|
+
return resources.flatMap((resource) => {
|
|
165
|
+
const claims = objectRecord(resource?.meta?.claims);
|
|
166
|
+
const ownerEmail = String(claims[CLAIM_ORGANIZATION_OWNER_EMAIL] || '').trim().toLowerCase();
|
|
167
|
+
const ownerTelephone = String(claims[CLAIM_ORGANIZATION_OWNER_TELEPHONE] || '').trim();
|
|
168
|
+
if (!((verified.email && ownerEmail === verified.email) || (verified.telephone && ownerTelephone === verified.telephone)))
|
|
169
|
+
return [];
|
|
170
|
+
const subjectDid = selectSubjectCardAlias(claims[CLAIM_ORGANIZATION_SAME_AS], context);
|
|
171
|
+
if (!subjectDid)
|
|
172
|
+
return [];
|
|
173
|
+
const role = context.sector === 'animal-care'
|
|
174
|
+
? 'RESPRSN'
|
|
175
|
+
: String(claims[CLAIM_ORGANIZATION_MEMBER_ROLE] || '').trim();
|
|
176
|
+
const relatedPersonId = String(claims[CLAIM_ORGANIZATION_OWNER_IDENTIFIER] || '').trim();
|
|
177
|
+
return [Object.freeze({
|
|
178
|
+
subjectDid,
|
|
179
|
+
...(String(resource?.id || '').trim() ? { organizationId: String(resource.id).trim() } : {}),
|
|
180
|
+
...(role ? { role } : {}),
|
|
181
|
+
...(relatedPersonId ? { relatedPersonId } : {}),
|
|
182
|
+
grantClaims: Object.freeze({}),
|
|
183
|
+
subjectClaims: claims,
|
|
184
|
+
})];
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
function readAcceptedLicenses(body, context) {
|
|
188
|
+
const rows = readData(body)[0]?.resource?.data;
|
|
189
|
+
if (!Array.isArray(rows))
|
|
190
|
+
return [];
|
|
191
|
+
return rows.flatMap((row) => {
|
|
192
|
+
const meta = objectRecord(row?.meta || row?.resource?.meta);
|
|
193
|
+
const subjectDid = String(meta.authorizedSubjectDid || '').trim();
|
|
194
|
+
if (!subjectDid)
|
|
195
|
+
return [];
|
|
196
|
+
requireCanonicalSubjectDid(subjectDid, context);
|
|
197
|
+
const claims = objectRecord(meta.claims);
|
|
198
|
+
const relatedRoles = String(claims[CLAIM_RELATED_PERSON_ROLE] || '')
|
|
199
|
+
.split(',').map((value) => value.trim().toUpperCase()).filter(Boolean);
|
|
200
|
+
const occupation = String(claims[CLAIM_OCCUPATION_IDENTIFIER] || '').trim();
|
|
201
|
+
const role = relatedRoles.includes('CAREGIVER') ? 'CAREGIVER' : relatedRoles[0] || occupation;
|
|
202
|
+
return [Object.freeze({
|
|
203
|
+
subjectDid,
|
|
204
|
+
...(role ? { role } : {}),
|
|
205
|
+
grantClaims: claims,
|
|
206
|
+
...(requiredOptional(meta.identifier) ? { authorizationEvidenceId: requiredOptional(meta.identifier) } : {}),
|
|
207
|
+
...(requiredOptional(meta.relatedPersonId) ? { relatedPersonId: requiredOptional(meta.relatedPersonId) } : {}),
|
|
208
|
+
...(requiredOptional(meta.issuerDid) ? { issuerDid: requiredOptional(meta.issuerDid) } : {}),
|
|
209
|
+
})];
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
function readFirstOrganization(body) {
|
|
213
|
+
const entry = readData(body)[0];
|
|
214
|
+
const claims = objectRecord(entry?.resource?.meta?.claims || entry?.meta?.claims);
|
|
215
|
+
const organizationId = String(entry?.resource?.id || entry?.id || '').trim();
|
|
216
|
+
return { claims, ...(organizationId ? { organizationId } : {}) };
|
|
217
|
+
}
|
|
218
|
+
function readData(body) {
|
|
219
|
+
const root = objectRecord(body);
|
|
220
|
+
const payload = objectRecord(root.body || root);
|
|
221
|
+
return Array.isArray(payload.data) ? payload.data : [];
|
|
222
|
+
}
|
|
223
|
+
function selectSubjectCardAlias(value, context) {
|
|
224
|
+
const aliases = publicAliases(value);
|
|
225
|
+
const candidate = aliases.find((alias) => context.sector === 'animal-care'
|
|
226
|
+
? alias.includes(':card:vetchain:')
|
|
227
|
+
: alias.includes(':card:uhc:'));
|
|
228
|
+
if (!candidate)
|
|
229
|
+
return undefined;
|
|
230
|
+
requireCanonicalSubjectDid(candidate, context);
|
|
231
|
+
return candidate;
|
|
232
|
+
}
|
|
233
|
+
function requireCanonicalSubjectDid(subjectDid, context) {
|
|
234
|
+
if (context.sector === 'animal-care' && !parseAnimalCardDidWeb(subjectDid)) {
|
|
235
|
+
throw new Error('animal_card_did_invalid');
|
|
236
|
+
}
|
|
237
|
+
if (context.sector !== 'animal-care' && !/^did:web:[^\s]+:card:uhc:(?:personal|professional):[^\s]+$/i.test(subjectDid)) {
|
|
238
|
+
throw new Error('human_card_did_invalid');
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
function publicAliases(value) {
|
|
242
|
+
const values = Array.isArray(value) ? value : String(value ?? '').split(',');
|
|
243
|
+
return [...new Set(values.map((alias) => String(alias).trim()).filter(Boolean))];
|
|
244
|
+
}
|
|
245
|
+
function gatewayPath(context, resource, action) {
|
|
246
|
+
return `/${context.tenantId}/cds-${context.jurisdiction}/v1/${context.sector}/individual/org.schema/${resource}/${action}`;
|
|
247
|
+
}
|
|
248
|
+
function normalizeRouteContext(value) {
|
|
249
|
+
const segment = (input) => {
|
|
250
|
+
const normalized = String(input ?? '').trim();
|
|
251
|
+
if (!normalized || normalized.includes('/') || normalized === '.' || normalized === '..')
|
|
252
|
+
throw new Error('gateway_route_context_invalid');
|
|
253
|
+
return normalized;
|
|
254
|
+
};
|
|
255
|
+
return Object.freeze({
|
|
256
|
+
tenantId: segment(value.tenantId),
|
|
257
|
+
jurisdiction: segment(value.jurisdiction),
|
|
258
|
+
sector: segment(value.sector),
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
function normalizeBaseUrl(value) {
|
|
262
|
+
const url = new URL(value);
|
|
263
|
+
if (url.protocol !== 'https:' && url.hostname !== 'localhost' && url.hostname !== '127.0.0.1') {
|
|
264
|
+
throw new Error('gateway_base_url_insecure');
|
|
265
|
+
}
|
|
266
|
+
return url.href.replace(/\/$/, '');
|
|
267
|
+
}
|
|
268
|
+
function requireSuccessfulResponse(response, code) {
|
|
269
|
+
if (response.status >= 200 && response.status < 300)
|
|
270
|
+
return;
|
|
271
|
+
const issue = objectRecord(response.body);
|
|
272
|
+
const diagnostics = Array.isArray(issue.issue)
|
|
273
|
+
? issue.issue.map((entry) => String(objectRecord(entry).diagnostics || '').trim()).find(Boolean)
|
|
274
|
+
: undefined;
|
|
275
|
+
throw new Error(`${code}:${response.status}${diagnostics ? `:${diagnostics}` : ''}`);
|
|
276
|
+
}
|
|
277
|
+
function objectRecord(value) {
|
|
278
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
279
|
+
}
|
|
280
|
+
function requiredOptional(value) {
|
|
281
|
+
const normalized = String(value ?? '').trim();
|
|
282
|
+
return normalized || undefined;
|
|
283
|
+
}
|
|
284
|
+
function positiveInteger(value, code) {
|
|
285
|
+
if (!Number.isInteger(value) || value < 1)
|
|
286
|
+
throw new Error(code);
|
|
287
|
+
return value;
|
|
288
|
+
}
|
|
289
|
+
function threadId(prefix) {
|
|
290
|
+
return `${prefix}-${randomUUID()}`;
|
|
291
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Authorized subject directory
|
|
2
|
+
|
|
3
|
+
`SosGatewaySubjectDirectoryClient` is the single Node/BFF adapter for listing
|
|
4
|
+
human and animal cards already associated with a verified contact. Portals do
|
|
5
|
+
not build gateway paths, search Bundles or polling loops themselves.
|
|
6
|
+
|
|
7
|
+
The client submits and polls these SOS GW routes:
|
|
8
|
+
|
|
9
|
+
- `individual/org.schema/Organization/_search` and `_search-response` for
|
|
10
|
+
owner/contact lookup and exact `sameAs` resolution;
|
|
11
|
+
- `individual/org.schema/License/_search` and `_search-response` for active,
|
|
12
|
+
accepted relationship evidence.
|
|
13
|
+
|
|
14
|
+
`Organization.sameAs` remains the comma-separated flat claim used by the
|
|
15
|
+
confidential resource. SOS GW indexes every normalized non-empty alias as its
|
|
16
|
+
own protected indexed attribute. Exact resolution therefore searches one
|
|
17
|
+
alias; a returned CSV is accepted only when it contains that exact alias.
|
|
18
|
+
Animal entries accept only the current species-bound card profile, in which a
|
|
19
|
+
seven-digit taxonomy context precedes the complete 21-digit card number. The
|
|
20
|
+
removed split `jurisdiction5:animalId16` form fails closed. The exact profile
|
|
21
|
+
literal remains owned and documented by the gateway that issues the card.
|
|
22
|
+
|
|
23
|
+
Discovery is not authorization. A signed OpenID token is verified by the BFF;
|
|
24
|
+
the SDK receives only the verified contact and uses the token as server-side
|
|
25
|
+
transport evidence. Selecting a returned card creates no relationship and
|
|
26
|
+
grants no clinical, communication or emergency access.
|
|
27
|
+
|
|
28
|
+
## Multiple controllers and future rotation
|
|
29
|
+
|
|
30
|
+
One child or animal may have multiple active controllers, such as two parents,
|
|
31
|
+
two legal guardians or two responsible keepers. Each controller must have a
|
|
32
|
+
separate governed `RelatedPerson` relationship and accepted licence/evidence.
|
|
33
|
+
The subject card remains in the subject organization's
|
|
34
|
+
`org.schema.Organization.sameAs`; it must not become
|
|
35
|
+
`org.schema.Person.sameAs` of either controller.
|
|
36
|
+
|
|
37
|
+
A later lifecycle contract may designate one active relationship as the
|
|
38
|
+
principal administrative controller. Revoking that controller must revoke only
|
|
39
|
+
that person's relationship, licence and controller keys, then explicitly
|
|
40
|
+
promote another eligible active relationship. It must not rotate the child or
|
|
41
|
+
animal card DID, replace the subject organization, or infer succession from
|
|
42
|
+
CSV ordering. This principal-controller promotion flow is documented here but
|
|
43
|
+
is not implemented by the directory client.
|
|
44
|
+
|
|
45
|
+
Employees are a different subject. Their own professional aliases belong in
|
|
46
|
+
`org.schema.Person.sameAs`; SOS GW normalizes that CSV, indexes every alias
|
|
47
|
+
independently and projects the same aliases into the employee DID Document's
|
|
48
|
+
`alsoKnownAs` alongside its organization-employee authorization URN.
|
|
@@ -23,10 +23,10 @@ developer machine.
|
|
|
23
23
|
`FileSystemPromptAudioStore` supports local and demo execution. Production
|
|
24
24
|
applications use `GcsPromptAudioStore` or implement `PromptAudioObjectStore`
|
|
25
25
|
over another approved object store without changing the cache lifecycle.
|
|
26
|
-
One
|
|
27
|
-
name has no product prefix and reuse occurs only when the complete
|
|
28
|
-
descriptor matches. Deployments may configure an isolation prefix
|
|
29
|
-
requires separate storage. `ElevenLabsPromptAudioSynthesizer`
|
|
26
|
+
One object bucket may be shared by compatible trusted runtimes: the default
|
|
27
|
+
object name has no product prefix and reuse occurs only when the complete
|
|
28
|
+
canonical descriptor matches. Deployments may configure an isolation prefix
|
|
29
|
+
when policy requires separate storage. `ElevenLabsPromptAudioSynthesizer`
|
|
30
30
|
defaults to `eleven_multilingual_v2`; API keys stay in the trusted runtime and
|
|
31
31
|
are neither logged nor included in an error.
|
|
32
32
|
|
package/docs/SDK_LAYERING.md
CHANGED
|
@@ -6,17 +6,13 @@ flowchart TB
|
|
|
6
6
|
DATA --> CORE["sos-sdk-core-ts<br/>runtime-neutral authorization"]
|
|
7
7
|
DATA --> NODE["sos-sdk-node-ts<br/>product-neutral Node adapters"]
|
|
8
8
|
CORE --> NODE
|
|
9
|
-
NODE --> RUNTIME["
|
|
10
|
-
CORE --> VET["vet-sdk-core-ts"]
|
|
11
|
-
CORE --> UHC["uhc-sdk-core-ts"]
|
|
12
|
-
DATA --> VET
|
|
13
|
-
DATA --> UHC
|
|
9
|
+
NODE --> RUNTIME["SOSCHAIN trusted Node runtimes"]
|
|
14
10
|
```
|
|
15
11
|
|
|
16
12
|
`sos-sdk-node-ts` is the common Node-runtime layer; it is not the SOSChain
|
|
17
13
|
product SDK. Product-neutral storage, transport, credential, job and audio
|
|
18
|
-
adapters belong here. Sector policy remains in
|
|
19
|
-
|
|
14
|
+
adapters belong here. Sector policy remains in downstream applications, and no
|
|
15
|
+
shared package may depend back on a product SDK.
|
|
20
16
|
|
|
21
17
|
For immutable prompt audio, `sos-data-utils-ts/prompt-audio` produces the
|
|
22
18
|
canonical descriptor and localized numeric-code fragments. This package
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sos-sdk-node-ts",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.9",
|
|
4
4
|
"description": "Product-neutral Node adapters shared by SOSChain, Vet and UHC runtimes",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Connecting Solution & Applications Ltd",
|
|
@@ -43,11 +43,17 @@
|
|
|
43
43
|
"./bff-email-assertion": {
|
|
44
44
|
"types": "./dist/bff-email-assertion.d.ts",
|
|
45
45
|
"default": "./dist/bff-email-assertion.js"
|
|
46
|
+
},
|
|
47
|
+
"./subject-card-directory-client": {
|
|
48
|
+
"types": "./dist/subject-card-directory-client.d.ts",
|
|
49
|
+
"default": "./dist/subject-card-directory-client.js"
|
|
46
50
|
}
|
|
47
51
|
},
|
|
48
52
|
"files": [
|
|
49
53
|
"dist",
|
|
50
54
|
"README.md",
|
|
55
|
+
"CHANGELOG.md",
|
|
56
|
+
"NOTICE",
|
|
51
57
|
"docs"
|
|
52
58
|
],
|
|
53
59
|
"scripts": {
|
|
@@ -59,6 +65,7 @@
|
|
|
59
65
|
"prepack": "npm run check"
|
|
60
66
|
},
|
|
61
67
|
"dependencies": {
|
|
68
|
+
"sos-data-utils-ts": "0.6.13",
|
|
62
69
|
"sos-sdk-core-ts": "0.5.2"
|
|
63
70
|
},
|
|
64
71
|
"devDependencies": {
|