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,70 @@
|
|
|
1
|
+
# PERSONAL_FLOW_STEP_BY_STEP
|
|
2
|
+
|
|
3
|
+
Canonical flow for personal/family onboarding and consented access.
|
|
4
|
+
|
|
5
|
+
Security note:
|
|
6
|
+
- User authentication/authorization (id-token + scopes + consent) is independent from transport/message protection.
|
|
7
|
+
- Backend may enforce DIDComm message security (JWS/JWE) per deployment policy: `plain` / `strict` / `auto-detect`.
|
|
8
|
+
|
|
9
|
+
## 1) Start individual/family organization onboarding
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
const started = await client.startIndividualOrganizationSimple({
|
|
13
|
+
alternateName,
|
|
14
|
+
controllerEmail, // or controllerTelephone
|
|
15
|
+
controllerRole: 'org.hl7.v3.RoleCode|RESPRSN',
|
|
16
|
+
});
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## 2) Show offer in UI and accept
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
const offerId = started.offerId;
|
|
23
|
+
const offerPreview = started.offerPreview;
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## 3) Confirm order (always)
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
const order = await client.confirmIndividualOrganizationOrderSimple({ offerId: offerId! });
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## 4) Configure consent/preauthorization
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
const consent = await client.grantProfessionalAccessSimple(
|
|
36
|
+
{ tenantId, jurisdiction, sector },
|
|
37
|
+
{
|
|
38
|
+
subjectDid,
|
|
39
|
+
actor: { identifier: practitionerDid },
|
|
40
|
+
actorRole: 'Practitioner',
|
|
41
|
+
purpose: 'TREAT',
|
|
42
|
+
actions: ['organization/Composition.rs'],
|
|
43
|
+
},
|
|
44
|
+
);
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## 5) Optional: import/update index data
|
|
48
|
+
|
|
49
|
+
Use your ingestion path (`/publisher/...`) and then subject-side runtime operations.
|
|
50
|
+
|
|
51
|
+
## 6) Professional requests token and reads allowed resources
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
const smart = await client.requestSmartTokenSimple({
|
|
55
|
+
idToken: practitionerIdToken,
|
|
56
|
+
targetEndpoint: client.getEndpointId({
|
|
57
|
+
section: 'organization',
|
|
58
|
+
format: 'org.hl7.fhir.r4',
|
|
59
|
+
resourceType: 'Composition',
|
|
60
|
+
action: '_search',
|
|
61
|
+
}, practitionerDid),
|
|
62
|
+
scopes: ['organization/Composition.rs'],
|
|
63
|
+
});
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Notes
|
|
67
|
+
|
|
68
|
+
- For personal onboarding in GW, verification is integrated in the onboarding flow (`individual/org.schema/Organization/_batch` + `Order/_batch`).
|
|
69
|
+
- Keep Offer/Order UX explicit even when amount is `0`.
|
|
70
|
+
- Use endpoint-id selectors from `docs/ENDPOINT_ID_CATALOG.md`.
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
# Portal Backend Integration Handover
|
|
2
|
+
|
|
3
|
+
Target audience: team implementing a portal backend that integrates with GW via `dataspace-client-sdk-node`.
|
|
4
|
+
|
|
5
|
+
## 1) What this SDK does vs does not do
|
|
6
|
+
|
|
7
|
+
`dataspace-client-sdk-node` does:
|
|
8
|
+
- build GW paths
|
|
9
|
+
- submit/poll async DIDComm plain flows
|
|
10
|
+
- provide high-level wrappers for common UC5 steps
|
|
11
|
+
|
|
12
|
+
`dataspace-client-sdk-node` does not do:
|
|
13
|
+
- ICA UX orchestration in browser
|
|
14
|
+
- wallet UX/signing UX for end-users
|
|
15
|
+
- generic OIDC4VP UI flow control
|
|
16
|
+
|
|
17
|
+
`vp_token` source:
|
|
18
|
+
- produced by your identity/wallet flow (usually ICA/OIDC4VP side)
|
|
19
|
+
- then passed to backend and used in GW `_activate`
|
|
20
|
+
|
|
21
|
+
## 2) Responsibility split
|
|
22
|
+
|
|
23
|
+
1. Portal frontend:
|
|
24
|
+
- UX, contract signature flow, identity/wallet flow
|
|
25
|
+
- obtains `vpToken`
|
|
26
|
+
- sends onboarding payloads to portal backend
|
|
27
|
+
2. Portal backend (this integration):
|
|
28
|
+
- uses this SDK to call GW
|
|
29
|
+
- executes end-to-end business order and polling
|
|
30
|
+
3. GW backend:
|
|
31
|
+
- validates proofs, processes onboarding, authorization and data access contracts
|
|
32
|
+
|
|
33
|
+
### Recommended SDK initialization
|
|
34
|
+
|
|
35
|
+
Initialize once with default context to avoid repeating `tenantId/jurisdiction/sector`:
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
const client = new DataspaceNodeClient({
|
|
39
|
+
baseUrl,
|
|
40
|
+
bearerToken,
|
|
41
|
+
ctx: { tenantId, jurisdiction, sector },
|
|
42
|
+
});
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## 3) Legal organization onboarding (complete, numbered)
|
|
46
|
+
|
|
47
|
+
Canonical order:
|
|
48
|
+
`_verify -> _activate -> Offer -> Order -> DCR -> token`
|
|
49
|
+
|
|
50
|
+
### Step 1. Frontend obtains `vpToken`
|
|
51
|
+
|
|
52
|
+
Expected backend input:
|
|
53
|
+
- `jurisdiction` (required)
|
|
54
|
+
- `sector` (required)
|
|
55
|
+
- `vpToken` (required)
|
|
56
|
+
- `numberOfMembers` (optional, generic; default `2`)
|
|
57
|
+
|
|
58
|
+
Notes:
|
|
59
|
+
- `jurisdiction` is explicit route context. It is not inferred from VAT.
|
|
60
|
+
- `sector` is explicit route context. It is not inferred from DID.
|
|
61
|
+
|
|
62
|
+
Set once in client:
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
client.setContextOrg({ tenantId, jurisdiction, sector });
|
|
66
|
+
client.setDefaultTimeoutSeconds(120);
|
|
67
|
+
client.setDefaultIntervalSeconds(2);
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Step 2. Backend activates organization in GW
|
|
71
|
+
|
|
72
|
+
Preferred SDK method (friendly):
|
|
73
|
+
- `activateOrganizationInGatewaySimple(...)`
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
const activation = await client.activateOrganizationInGatewaySimple({
|
|
77
|
+
vpToken,
|
|
78
|
+
serviceProviderDidWeb, // or serviceProviderUrl (SDK maps URL -> did:web:<host>)
|
|
79
|
+
controllerEmail, // or controllerTelephone
|
|
80
|
+
controllerRole, // e.g. ISCO-08|1112
|
|
81
|
+
numberOfMembers, // optional, default 2
|
|
82
|
+
});
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Advanced equivalent (less friendly, same behavior):
|
|
86
|
+
- `activateOrganizationInGatewayFromIcaProof(ctx, input, pollOptions)`
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
const activation = await client.activateOrganizationInGatewayFromIcaProof(
|
|
90
|
+
{ jurisdiction, sector },
|
|
91
|
+
{
|
|
92
|
+
vpToken,
|
|
93
|
+
numberOfMembers,
|
|
94
|
+
},
|
|
95
|
+
{ timeoutMs: 120000, intervalMs: 2000 },
|
|
96
|
+
);
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Default behavior:
|
|
100
|
+
- if `numberOfMembers` is not provided, SDK defaults to `2`
|
|
101
|
+
(controller + one operational employee).
|
|
102
|
+
|
|
103
|
+
### Step 3. Backend extracts `offerId` from `_activate` response
|
|
104
|
+
|
|
105
|
+
Use SDK helper:
|
|
106
|
+
- `client.getOfferIdFromResponse(activation)`
|
|
107
|
+
|
|
108
|
+
Extract offer preview fields from the first DIDComm entry via:
|
|
109
|
+
- `client.getFirstDidcommDataEntryFromResponse(activation)`
|
|
110
|
+
- or directly as UI object: `client.getOfferPreviewFromResponse(activation)`
|
|
111
|
+
|
|
112
|
+
Expected offer claims (GW examples/contracts):
|
|
113
|
+
- `org.schema.Offer.identifier`
|
|
114
|
+
- `org.schema.Offer.price`
|
|
115
|
+
- `org.schema.Offer.priceCurrency`
|
|
116
|
+
- `org.schema.Offer.eligibleQuantity.value`
|
|
117
|
+
- `org.schema.Offer.itemOffered.name`
|
|
118
|
+
- `org.schema.Offer.itemOffered.sku`
|
|
119
|
+
- `org.schema.Offer.acceptedPaymentMethod`
|
|
120
|
+
- `org.schema.Offer.checkoutPageURLTemplate`
|
|
121
|
+
|
|
122
|
+
Recommended UI mapping helper:
|
|
123
|
+
- `client.getOfferPreviewFromResponse(activation)` ->
|
|
124
|
+
`{ offerId, amount, currency, seats, planName, sku, paymentMethod, checkoutUrl }`
|
|
125
|
+
|
|
126
|
+
### Step 4. Backend executes legal organization order (always)
|
|
127
|
+
|
|
128
|
+
Even with `0` amount, Order is still required.
|
|
129
|
+
|
|
130
|
+
Preferred SDK method (friendly):
|
|
131
|
+
- `confirmLegalOrganizationOrderSimple(...)`
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
const legalOrgOrder = await client.confirmLegalOrganizationOrderSimple({
|
|
135
|
+
offerId,
|
|
136
|
+
});
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Advanced equivalent (less friendly):
|
|
140
|
+
- `hostRegistryOrderBatchPath(...)`
|
|
141
|
+
- `hostRegistryOrderPollPath(...)`
|
|
142
|
+
- `submitAndPoll(...)`
|
|
143
|
+
|
|
144
|
+
Example:
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
const offerId = client.getOfferIdFromResponse(activation);
|
|
148
|
+
if (!offerId) throw new Error('Offer id missing in activation response.');
|
|
149
|
+
|
|
150
|
+
const legalOrgOrder = await client.submitAndPoll(
|
|
151
|
+
client.hostRegistryOrderBatchPath({ jurisdiction, sector }),
|
|
152
|
+
client.hostRegistryOrderPollPath({ jurisdiction, sector }),
|
|
153
|
+
{
|
|
154
|
+
thid: `order-${Date.now()}`,
|
|
155
|
+
body: {
|
|
156
|
+
data: [{
|
|
157
|
+
type: 'Organization-order-request-v1.0',
|
|
158
|
+
meta: { claims: { 'Order.acceptedOffer.identifier': offerId } },
|
|
159
|
+
}],
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
);
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### Step 4.1 UX requirement: show offer summary before order
|
|
166
|
+
|
|
167
|
+
Portal UX should present:
|
|
168
|
+
1. Offer identifier
|
|
169
|
+
2. Total amount and currency (if provided)
|
|
170
|
+
3. Product/license summary
|
|
171
|
+
4. Acceptance action (user confirms, then backend sends Order)
|
|
172
|
+
|
|
173
|
+
Even if amount is `0`, user acceptance and Order submission still happen.
|
|
174
|
+
|
|
175
|
+
### Step 5. Backend creates doctor employee
|
|
176
|
+
|
|
177
|
+
SDK method:
|
|
178
|
+
- `createOrganizationEmployee(...)`
|
|
179
|
+
|
|
180
|
+
Use claim constants:
|
|
181
|
+
- `ClaimsPersonSchemaorg` from `gdc-common-utils-ts/constants/schemaorg`
|
|
182
|
+
|
|
183
|
+
```ts
|
|
184
|
+
const employee = await client.createOrganizationEmployee(
|
|
185
|
+
{ tenantId, jurisdiction, sector },
|
|
186
|
+
{
|
|
187
|
+
employeeClaims: {
|
|
188
|
+
'@context': 'org.schema',
|
|
189
|
+
[ClaimsPersonSchemaorg.email]: 'doctor@example.com',
|
|
190
|
+
[ClaimsPersonSchemaorg.hasOccupation]: 'ISCO-08|2211',
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
);
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### Step 6. Backend activates doctor device (DCR path)
|
|
197
|
+
|
|
198
|
+
SDK method:
|
|
199
|
+
- `activateEmployeeDeviceWithActivationCodeSimple(...)` (recommended)
|
|
200
|
+
- `activateEmployeeDeviceWithActivationCode(...)` (advanced)
|
|
201
|
+
|
|
202
|
+
```ts
|
|
203
|
+
const device = await client.activateEmployeeDeviceWithActivationCodeSimple({
|
|
204
|
+
activationCode,
|
|
205
|
+
idToken: doctorUserIdToken,
|
|
206
|
+
dcrPayload: {
|
|
207
|
+
application_type: 'web',
|
|
208
|
+
token_endpoint_auth_method: 'private_key_jwt',
|
|
209
|
+
jwks: { keys: [doctorPublicJwk] },
|
|
210
|
+
},
|
|
211
|
+
});
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### Step 7. Backend obtains SMART token for authorized access
|
|
215
|
+
|
|
216
|
+
SDK method:
|
|
217
|
+
- `requestSmartTokenSimple(...)` (recommended)
|
|
218
|
+
- `requestSmartToken(...)` (advanced)
|
|
219
|
+
|
|
220
|
+
```ts
|
|
221
|
+
const smart = await client.requestSmartTokenSimple({
|
|
222
|
+
idToken: doctorUserIdToken,
|
|
223
|
+
targetEndpoint: client.getEndpointId({
|
|
224
|
+
section: 'organization',
|
|
225
|
+
format: 'org.hl7.fhir.r4',
|
|
226
|
+
resourceType: 'Composition',
|
|
227
|
+
action: '_search',
|
|
228
|
+
}, doctorDidWeb),
|
|
229
|
+
scopes: ['organization/Composition.rs'],
|
|
230
|
+
});
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
## 4) Individual subject onboarding + access grant (doctor reads index)
|
|
234
|
+
|
|
235
|
+
### Step 8. Register personal organization + family order
|
|
236
|
+
|
|
237
|
+
SDK method:
|
|
238
|
+
- `bootstrapSubjectOrganizationIndex(...)`
|
|
239
|
+
|
|
240
|
+
```ts
|
|
241
|
+
const subjectBootstrap = await client.bootstrapSubjectOrganizationIndex(
|
|
242
|
+
{ tenantId, jurisdiction, sector },
|
|
243
|
+
{
|
|
244
|
+
registrationPayload,
|
|
245
|
+
confirmationPayload, // include accepted offer id
|
|
246
|
+
},
|
|
247
|
+
);
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
### Step 9. Grant doctor consent to read subject index/data
|
|
251
|
+
|
|
252
|
+
SDK method:
|
|
253
|
+
- `grantProfessionalAccessSimple(...)`
|
|
254
|
+
|
|
255
|
+
```ts
|
|
256
|
+
await client.grantProfessionalAccessSimple(
|
|
257
|
+
{ tenantId, jurisdiction, sector },
|
|
258
|
+
{
|
|
259
|
+
subjectDid: subjectDidWeb,
|
|
260
|
+
actor: { identifier: doctorDidWeb },
|
|
261
|
+
actorRole: 'Practitioner',
|
|
262
|
+
purpose: 'TREAT',
|
|
263
|
+
actions: ['organization/Composition.rs'],
|
|
264
|
+
},
|
|
265
|
+
);
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
### Step 10. Doctor uses SMART token to read permitted resources
|
|
269
|
+
|
|
270
|
+
Use bearer returned by step 7 in subsequent resource calls.
|
|
271
|
+
|
|
272
|
+
## 5) Minimal backend request contracts from frontend
|
|
273
|
+
|
|
274
|
+
Legal activate request:
|
|
275
|
+
|
|
276
|
+
```json
|
|
277
|
+
{
|
|
278
|
+
"jurisdiction": "ES",
|
|
279
|
+
"sector": "health-care",
|
|
280
|
+
"vpToken": "<vp_token>"
|
|
281
|
+
}
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
Personal register request:
|
|
285
|
+
|
|
286
|
+
```json
|
|
287
|
+
{
|
|
288
|
+
"tenantId": "acme",
|
|
289
|
+
"jurisdiction": "ES",
|
|
290
|
+
"sector": "health-care",
|
|
291
|
+
"registrationPayload": {},
|
|
292
|
+
"confirmationPayload": {}
|
|
293
|
+
}
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
## 6) Primary SDK methods checklist
|
|
297
|
+
|
|
298
|
+
1. `activateOrganizationInGatewayFromIcaProof`
|
|
299
|
+
2. extract `offerId` from activation response
|
|
300
|
+
3. `submitAndPoll` + `hostRegistryOrderBatchPath` / `hostRegistryOrderPollPath`
|
|
301
|
+
4. `createOrganizationEmployee`
|
|
302
|
+
5. `activateEmployeeDeviceWithActivationCodeSimple`
|
|
303
|
+
6. `requestSmartTokenSimple`
|
|
304
|
+
7. `bootstrapSubjectOrganizationIndex`
|
|
305
|
+
8. `grantProfessionalAccessSimple`
|
|
306
|
+
|
|
307
|
+
## 7) Async/poll UX pattern (important)
|
|
308
|
+
|
|
309
|
+
All onboarding operations are async (`submit` + `poll`).
|
|
310
|
+
|
|
311
|
+
Recommended backend pattern per step:
|
|
312
|
+
1. Emit status `submitted` after POST returns `202`.
|
|
313
|
+
2. Poll until completion.
|
|
314
|
+
3. Emit status `completed` or `failed` with diagnostics.
|
|
315
|
+
|
|
316
|
+
Polling interval behavior:
|
|
317
|
+
- if caller sets `intervalMs`, that value is forced.
|
|
318
|
+
- if caller does not set `intervalMs`, SDK uses backend `Retry-After` when present.
|
|
319
|
+
- fallback default is `2000ms`.
|
|
320
|
+
|
|
321
|
+
Recommended portal UX states:
|
|
322
|
+
1. `Verifying identity proof`
|
|
323
|
+
2. `Activating organization`
|
|
324
|
+
3. `Offer available` (show price/summary)
|
|
325
|
+
4. `Waiting for acceptance`
|
|
326
|
+
5. `Submitting order`
|
|
327
|
+
6. `Creating employee`
|
|
328
|
+
7. `Activating device`
|
|
329
|
+
8. `Token ready`
|
|
330
|
+
|
|
331
|
+
Implementation note:
|
|
332
|
+
- If you need real-time UX updates, expose backend progress via SSE/WebSocket.
|
|
333
|
+
- If not, frontend can poll your backend orchestration endpoint for step status.
|
|
334
|
+
|
|
335
|
+
## 8) References
|
|
336
|
+
|
|
337
|
+
- `docs/BACKEND_NODE_INTEGRATION.md`
|
|
338
|
+
- `docs/REACT_WEB_INTEGRATION.md`
|
|
339
|
+
- `docs/DEVELOPER_USE_CASES.md`
|
|
340
|
+
- `docs/API.md`
|
|
341
|
+
- `examples/e2e-bootstrap-tenant.mjs`
|
|
342
|
+
- `tests/uc5-org-onboarding.flow.test.mjs`
|
|
343
|
+
- `tests/uc5-subject-data.flow.test.mjs`
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# PRACTITIONER_FLOW_STEP_BY_STEP
|
|
2
|
+
|
|
3
|
+
Flow for physician/practitioner employee after controller has issued `activationCode`.
|
|
4
|
+
|
|
5
|
+
## Rules first
|
|
6
|
+
|
|
7
|
+
- `jurisdiction` is mandatory (country context for tenant routing and authorization).
|
|
8
|
+
- `activationCode` is single-use bootstrap material for identity activation.
|
|
9
|
+
- Identity token endpoints and entity/business endpoints are different concerns:
|
|
10
|
+
- identity token flow path: `/host/cds-{jurisdiction}/v1/{sector}/{tenantId}/identity/auth/...`
|
|
11
|
+
- entity/business paths: `/{tenantId}/cds-{jurisdiction}/v1/{sector}/...`
|
|
12
|
+
- Practitioner signs `client_assertion` with their private signing key when required by the token endpoint.
|
|
13
|
+
- Message transport may also use backend wallet JWS/JWE protection (deployment policy: `plain` / `strict` / `auto-detect`).
|
|
14
|
+
|
|
15
|
+
## 1) Receive activation code
|
|
16
|
+
|
|
17
|
+
Input from controller flow:
|
|
18
|
+
- `activationCode`
|
|
19
|
+
|
|
20
|
+
Runtime context:
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
const profileContext = {
|
|
24
|
+
baseUrl: process.env.BASE_URL!,
|
|
25
|
+
jurisdiction: process.env.JURISDICTION!, // REQUIRED
|
|
26
|
+
sector: process.env.SECTOR || 'health-care',
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const sessionContext = {
|
|
30
|
+
tenantId: currentSession.tenantId,
|
|
31
|
+
practitionerDidWeb: currentSession.practitioner.didWeb,
|
|
32
|
+
practitionerIdToken: currentSession.idToken,
|
|
33
|
+
practitionerPublicJwk: currentSession.practitioner.publicJwk,
|
|
34
|
+
};
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## 2) Activate device identity (DCR bootstrap)
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
const client = new DataspaceNodeClient({
|
|
41
|
+
baseUrl: profileContext.baseUrl,
|
|
42
|
+
ctx: {
|
|
43
|
+
tenantId: sessionContext.tenantId,
|
|
44
|
+
jurisdiction: profileContext.jurisdiction,
|
|
45
|
+
sector: profileContext.sector,
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const device = await client.activateEmployeeDeviceWithActivationCodeSimple({
|
|
50
|
+
tenantId: sessionContext.tenantId,
|
|
51
|
+
jurisdiction: profileContext.jurisdiction,
|
|
52
|
+
sector: profileContext.sector,
|
|
53
|
+
activationCode,
|
|
54
|
+
idToken: sessionContext.practitionerIdToken,
|
|
55
|
+
dcrPayload: {
|
|
56
|
+
application_type: 'web',
|
|
57
|
+
token_endpoint_auth_method: 'private_key_jwt',
|
|
58
|
+
jwks: { keys: [sessionContext.practitionerPublicJwk] },
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## 3) Request SMART token for protected operations
|
|
64
|
+
|
|
65
|
+
`targetEndpoint` is only a local token-target identifier. Authorization is defined by `scopes`.
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
const smart = await client.requestSmartTokenSimple({
|
|
69
|
+
tenantId: sessionContext.tenantId,
|
|
70
|
+
jurisdiction: profileContext.jurisdiction,
|
|
71
|
+
sector: profileContext.sector,
|
|
72
|
+
idToken: sessionContext.practitionerIdToken,
|
|
73
|
+
targetEndpoint: `smart:practitioner:${sessionContext.practitionerDidWeb}:ips-read`,
|
|
74
|
+
scopes: [
|
|
75
|
+
'organization/Composition.rs',
|
|
76
|
+
'organization/Consent.cruds',
|
|
77
|
+
'organization/Communication.cruds',
|
|
78
|
+
],
|
|
79
|
+
});
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
IPS note:
|
|
83
|
+
- Access to IPS sections is authorized via scopes (for example `organization/Composition.rs`) and consent/policy checks.
|
|
84
|
+
- Do not derive authorization from `targetEndpoint` or from a single endpoint selector.
|
|
85
|
+
|
|
86
|
+
## 3.1) Where JWT signing happens (`private_key_jwt`)
|
|
87
|
+
|
|
88
|
+
If your token endpoint requires `client_assertion` (`private_key_jwt`), the JWT must be signed with the practitioner's/controller's private key.
|
|
89
|
+
|
|
90
|
+
Option A (recommended): SDK signs and sends `client_assertion` for you.
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
const token = await client.authenticateBackendSmartStandard({
|
|
94
|
+
clientId: sessionContext.practitionerDidWeb,
|
|
95
|
+
scopes: ['organization/Composition.rs'],
|
|
96
|
+
targetEndpoint: `smart:practitioner:${sessionContext.practitionerDidWeb}:ips-read`,
|
|
97
|
+
tokenUrl: `${profileContext.baseUrl}/token`,
|
|
98
|
+
audience: `${profileContext.baseUrl}/token`,
|
|
99
|
+
walletContext: {
|
|
100
|
+
tenantId: sessionContext.tenantId,
|
|
101
|
+
jurisdiction: profileContext.jurisdiction,
|
|
102
|
+
sector: profileContext.sector,
|
|
103
|
+
},
|
|
104
|
+
publicJwk: sessionContext.practitionerPublicJwk,
|
|
105
|
+
});
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Option B (manual): build/sign JWT yourself, then attach it in token request.
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
const now = Math.floor(Date.now() / 1000);
|
|
112
|
+
const encodedHeader = base64url(JSON.stringify({
|
|
113
|
+
alg: 'ES384',
|
|
114
|
+
typ: 'JWT',
|
|
115
|
+
kid: sessionContext.practitionerPublicJwk.kid,
|
|
116
|
+
}));
|
|
117
|
+
const encodedPayload = base64url(JSON.stringify({
|
|
118
|
+
iss: sessionContext.practitionerDidWeb,
|
|
119
|
+
sub: sessionContext.practitionerDidWeb,
|
|
120
|
+
aud: `${profileContext.baseUrl}/token`,
|
|
121
|
+
iat: now,
|
|
122
|
+
exp: now + 300,
|
|
123
|
+
jti: crypto.randomUUID(),
|
|
124
|
+
}));
|
|
125
|
+
const signingInput = `${encodedHeader}.${encodedPayload}`; // what must be signed
|
|
126
|
+
|
|
127
|
+
const signatureBase64Url = await externalSigner(signingInput); // integrator-managed signer
|
|
128
|
+
const clientAssertion = `${encodedHeader}.${encodedPayload}.${signatureBase64Url}`;
|
|
129
|
+
|
|
130
|
+
// Equivalent high-level helper style:
|
|
131
|
+
// const clientAssertion = await wallet.signCompactJws({ header, claims });
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
const tokenRequestBody = {
|
|
136
|
+
grant_type: 'client_credentials',
|
|
137
|
+
client_id: sessionContext.practitionerDidWeb,
|
|
138
|
+
scope: 'organization/Composition.rs organization/Consent.cruds organization/Communication.cruds',
|
|
139
|
+
client_assertion_type: 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',
|
|
140
|
+
client_assertion, // signed JWT is attached here
|
|
141
|
+
};
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Integrator note:
|
|
145
|
+
- SDK can help generate/sign compact JWTs, but key custody/signature execution belongs to the integrator.
|
|
146
|
+
- The signer may be wallet SDK, secure enclave, KMS, HSM, or remote signature service.
|
|
147
|
+
|
|
148
|
+
```ts
|
|
149
|
+
// Alternative helper form if your signer abstraction already returns compact JWS:
|
|
150
|
+
const clientAssertion = await wallet.signCompactJws({
|
|
151
|
+
header: {
|
|
152
|
+
alg: 'ES384',
|
|
153
|
+
typ: 'JWT',
|
|
154
|
+
kid: sessionContext.practitionerPublicJwk.kid,
|
|
155
|
+
},
|
|
156
|
+
claims: {
|
|
157
|
+
iss: sessionContext.practitionerDidWeb,
|
|
158
|
+
sub: sessionContext.practitionerDidWeb,
|
|
159
|
+
aud: `${profileContext.baseUrl}/token`,
|
|
160
|
+
iat: now,
|
|
161
|
+
exp: now + 300,
|
|
162
|
+
jti: crypto.randomUUID(),
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
For the current GW `identity/auth/_token` flow documented in this SDK, `requestSmartTokenSimple(...)` is id-token based.
|
|
168
|
+
Use `private_key_jwt` flow when your deployment exposes a standards token endpoint that requires `client_assertion`.
|
|
169
|
+
|
|
170
|
+
## 4) Call protected APIs with returned bearer
|
|
171
|
+
|
|
172
|
+
Use `smart.accessToken` in subsequent requests.
|
|
173
|
+
|
|
174
|
+
Complete path examples:
|
|
175
|
+
- exchange activation code:
|
|
176
|
+
`/host/cds-ES/v1/health-care/{tenantId}/identity/auth/_exchange`
|
|
177
|
+
- DCR:
|
|
178
|
+
`/host/cds-ES/v1/health-care/{tenantId}/identity/auth/_dcr`
|
|
179
|
+
- SMART token:
|
|
180
|
+
`/host/cds-ES/v1/health-care/{tenantId}/identity/auth/_token`
|
|
181
|
+
- example protected entity route (if practitioner is allowed):
|
|
182
|
+
`/{tenantId}/cds-ES/v1/health-care/entity/org.schema/Employee/_search`
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# React Web Integration
|
|
2
|
+
|
|
3
|
+
This guide is frontend-only.
|
|
4
|
+
Backend SDK usage is documented in `docs/BACKEND_NODE_INTEGRATION.md`.
|
|
5
|
+
|
|
6
|
+
## 1. What the React app does
|
|
7
|
+
|
|
8
|
+
1. Run ICA UX flow and obtain `vpToken`.
|
|
9
|
+
2. Send onboarding payloads to your backend.
|
|
10
|
+
3. Show progress and status from backend responses.
|
|
11
|
+
|
|
12
|
+
React does not call privileged GW onboarding routes directly.
|
|
13
|
+
|
|
14
|
+
## 2. Legal onboarding payload sent by frontend
|
|
15
|
+
|
|
16
|
+
Minimum payload to backend endpoint `/api/onboarding/legal/activate`:
|
|
17
|
+
|
|
18
|
+
```json
|
|
19
|
+
{
|
|
20
|
+
"jurisdiction": "ES",
|
|
21
|
+
"sector": "health-care",
|
|
22
|
+
"vpToken": "<vp_token>"
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Notes:
|
|
27
|
+
- `jurisdiction` is explicit route context. Do not infer from VAT.
|
|
28
|
+
- `sector` is explicit route context. Do not infer from DID.
|
|
29
|
+
- `organizationVc` / `legalRepresentativeVc` can be sent only for legacy compatibility.
|
|
30
|
+
|
|
31
|
+
## 3. Frontend code example
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
export async function activateLegalOrganization(input: {
|
|
35
|
+
jurisdiction: string;
|
|
36
|
+
sector: string;
|
|
37
|
+
vpToken: string;
|
|
38
|
+
}) {
|
|
39
|
+
const response = await fetch('/api/onboarding/legal/activate', {
|
|
40
|
+
method: 'POST',
|
|
41
|
+
headers: { 'content-type': 'application/json' },
|
|
42
|
+
body: JSON.stringify(input),
|
|
43
|
+
});
|
|
44
|
+
if (!response.ok) throw new Error(`Activation failed: ${response.status}`);
|
|
45
|
+
return response.json();
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## 4. Personal/family onboarding from frontend
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
export async function registerPersonalOrganization(payload: {
|
|
53
|
+
tenantId: string;
|
|
54
|
+
jurisdiction: string;
|
|
55
|
+
sector: string;
|
|
56
|
+
registrationPayload: unknown;
|
|
57
|
+
confirmationPayload?: unknown;
|
|
58
|
+
}) {
|
|
59
|
+
const response = await fetch('/api/onboarding/personal/register', {
|
|
60
|
+
method: 'POST',
|
|
61
|
+
headers: { 'content-type': 'application/json' },
|
|
62
|
+
body: JSON.stringify(payload),
|
|
63
|
+
});
|
|
64
|
+
if (!response.ok) throw new Error(`Personal onboarding failed: ${response.status}`);
|
|
65
|
+
return response.json();
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## 5. Backend document
|
|
70
|
+
|
|
71
|
+
For exact SDK methods and backend implementation steps, use:
|
|
72
|
+
- `docs/BACKEND_NODE_INTEGRATION.md`
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { DataspaceNodeClient } from '../dist/index.js';
|
|
2
|
+
import { ClaimsPersonSchemaorg } from 'gdc-common-utils-ts/constants/schemaorg';
|
|
2
3
|
|
|
3
4
|
function parseCsv(value, fallback = []) {
|
|
4
5
|
const items = String(value || '')
|
|
@@ -106,8 +107,8 @@ async function main() {
|
|
|
106
107
|
{
|
|
107
108
|
employeeClaims: {
|
|
108
109
|
'@context': 'org.schema',
|
|
109
|
-
|
|
110
|
-
|
|
110
|
+
[ClaimsPersonSchemaorg.email]: process.env.CONTROLLER_EMAIL || 'controller@example.com',
|
|
111
|
+
[ClaimsPersonSchemaorg.hasOccupation]: process.env.CONTROLLER_ROLE || 'ISCO-08|1342',
|
|
111
112
|
},
|
|
112
113
|
},
|
|
113
114
|
pollOptions,
|