dataspace-client-sdk-node 0.1.1 → 0.2.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/CHANGELOG.md +31 -0
- package/README.md +44 -1
- package/dist/client.d.ts +153 -33
- package/dist/client.js +619 -93
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/types.d.ts +112 -1
- package/dist/vp-token.d.ts +37 -0
- package/dist/vp-token.js +56 -0
- package/docs/API.md +19 -4
- package/docs/BACKEND_NODE_INTEGRATION.md +249 -0
- package/docs/CONTROLLER_FLOW_STEP_BY_STEP.md +283 -0
- package/docs/DATA_MODEL_ALIGNMENT.md +37 -13
- package/docs/DEVELOPER_USE_CASES.md +10 -2
- package/docs/E2E_LOCAL_GW_UC5.md +49 -0
- package/docs/ENDPOINT_ID_CATALOG.md +90 -0
- package/docs/LEGAL_ORGANIZATION_FLOW_STEP_BY_STEP.md +84 -0
- package/docs/PERSONAL_FLOW_STEP_BY_STEP.md +70 -0
- package/docs/PORTAL_BACKEND_INTEGRATION_HANDOVER.md +343 -0
- package/docs/PRACTITIONER_FLOW_STEP_BY_STEP.md +182 -0
- package/docs/REACT_WEB_INTEGRATION.md +72 -0
- package/examples/e2e-bootstrap-tenant.mjs +3 -2
- package/examples/e2e-individual-flow.mjs +13 -8
- package/examples/host-activate-and-employee.mjs +3 -2
- package/examples/smoke-legal-org-local.mjs +40 -0
- package/package.json +4 -3
- package/src/client.ts +784 -132
- package/src/index.ts +1 -0
- package/src/types.ts +123 -1
- package/src/vp-token.ts +91 -0
- package/tests/client.test.mjs +491 -0
- package/tests/fixtures/ica-vp-minimal.json +67 -0
- package/tests/helpers/vp-token-fixture.mjs +23 -0
- package/tests/live-gw-uc5.e2e.test.mjs +108 -0
- package/SDK_PARITY_MAP.md +0 -120
- package/TODO_PROMPT_NEXT_STEPS.md +0 -185
- package/artifacts/update-smart-wallet.js +0 -1016
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
# CONTROLLER_FLOW_STEP_BY_STEP
|
|
2
|
+
|
|
3
|
+
Strict step-by-step flow for organization onboarding by controller, then controller runtime activation, then employee provisioning.
|
|
4
|
+
|
|
5
|
+
## Security model (read first)
|
|
6
|
+
|
|
7
|
+
Security planes:
|
|
8
|
+
- Transport plane (deployment-specific): backend ↔ gateway channel protection (optional bearer, mTLS, API gateway policy).
|
|
9
|
+
- Identity/business plane (functional flow): controller/member credentials and tokens (`vp_token`, `idToken`, DCR, SMART).
|
|
10
|
+
- Operator/hosting plane: infrastructure tenancy and node-operator lifecycle.
|
|
11
|
+
|
|
12
|
+
Do not mix these planes in implementation or documentation.
|
|
13
|
+
|
|
14
|
+
Secure communication intent:
|
|
15
|
+
- controller/member auth tokens are identity-plane credentials
|
|
16
|
+
- in addition, backend P2P messages can be protected with embedded JWS/JWE via backend wallet
|
|
17
|
+
- use `plain` / `strict` / `auto-detect` communication policy per deployment
|
|
18
|
+
|
|
19
|
+
- `initialOrder` = first order linked to onboarding `offerId` from `_activate`.
|
|
20
|
+
- `licenseOrder` = later order(s) for additional employee licenses.
|
|
21
|
+
- `initialOrder` is authorized by onboarding proof (`vp_token`) + gateway onboarding policy.
|
|
22
|
+
- `licenseOrder` is a runtime business operation and must use controller runtime auth (DCR + SMART token).
|
|
23
|
+
- DCR for controller and DCR for employee are different operations and different identities.
|
|
24
|
+
|
|
25
|
+
Current GW behavior for `Employee/_batch` (important):
|
|
26
|
+
- if employee license pool exists and has `available` seats: employee is created (`201`) and one seat is consumed
|
|
27
|
+
- if employee license pool exists but has no `available` seats: response is `Employee-license-offer-v1.0` (no employee created)
|
|
28
|
+
- if employee license pool does not exist: employee is created without license gating (legacy/non-strict mode)
|
|
29
|
+
|
|
30
|
+
## 0) Runtime context (not all from `.env`)
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
// optional transport security context (deployment-specific, not user identity)
|
|
34
|
+
const transportSecurity = {
|
|
35
|
+
gwBearerToken: process.env.GW_BEARER_TOKEN, // optional
|
|
36
|
+
};
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
// profile-level runtime context for this authenticated role in this organization
|
|
41
|
+
const profileContext = {
|
|
42
|
+
baseUrl: process.env.BASE_URL!,
|
|
43
|
+
jurisdiction: process.env.JURISDICTION!, // REQUIRED
|
|
44
|
+
sector: process.env.SECTOR || 'health-care',
|
|
45
|
+
hostRegistrySector: process.env.HOST_REGISTRY_SECTOR || 'test-network',
|
|
46
|
+
gwDidWeb: process.env.GW_DID_WEB!,
|
|
47
|
+
credentialExpSeconds: Number(process.env.CREDENTIAL_EXP_SECONDS || 300),
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// user/session context (from authenticated portal user + selected tenant/org)
|
|
51
|
+
const sessionContext = {
|
|
52
|
+
tenantId: currentSession.tenantId,
|
|
53
|
+
controllerDidWeb: currentSession.controller.didWeb,
|
|
54
|
+
controllerSignKid: currentSession.controller.signKid, // RFC7638 thumbprint / key id
|
|
55
|
+
controllerIdToken: currentSession.idToken,
|
|
56
|
+
controllerPublicJwk: currentSession.controller.publicJwk,
|
|
57
|
+
controllerEmail: currentSession.controller.email,
|
|
58
|
+
newMemberEmailToInvite: uiFormNewMember.email,
|
|
59
|
+
newMemberRoleCode: uiFormNewMember.hasOccupation || 'ISCO-08|2211',
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// credentials issued previously by ICA
|
|
63
|
+
const onboardingProof = {
|
|
64
|
+
organizationVcJwt: currentSession.ica.organizationVcJwt,
|
|
65
|
+
legalRepresentativeVcJwt: currentSession.ica.legalRepresentativeVcJwt, // optional by policy
|
|
66
|
+
};
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## 1) Build and externally sign `vp_token` (controller identity proof)
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
import {
|
|
73
|
+
DataspaceNodeClient,
|
|
74
|
+
createVP, addVC, prepareForSignature, prepareBytesForSignature, buildVpTokenCompact,
|
|
75
|
+
buildEpochWindow, generateUuidLike,
|
|
76
|
+
} from 'dataspace-client-sdk-node';
|
|
77
|
+
import { ClaimsPersonSchemaorg } from 'gdc-common-utils-ts/constants/schemaorg';
|
|
78
|
+
|
|
79
|
+
const { iat, exp } = buildEpochWindow(profileContext.credentialExpSeconds);
|
|
80
|
+
const header = {
|
|
81
|
+
alg: 'ES256',
|
|
82
|
+
typ: 'JWT',
|
|
83
|
+
kid: `${sessionContext.controllerDidWeb}#${sessionContext.controllerSignKid}`,
|
|
84
|
+
};
|
|
85
|
+
const uniquePresentationNonce = generateUuidLike();
|
|
86
|
+
const vp = createVP({
|
|
87
|
+
iss: sessionContext.controllerDidWeb,
|
|
88
|
+
sub: sessionContext.controllerDidWeb,
|
|
89
|
+
aud: profileContext.gwDidWeb,
|
|
90
|
+
iat,
|
|
91
|
+
exp,
|
|
92
|
+
nonce: uniquePresentationNonce,
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
addVC(vp, onboardingProof.organizationVcJwt);
|
|
96
|
+
if (onboardingProof.legalRepresentativeVcJwt) addVC(vp, onboardingProof.legalRepresentativeVcJwt);
|
|
97
|
+
|
|
98
|
+
const { encodedHeader, encodedPayload } = prepareForSignature(header, vp);
|
|
99
|
+
const bytesToSign = prepareBytesForSignature(header, vp);
|
|
100
|
+
const signatureBase64Url = await externalSigner(bytesToSign); // wallet/HSM
|
|
101
|
+
const vpToken = buildVpTokenCompact(encodedHeader, encodedPayload, signatureBase64Url);
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Signing responsibility (integrator):
|
|
105
|
+
- SDK provides `prepareForSignature(...)` and `prepareBytesForSignature(...)` to produce the JWT signing input.
|
|
106
|
+
- Signing input is exactly: `base64url(header) + "." + base64url(payload)`.
|
|
107
|
+
- The integrator signs that input with the current user's key material (wallet, secure storage, KMS, HSM, etc.).
|
|
108
|
+
- SDK does not require local custody of private keys.
|
|
109
|
+
|
|
110
|
+
## 2) Activate organization in GW (`_activate`)
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
const client = new DataspaceNodeClient({
|
|
114
|
+
baseUrl: profileContext.baseUrl,
|
|
115
|
+
...(transportSecurity.gwBearerToken ? { bearerToken: transportSecurity.gwBearerToken } : {}),
|
|
116
|
+
ctx: { tenantId: sessionContext.tenantId, jurisdiction: profileContext.jurisdiction, sector: profileContext.sector },
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const activation = await client.activateOrganizationInGatewayFromIcaProof(
|
|
120
|
+
{ jurisdiction: profileContext.jurisdiction, sector: profileContext.hostRegistrySector },
|
|
121
|
+
{
|
|
122
|
+
vpToken,
|
|
123
|
+
organizationVc: onboardingProof.organizationVcJwt,
|
|
124
|
+
legalRepresentativeVc: onboardingProof.legalRepresentativeVcJwt,
|
|
125
|
+
numberOfMembers: 2,
|
|
126
|
+
},
|
|
127
|
+
);
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Jurisdiction is mandatory in all flows:
|
|
131
|
+
- Professional: the organization/tenant is registered in one jurisdiction (country) and routes/scopes resolve against it.
|
|
132
|
+
- Personal: user selects jurisdiction first, then service providers are filtered for that jurisdiction.
|
|
133
|
+
- Integrator rule: reject onboarding/session setup when `jurisdiction` is missing.
|
|
134
|
+
|
|
135
|
+
Auth model note:
|
|
136
|
+
- user auth tokens (`idToken`, SMART access token) belong to the identity/business plane (authenticated user session).
|
|
137
|
+
- `transportSecurity.gwBearerToken` belongs to the transport plane only and is optional by deployment.
|
|
138
|
+
- `transportSecurity.gwBearerToken` is not an ICA exchange token and not a controller API key.
|
|
139
|
+
|
|
140
|
+
## 3) Read Offer and show explicit acceptance UI
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
const offerId = client.getOfferIdFromResponse(activation);
|
|
144
|
+
const offer = client.getOfferInfoFromResponse(activation);
|
|
145
|
+
if (!offerId) throw new Error('Offer id missing in activation response');
|
|
146
|
+
// show offer in UI and wait for user acceptance
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## 4) Send Order (always, even if amount is 0)
|
|
150
|
+
|
|
151
|
+
```ts
|
|
152
|
+
// This is the INITIAL ORDER (onboarding order).
|
|
153
|
+
const initialOrder = await client.confirmLegalOrganizationOrderSimple({ offerId });
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## 5) Controller runtime activation (DCR) to operate organization's APIs
|
|
157
|
+
|
|
158
|
+
This is separate from onboarding proof. It creates controller device runtime identity.
|
|
159
|
+
|
|
160
|
+
```ts
|
|
161
|
+
const controllerActivationCode =
|
|
162
|
+
client.getActivationCodeFromResponse(initialOrder) ||
|
|
163
|
+
client.getActivationCodeFromResponse(activation);
|
|
164
|
+
if (!controllerActivationCode) throw new Error('Controller activation code not found');
|
|
165
|
+
|
|
166
|
+
const controllerDevice = await client.activateEmployeeDeviceWithActivationCodeSimple({
|
|
167
|
+
tenantId: sessionContext.tenantId,
|
|
168
|
+
jurisdiction: profileContext.jurisdiction,
|
|
169
|
+
sector: profileContext.sector,
|
|
170
|
+
activationCode: controllerActivationCode,
|
|
171
|
+
idToken: sessionContext.controllerIdToken,
|
|
172
|
+
dcrPayload: {
|
|
173
|
+
application_type: 'web',
|
|
174
|
+
token_endpoint_auth_method: 'private_key_jwt',
|
|
175
|
+
jwks: { keys: [sessionContext.controllerPublicJwk] },
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## 6) Controller identity token vs entity token (separate concerns)
|
|
181
|
+
|
|
182
|
+
- Identity token request endpoint is tenant-scoped auth route:
|
|
183
|
+
`/host/cds-{jurisdiction}/v1/{sector}/{tenantId}/identity/auth/_token`
|
|
184
|
+
- The returned access token is then used to call entity/business endpoints like:
|
|
185
|
+
`/{tenantId}/cds-{jurisdiction}/v1/{sector}/entity/org.schema/Employee/_batch`
|
|
186
|
+
|
|
187
|
+
Use explicit cache keys by intent to avoid confusion:
|
|
188
|
+
- `controller_identity_token:*` for auth/token operations
|
|
189
|
+
- `controller_entity_token:*` for entity/business operations
|
|
190
|
+
|
|
191
|
+
```ts
|
|
192
|
+
const controllerSmart = await client.requestSmartTokenSimple({
|
|
193
|
+
tenantId: sessionContext.tenantId,
|
|
194
|
+
jurisdiction: profileContext.jurisdiction,
|
|
195
|
+
sector: profileContext.sector,
|
|
196
|
+
idToken: sessionContext.controllerIdToken,
|
|
197
|
+
targetEndpoint: client.getEndpointId(
|
|
198
|
+
{ section: 'organization', format: 'org.hl7.fhir.r4', resourceType: 'Person', action: '_batch' },
|
|
199
|
+
sessionContext.controllerDidWeb,
|
|
200
|
+
),
|
|
201
|
+
scopes: [
|
|
202
|
+
'organization/Person.cruds',
|
|
203
|
+
'organization/Organization.cruds',
|
|
204
|
+
'organization/Consent.cruds',
|
|
205
|
+
],
|
|
206
|
+
});
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
## 7) Create newMember/practitioner employee (runtime, uses controller token)
|
|
210
|
+
|
|
211
|
+
```ts
|
|
212
|
+
const employee = await client.createOrganizationEmployee(
|
|
213
|
+
{ tenantId: sessionContext.tenantId, jurisdiction: profileContext.jurisdiction, sector: profileContext.sector },
|
|
214
|
+
{
|
|
215
|
+
employeeClaims: {
|
|
216
|
+
'@context': 'org.schema',
|
|
217
|
+
[ClaimsPersonSchemaorg.email]: sessionContext.newMemberEmailToInvite,
|
|
218
|
+
[ClaimsPersonSchemaorg.hasOccupation]: sessionContext.newMemberRoleCode,
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
);
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
## 8) Extract newMember activation code and hand off to practitioner flow
|
|
225
|
+
|
|
226
|
+
```ts
|
|
227
|
+
const newMemberActivationCode = client.getActivationCodeFromResponse(employee);
|
|
228
|
+
if (!newMemberActivationCode) throw new Error('newMember activation code not found');
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
## 9) If no license is available: create `licenseOrder` (separate from `initialOrder`)
|
|
232
|
+
|
|
233
|
+
When employee creation returns license-offer/gating instead of activation code:
|
|
234
|
+
|
|
235
|
+
1. Extract `licenseOfferId` from employee response (same `getOfferIdFromResponse(...)` helper).
|
|
236
|
+
2. Show offer in UI and request explicit user acceptance.
|
|
237
|
+
3. Submit a new order as `licenseOrder`.
|
|
238
|
+
4. Retry employee creation after successful `licenseOrder`.
|
|
239
|
+
|
|
240
|
+
```ts
|
|
241
|
+
const licenseOfferId = client.getOfferIdFromResponse(employee);
|
|
242
|
+
if (!licenseOfferId) throw new Error('License offer id missing');
|
|
243
|
+
|
|
244
|
+
// This is a LICENSE ORDER (runtime order), not the onboarding order.
|
|
245
|
+
const licenseOrder = await client.confirmLegalOrganizationOrderSimple({
|
|
246
|
+
offerId: licenseOfferId,
|
|
247
|
+
});
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
Authentication rules for this step:
|
|
251
|
+
- never reuse onboarding `vp_token` for runtime ordering
|
|
252
|
+
- use controller runtime identity (DCR already completed)
|
|
253
|
+
- use controller SMART token for protected runtime routes
|
|
254
|
+
|
|
255
|
+
Route intent:
|
|
256
|
+
- controller's onboarding activation and onboarding order for the legal organization: `/host/.../registry/...`
|
|
257
|
+
- controller's identity DCR/token flows are tenant-scoped for the legal organization but exposed as `/host/.../{tenantId}/identity/auth/...`
|
|
258
|
+
- employee business operations use tenant-scoped data routes: `/{tenantId}/.../entity/...` (or corresponding section)
|
|
259
|
+
|
|
260
|
+
Complete path examples:
|
|
261
|
+
(using `test-network` but `test` can be used for local development)
|
|
262
|
+
- activate organization:
|
|
263
|
+
`/host/cds-ES/v1/test-network/registry/org.schema/Organization/_activate`
|
|
264
|
+
- poll activate:
|
|
265
|
+
`/host/cds-ES/v1/test-network/registry/org.schema/Organization/_activate-response`
|
|
266
|
+
- initial onboarding order:
|
|
267
|
+
`/host/cds-ES/v1/test-network/registry/org.schema/Order/_batch`
|
|
268
|
+
- poll onboarding order:
|
|
269
|
+
`/host/cds-ES/v1/test-network/registry/org.schema/Order/_batch-response`
|
|
270
|
+
- controller/employee identity exchange (activation code -> initial token):
|
|
271
|
+
`/host/cds-ES/v1/health-care/{tenantId}/identity/auth/_exchange`
|
|
272
|
+
- controller's DCR (consumes the initial access token with scope 'dcr' as per OpenID DCR):
|
|
273
|
+
`/host/cds-ES/v1/health-care/{tenantId}/identity/auth/_dcr`
|
|
274
|
+
- controller's smart token for entity management:
|
|
275
|
+
`/host/cds-ES/v1/health-care/{tenantId}/identity/auth/_token`
|
|
276
|
+
- employee creation:
|
|
277
|
+
`/{tenantId}/cds-ES/v1/health-care/entity/org.schema/Employee/_batch`
|
|
278
|
+
|
|
279
|
+
Continue with:
|
|
280
|
+
- `PRACTITIONER_FLOW_STEP_BY_STEP.md`
|
|
281
|
+
|
|
282
|
+
Shared wallet derivation profile:
|
|
283
|
+
- `BACKEND_NODE_INTEGRATION.md` ("Deterministic Wallet Profile")
|
|
@@ -1,31 +1,55 @@
|
|
|
1
|
-
# Data Model Alignment (GW +
|
|
1
|
+
# Data Model Alignment (GW + ICA + DataConv + SDK)
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Reference alignment for SDK consumers and backend integrators.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## API Namespace Alignment
|
|
6
6
|
|
|
7
|
-
-
|
|
7
|
+
- Gateway onboarding and operational flows use `/host/...` with `auth`-based security (OIDC + smart token post-DCR).
|
|
8
|
+
- ICA flows use `/ica/...` namespace.
|
|
9
|
+
- DataConv flows use `/publisher/...` namespace.
|
|
10
|
+
- SDK helpers should abstract these prefixes so integrators do not rebuild paths manually.
|
|
11
|
+
|
|
12
|
+
## Business Sector vs Host Registry Sector
|
|
13
|
+
|
|
14
|
+
- Host onboarding/routing can use infrastructure/network sector (`HOST_REGISTRY_SECTOR`).
|
|
8
15
|
- Tenant runtime model uses business sector (`SECTOR`).
|
|
9
|
-
- Canonical tenant vault ID:
|
|
16
|
+
- Canonical tenant vault ID format:
|
|
10
17
|
- `<SECTOR>_<tenantId>`
|
|
11
18
|
- Example: `health-care_acme`
|
|
12
19
|
|
|
20
|
+
## Legal Organization Activation Inputs
|
|
21
|
+
|
|
22
|
+
- `vp_token` is the primary proof artifact.
|
|
23
|
+
- Organization VC can be extracted from the VP (`LegalOrganizationCredential` / `OrganizationCredential`).
|
|
24
|
+
- Representative VC is optional depending on policy, but contact/role claims are still required for controller bootstrap.
|
|
25
|
+
- Controller contact supports both:
|
|
26
|
+
- `org.schema.Person.email`
|
|
27
|
+
- `org.schema.Person.telephone`
|
|
28
|
+
- Do not document phone-only assumptions as general contract.
|
|
29
|
+
|
|
30
|
+
## Offer/Order Commercial Contract
|
|
31
|
+
|
|
32
|
+
- Offer/Order remains part of the onboarding business flow even when amount is `0`.
|
|
33
|
+
- SDK should expose helpers to:
|
|
34
|
+
- extract offer identifier from activation/orderable claims
|
|
35
|
+
- expose offer preview fields (amount, currency, description) for UI confirmation
|
|
36
|
+
- Backend should send Order acceptance explicitly using `Order.acceptedOffer.identifier`.
|
|
37
|
+
|
|
13
38
|
## Individual Organization Model
|
|
14
39
|
|
|
15
|
-
- Section: `<prefix>_individual
|
|
16
|
-
-
|
|
40
|
+
- Section naming: `<prefix>_individual`.
|
|
41
|
+
- Canonical individual org identifier: `org.schema.Organization.identifier.value` (UUID).
|
|
42
|
+
- Contact model is the same as legal org onboarding: email or telephone.
|
|
17
43
|
|
|
18
44
|
## Search Contract (`searchFamilyOrganization`)
|
|
19
45
|
|
|
20
46
|
- Input mapping:
|
|
21
47
|
- `controllerPhone` -> `org.schema.Organization.owner.telephone`
|
|
48
|
+
- `controllerEmail` -> `org.schema.Organization.owner.email`
|
|
22
49
|
- `usualname` -> `org.schema.Organization.alternateName`
|
|
23
50
|
- `birthDate` -> `org.schema.Organization.foundingDate` (optional)
|
|
24
51
|
|
|
25
|
-
## Voice
|
|
26
|
-
|
|
27
|
-
- Preferred claim for subject call target:
|
|
28
|
-
- `org.schema.Organization.telephone`
|
|
29
|
-
- Fallback claim:
|
|
30
|
-
- `org.schema.Organization.owner.telephone`
|
|
52
|
+
## Voice/Phone Use Case Notes
|
|
31
53
|
|
|
54
|
+
- Voice agents may prioritize `telephone` for call target resolution.
|
|
55
|
+
- Generic portal/web integrations must support both email and telephone identity channels.
|
|
@@ -11,6 +11,7 @@ SDK: `dataspace-client-sdk-node` (`DataspaceNodeClient`)
|
|
|
11
11
|
|
|
12
12
|
```ts
|
|
13
13
|
import { DataspaceNodeClient } from 'dataspace-client-sdk-node';
|
|
14
|
+
import { ClaimsPersonSchemaorg } from 'gdc-common-utils-ts/constants/schemaorg';
|
|
14
15
|
|
|
15
16
|
const client = new DataspaceNodeClient({
|
|
16
17
|
baseUrl: process.env.GW_BASE_URL!,
|
|
@@ -29,6 +30,7 @@ const ctx = {
|
|
|
29
30
|
Terminology note: `subject` names in methods/claims refer to the member (person/patient) orchestrated by a personal organization with controller/manager and associated members.
|
|
30
31
|
|
|
31
32
|
Method: `bootstrapSubjectOrganizationIndex(ctx, input)`
|
|
33
|
+
Test: [tests/client.test.mjs](/Users/fernando/GITS/gdc-workspace/dataspace-client-sdk-node/tests/client.test.mjs), [tests/uc5-subject-data.flow.test.mjs](/Users/fernando/GITS/gdc-workspace/dataspace-client-sdk-node/tests/uc5-subject-data.flow.test.mjs)
|
|
32
34
|
|
|
33
35
|
```ts
|
|
34
36
|
const result = await client.bootstrapSubjectOrganizationIndex(ctx, {
|
|
@@ -48,6 +50,7 @@ const result = await client.bootstrapSubjectOrganizationIndex(ctx, {
|
|
|
48
50
|
## UC5.2 Legal organization activation in GW (from ICA proof)
|
|
49
51
|
|
|
50
52
|
Method: `activateOrganizationInGatewayFromIcaProof(hostCtx, input)`
|
|
53
|
+
Test: [tests/client.test.mjs](/Users/fernando/GITS/gdc-workspace/dataspace-client-sdk-node/tests/client.test.mjs), [tests/uc5-org-onboarding.flow.test.mjs](/Users/fernando/GITS/gdc-workspace/dataspace-client-sdk-node/tests/uc5-org-onboarding.flow.test.mjs)
|
|
51
54
|
|
|
52
55
|
```ts
|
|
53
56
|
const activation = await client.activateOrganizationInGatewayFromIcaProof(
|
|
@@ -64,13 +67,14 @@ const activation = await client.activateOrganizationInGatewayFromIcaProof(
|
|
|
64
67
|
## UC5.3 Create employee / professional license
|
|
65
68
|
|
|
66
69
|
Method: `createOrganizationEmployee(ctx, input)`
|
|
70
|
+
Test: [tests/client.test.mjs](/Users/fernando/GITS/gdc-workspace/dataspace-client-sdk-node/tests/client.test.mjs), [tests/uc5-org-onboarding.flow.test.mjs](/Users/fernando/GITS/gdc-workspace/dataspace-client-sdk-node/tests/uc5-org-onboarding.flow.test.mjs)
|
|
67
71
|
|
|
68
72
|
```ts
|
|
69
73
|
await client.createOrganizationEmployee(ctx, {
|
|
70
74
|
employeeClaims: {
|
|
71
75
|
'@context': 'org.schema',
|
|
72
|
-
|
|
73
|
-
|
|
76
|
+
[ClaimsPersonSchemaorg.email]: 'doctor@example.com',
|
|
77
|
+
[ClaimsPersonSchemaorg.hasOccupation]: 'ISCO-08|2211',
|
|
74
78
|
},
|
|
75
79
|
});
|
|
76
80
|
```
|
|
@@ -78,6 +82,7 @@ await client.createOrganizationEmployee(ctx, {
|
|
|
78
82
|
## UC5.4 Activate employee device
|
|
79
83
|
|
|
80
84
|
Method: `activateEmployeeDeviceWithActivationCode(ctx, input)`
|
|
85
|
+
Test: [tests/client.test.mjs](/Users/fernando/GITS/gdc-workspace/dataspace-client-sdk-node/tests/client.test.mjs), [tests/uc5-org-onboarding.flow.test.mjs](/Users/fernando/GITS/gdc-workspace/dataspace-client-sdk-node/tests/uc5-org-onboarding.flow.test.mjs)
|
|
81
86
|
|
|
82
87
|
```ts
|
|
83
88
|
const device = await client.activateEmployeeDeviceWithActivationCode(ctx, {
|
|
@@ -94,6 +99,7 @@ const device = await client.activateEmployeeDeviceWithActivationCode(ctx, {
|
|
|
94
99
|
## UC5.5 Import IPS/FHIR and update index
|
|
95
100
|
|
|
96
101
|
Method: `importIpsOrFhirAndUpdateIndex(ctx, input)`
|
|
102
|
+
Test: [tests/client.test.mjs](/Users/fernando/GITS/gdc-workspace/dataspace-client-sdk-node/tests/client.test.mjs), [tests/uc5-subject-data.flow.test.mjs](/Users/fernando/GITS/gdc-workspace/dataspace-client-sdk-node/tests/uc5-subject-data.flow.test.mjs)
|
|
97
103
|
|
|
98
104
|
```ts
|
|
99
105
|
await client.importIpsOrFhirAndUpdateIndex(ctx, {
|
|
@@ -110,6 +116,7 @@ await client.importIpsOrFhirAndUpdateIndex(ctx, {
|
|
|
110
116
|
|
|
111
117
|
Step 1 method: `grantProfessionalAccessSimple(ctx, input)`
|
|
112
118
|
Step 2 method: `requestSmartToken(input)`
|
|
119
|
+
Test: [tests/client.test.mjs](/Users/fernando/GITS/gdc-workspace/dataspace-client-sdk-node/tests/client.test.mjs), [tests/uc5-subject-data.flow.test.mjs](/Users/fernando/GITS/gdc-workspace/dataspace-client-sdk-node/tests/uc5-subject-data.flow.test.mjs)
|
|
113
120
|
|
|
114
121
|
Domain note: the protected context is modeled as organization (including personal organizations with controller + subject/person/patient members).
|
|
115
122
|
|
|
@@ -138,6 +145,7 @@ const token = await client.requestSmartToken({
|
|
|
138
145
|
## UC5.7 Generate digital twin
|
|
139
146
|
|
|
140
147
|
Method: `generateDigitalTwinFromSubjectData(ctx, input)`
|
|
148
|
+
Test: [tests/client.test.mjs](/Users/fernando/GITS/gdc-workspace/dataspace-client-sdk-node/tests/client.test.mjs), [tests/uc5-subject-data.flow.test.mjs](/Users/fernando/GITS/gdc-workspace/dataspace-client-sdk-node/tests/uc5-subject-data.flow.test.mjs)
|
|
141
149
|
|
|
142
150
|
```ts
|
|
143
151
|
await client.generateDigitalTwinFromSubjectData(ctx, {
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# E2E Local GW UC5 (Reproducible, No Mocks)
|
|
2
|
+
|
|
3
|
+
This test runs a real UC5 chain against a locally running GW in demo mode:
|
|
4
|
+
|
|
5
|
+
1. Legal entity controller activates tenant (`Organization/_activate`).
|
|
6
|
+
2. Individual controller bootstraps personal/individual organization (`Organization/_batch` + `Order/_batch`).
|
|
7
|
+
3. Consent is created (`Consent/_batch`) with `organization/Composition.rs`.
|
|
8
|
+
4. Professional requests SMART token and receives scoped token.
|
|
9
|
+
|
|
10
|
+
Test file:
|
|
11
|
+
- [live-gw-uc5.e2e.test.mjs](/Users/fernando/GITS/gdc-workspace/dataspace-client-sdk-node/tests/live-gw-uc5.e2e.test.mjs)
|
|
12
|
+
|
|
13
|
+
## Prerequisites
|
|
14
|
+
|
|
15
|
+
1. Start GW local in demo mode:
|
|
16
|
+
```bash
|
|
17
|
+
npm -C /Users/fernando/GITS/gdc-workspace/gwtemplate-node-ts run api:local-demo
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
2. Provide ICA proof as either:
|
|
21
|
+
- `VP_TOKEN` (preferred, real signed VP), or
|
|
22
|
+
- `VP_TOKEN_FILE` pointing to a minimal fixture payload.
|
|
23
|
+
|
|
24
|
+
Default fixture included:
|
|
25
|
+
- [ica-vp-minimal.json](/Users/fernando/GITS/gdc-workspace/dataspace-client-sdk-node/tests/fixtures/ica-vp-minimal.json)
|
|
26
|
+
- Built at runtime into unsigned compact JWT only for local demo reproducibility.
|
|
27
|
+
|
|
28
|
+
3. Use a tenant id aligned with your activation proof (`TENANT_ID`).
|
|
29
|
+
|
|
30
|
+
## Run
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
cd /Users/fernando/GITS/gdc-workspace/dataspace-client-sdk-node
|
|
34
|
+
BASE_URL=http://127.0.0.1:3000 \
|
|
35
|
+
VP_TOKEN_FILE=./tests/fixtures/ica-vp-minimal.json \
|
|
36
|
+
TENANT_ID=VATES-B00000000 \
|
|
37
|
+
JURISDICTION=ES \
|
|
38
|
+
SECTOR=health-care \
|
|
39
|
+
HOST_REGISTRY_SECTOR=test \
|
|
40
|
+
PROFESSIONAL_ID_TOKEN='eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiJwcm9mZXNzaW9uYWwifQ.demo' \
|
|
41
|
+
npm run test:e2e:live-gw-uc5
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Notes
|
|
45
|
+
|
|
46
|
+
- This is a real integration test (no `fetch` mocks).
|
|
47
|
+
- `_activate` in this E2E uses `vp_token` in payload (no Bearer header required by the test).
|
|
48
|
+
- If `TENANT_ID` does not match activation proof context, downstream tenant-scoped steps can return `404`.
|
|
49
|
+
- The scripted scope assertion verifies `organization/Composition.rs` is granted in SMART token response.
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# ENDPOINT_ID_CATALOG
|
|
2
|
+
|
|
3
|
+
Canonical endpoint-id construction for token cache/session reuse.
|
|
4
|
+
|
|
5
|
+
Use `client.getEndpointId(selector, providerDid?)` and avoid ad-hoc strings.
|
|
6
|
+
|
|
7
|
+
Selector shape:
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
type EndpointSelector = {
|
|
11
|
+
section: string;
|
|
12
|
+
format: string;
|
|
13
|
+
resourceType: string;
|
|
14
|
+
action: string;
|
|
15
|
+
};
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Generation rule:
|
|
19
|
+
- With `providerDid`: `did:web:...#section:format:resourceType:action`
|
|
20
|
+
- Without `providerDid`: `section:format:resourceType:action`
|
|
21
|
+
|
|
22
|
+
## Recommended selectors
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
export const ENDPOINT_SELECTORS = {
|
|
26
|
+
ORG_COMPOSITION_SEARCH: {
|
|
27
|
+
section: 'organization',
|
|
28
|
+
format: 'org.hl7.fhir.r4',
|
|
29
|
+
resourceType: 'Composition',
|
|
30
|
+
action: '_search',
|
|
31
|
+
},
|
|
32
|
+
INDIVIDUAL_CONSENT_BATCH: {
|
|
33
|
+
section: 'individual',
|
|
34
|
+
format: 'org.hl7.fhir.r4',
|
|
35
|
+
resourceType: 'Consent',
|
|
36
|
+
action: '_batch',
|
|
37
|
+
},
|
|
38
|
+
INDIVIDUAL_COMMUNICATION_BATCH: {
|
|
39
|
+
section: 'individual',
|
|
40
|
+
format: 'org.hl7.fhir.r4',
|
|
41
|
+
resourceType: 'Communication',
|
|
42
|
+
action: '_batch',
|
|
43
|
+
},
|
|
44
|
+
INDIVIDUAL_DOCUMENTREFERENCE_BATCH: {
|
|
45
|
+
section: 'individual',
|
|
46
|
+
format: 'org.hl7.fhir.r4',
|
|
47
|
+
resourceType: 'DocumentReference',
|
|
48
|
+
action: '_batch',
|
|
49
|
+
},
|
|
50
|
+
IDENTITY_AUTH_TOKEN: {
|
|
51
|
+
section: 'identity',
|
|
52
|
+
format: 'auth',
|
|
53
|
+
resourceType: 'token',
|
|
54
|
+
action: '_token',
|
|
55
|
+
},
|
|
56
|
+
IDENTITY_AUTH_DCR: {
|
|
57
|
+
section: 'identity',
|
|
58
|
+
format: 'auth',
|
|
59
|
+
resourceType: 'device',
|
|
60
|
+
action: '_dcr',
|
|
61
|
+
},
|
|
62
|
+
IDENTITY_AUTH_EXCHANGE: {
|
|
63
|
+
section: 'identity',
|
|
64
|
+
format: 'auth',
|
|
65
|
+
resourceType: 'token',
|
|
66
|
+
action: '_exchange',
|
|
67
|
+
},
|
|
68
|
+
} as const;
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Example
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
const targetEndpoint = client.getEndpointId(
|
|
75
|
+
ENDPOINT_SELECTORS.ORG_COMPOSITION_SEARCH,
|
|
76
|
+
ORG_CONTROLLER_DID_WEB,
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
const smart = await client.requestSmartTokenSimple({
|
|
80
|
+
idToken,
|
|
81
|
+
targetEndpoint,
|
|
82
|
+
scopes: ['organization/Composition.rs'],
|
|
83
|
+
});
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Alignment policy
|
|
87
|
+
|
|
88
|
+
- Prefer current namespaces and flows: `/host`, `/ica`, `/publisher`, and GW runtime `identity/auth`.
|
|
89
|
+
- Do not introduce new integrations based on legacy aliases.
|
|
90
|
+
- Keep endpoint-id selectors stable and explicit across Node + frontend SDKs.
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# LEGAL_ORGANIZATION_FLOW_STEP_BY_STEP
|
|
2
|
+
|
|
3
|
+
Canonical legal-organization flow index.
|
|
4
|
+
This file avoids duplicating controller/practitioner details and points to the source guides.
|
|
5
|
+
|
|
6
|
+
## Mandatory rules for integrators
|
|
7
|
+
|
|
8
|
+
Security planes:
|
|
9
|
+
- Transport plane: backend ↔ gateway channel protection (deployment-specific).
|
|
10
|
+
- Identity/business plane: user/controller/member authentication and authorization.
|
|
11
|
+
- Operator/hosting plane: infrastructure/operator lifecycle.
|
|
12
|
+
|
|
13
|
+
Keep these planes separated; do not treat transport credentials as user identity tokens.
|
|
14
|
+
|
|
15
|
+
Secure messaging note:
|
|
16
|
+
- Authentication for controller/member actions is identity-plane (`vp_token`, `idToken`, SMART/client_assertion).
|
|
17
|
+
- Backend-to-service P2P messages can additionally be signed/encrypted (embedded JWS/JWE) according to deployment communication mode.
|
|
18
|
+
|
|
19
|
+
Wallet profile:
|
|
20
|
+
- deterministic key derivation/profile rules are centralized in `BACKEND_NODE_INTEGRATION.md` ("Deterministic Wallet Profile").
|
|
21
|
+
- communication mode (`plain` / `strict` / `auto-detect`) is centralized in `BACKEND_NODE_INTEGRATION.md`.
|
|
22
|
+
|
|
23
|
+
- `jurisdiction` (country) is required in every step.
|
|
24
|
+
- Professional tenants are registered in one jurisdiction and all routes resolve against it.
|
|
25
|
+
- Identity auth routes and business/entity routes are different:
|
|
26
|
+
- identity: `/host/cds-{jurisdiction}/v1/{sector}/{tenantId}/identity/auth/...`
|
|
27
|
+
- business: `/{tenantId}/cds-{jurisdiction}/v1/{sector}/...`
|
|
28
|
+
- `vp_token` is for onboarding proof (`_activate`), not for runtime calls.
|
|
29
|
+
- Controller DCR/token and practitioner DCR/token are distinct flows.
|
|
30
|
+
|
|
31
|
+
## Runtime context pattern (high-level)
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
const profileContext = {
|
|
35
|
+
baseUrl: process.env.BASE_URL!,
|
|
36
|
+
jurisdiction: process.env.JURISDICTION!, // REQUIRED
|
|
37
|
+
sector: process.env.SECTOR || 'health-care',
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const sessionContext = {
|
|
41
|
+
tenantId: currentSession.tenantId,
|
|
42
|
+
controller: currentSession.controller,
|
|
43
|
+
practitioner: currentSession.practitioner,
|
|
44
|
+
};
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Use `profileContext` for role/organization runtime context and `sessionContext` for logged-in user/session data.
|
|
48
|
+
|
|
49
|
+
## Phase A: controller flow (authoritative)
|
|
50
|
+
|
|
51
|
+
1. Build/sign VP token (`nonce`, no `jti` in VP for this flow).
|
|
52
|
+
2. `_activate` organization in host registry.
|
|
53
|
+
3. Read offer and explicit user acceptance.
|
|
54
|
+
4. Submit `initialOrder`.
|
|
55
|
+
5. Controller DCR bootstrap (`_exchange` then `_dcr`).
|
|
56
|
+
6. Request controller runtime token (`_token`).
|
|
57
|
+
7. Create employee/member in entity route.
|
|
58
|
+
8. Extract activation code for practitioner.
|
|
59
|
+
9. If license seats are exhausted: receive `Employee-license-offer-v1.0`, accept, submit `licenseOrder`, retry employee create.
|
|
60
|
+
|
|
61
|
+
Detailed guide (single source of truth):
|
|
62
|
+
- `CONTROLLER_FLOW_STEP_BY_STEP.md`
|
|
63
|
+
|
|
64
|
+
## Phase B: practitioner flow (authoritative)
|
|
65
|
+
|
|
66
|
+
1. Receive practitioner activation code from controller flow.
|
|
67
|
+
2. Practitioner DCR bootstrap (`_exchange` then `_dcr`).
|
|
68
|
+
3. Request practitioner SMART/runtime token (`_token`) with required scopes.
|
|
69
|
+
4. Call protected organization/business endpoints.
|
|
70
|
+
|
|
71
|
+
Detailed guide (single source of truth):
|
|
72
|
+
- `PRACTITIONER_FLOW_STEP_BY_STEP.md`
|
|
73
|
+
|
|
74
|
+
## Scope of this file
|
|
75
|
+
|
|
76
|
+
- Keep this file as top-level legal organization map.
|
|
77
|
+
- Put implementation details in:
|
|
78
|
+
- `CONTROLLER_FLOW_STEP_BY_STEP.md`
|
|
79
|
+
- `PRACTITIONER_FLOW_STEP_BY_STEP.md`
|
|
80
|
+
- `BACKEND_NODE_INTEGRATION.md`
|
|
81
|
+
|
|
82
|
+
Related references:
|
|
83
|
+
- `ENDPOINT_ID_CATALOG.md`
|
|
84
|
+
- `DATA_MODEL_ALIGNMENT.md`
|