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.
Files changed (37) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/README.md +44 -1
  3. package/dist/client.d.ts +153 -33
  4. package/dist/client.js +619 -93
  5. package/dist/index.d.ts +1 -0
  6. package/dist/index.js +1 -0
  7. package/dist/types.d.ts +112 -1
  8. package/dist/vp-token.d.ts +37 -0
  9. package/dist/vp-token.js +56 -0
  10. package/docs/API.md +19 -4
  11. package/docs/BACKEND_NODE_INTEGRATION.md +249 -0
  12. package/docs/CONTROLLER_FLOW_STEP_BY_STEP.md +283 -0
  13. package/docs/DATA_MODEL_ALIGNMENT.md +37 -13
  14. package/docs/DEVELOPER_USE_CASES.md +10 -2
  15. package/docs/E2E_LOCAL_GW_UC5.md +49 -0
  16. package/docs/ENDPOINT_ID_CATALOG.md +90 -0
  17. package/docs/LEGAL_ORGANIZATION_FLOW_STEP_BY_STEP.md +84 -0
  18. package/docs/PERSONAL_FLOW_STEP_BY_STEP.md +70 -0
  19. package/docs/PORTAL_BACKEND_INTEGRATION_HANDOVER.md +343 -0
  20. package/docs/PRACTITIONER_FLOW_STEP_BY_STEP.md +182 -0
  21. package/docs/REACT_WEB_INTEGRATION.md +72 -0
  22. package/examples/e2e-bootstrap-tenant.mjs +3 -2
  23. package/examples/e2e-individual-flow.mjs +13 -8
  24. package/examples/host-activate-and-employee.mjs +3 -2
  25. package/examples/smoke-legal-org-local.mjs +40 -0
  26. package/package.json +4 -3
  27. package/src/client.ts +784 -132
  28. package/src/index.ts +1 -0
  29. package/src/types.ts +123 -1
  30. package/src/vp-token.ts +91 -0
  31. package/tests/client.test.mjs +491 -0
  32. package/tests/fixtures/ica-vp-minimal.json +67 -0
  33. package/tests/helpers/vp-token-fixture.mjs +23 -0
  34. package/tests/live-gw-uc5.e2e.test.mjs +108 -0
  35. package/SDK_PARITY_MAP.md +0 -120
  36. package/TODO_PROMPT_NEXT_STEPS.md +0 -185
  37. package/artifacts/update-smart-wallet.js +0 -1016
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from './types.js';
2
2
  export * from './builders.js';
3
+ export * from './vp-token.js';
3
4
  export * from './client.js';
4
5
  export * from './sdk/dataspace-wallet-sdk-node/index.js';
package/dist/index.js CHANGED
@@ -4,5 +4,6 @@
4
4
  // See subjectVaultPhoneResolution.test.ts for context and migration plan.
5
5
  export * from './types.js';
6
6
  export * from './builders.js';
7
+ export * from './vp-token.js';
7
8
  export * from './client.js';
8
9
  export * from './sdk/dataspace-wallet-sdk-node/index.js';
package/dist/types.d.ts CHANGED
@@ -158,6 +158,23 @@ export type FamilyOrganizationSummary = {
158
158
  missingFields?: string[];
159
159
  updatedAt?: string;
160
160
  };
161
+ export type OfferPreview = {
162
+ offerId?: string;
163
+ amount?: string;
164
+ currency?: string;
165
+ seats?: number;
166
+ planName?: string;
167
+ sku?: string;
168
+ paymentMethod?: string;
169
+ checkoutUrl?: string;
170
+ };
171
+ export type OfferInfo = OfferPreview;
172
+ export type EndpointSelector = {
173
+ section: string;
174
+ format: string;
175
+ resourceType: string;
176
+ action: string;
177
+ };
161
178
  /**
162
179
  * Input for organization activation in GW using ICA-derived proof material.
163
180
  *
@@ -166,11 +183,40 @@ export type FamilyOrganizationSummary = {
166
183
  */
167
184
  export type GatewayOrganizationActivationInput = {
168
185
  vpToken: string;
186
+ /** Generic requested seats/members for initial offer sizing. Defaults to 2. */
187
+ numberOfMembers?: number;
188
+ organizationVc?: string;
189
+ legalRepresentativeVc?: string;
190
+ regulatoryEvidence?: Record<string, unknown>;
191
+ /** @deprecated Prefer `numberOfMembers` and explicit input fields. */
192
+ additionalClaims?: Record<string, unknown>;
193
+ };
194
+ export type GatewayOrganizationActivationSimpleInput = {
195
+ jurisdiction?: string;
196
+ sector?: string;
197
+ vpToken: string;
198
+ serviceProviderDidWeb?: string;
199
+ serviceProviderUrl?: string;
200
+ controllerEmail?: string;
201
+ controllerTelephone?: string;
202
+ controllerRole: string;
203
+ numberOfMembers?: number;
204
+ timeoutSeconds?: number;
205
+ intervalSeconds?: number;
169
206
  organizationVc?: string;
170
207
  legalRepresentativeVc?: string;
171
208
  regulatoryEvidence?: Record<string, unknown>;
172
209
  additionalClaims?: Record<string, unknown>;
173
210
  };
211
+ export type LegalOrganizationOrderSimpleInput = {
212
+ jurisdiction?: string;
213
+ sector?: string;
214
+ offerId: string;
215
+ timeoutSeconds?: number;
216
+ intervalSeconds?: number;
217
+ dataType?: string;
218
+ additionalClaims?: Record<string, unknown>;
219
+ };
174
220
  /**
175
221
  * Input for device activation based on activation code exchange + DCR.
176
222
  */
@@ -180,6 +226,16 @@ export type EmployeeDeviceActivationInput = {
180
226
  dcrPayload: Record<string, unknown>;
181
227
  pollOptions?: PollOptions;
182
228
  };
229
+ export type EmployeeDeviceActivationSimpleInput = {
230
+ tenantId?: string;
231
+ jurisdiction?: string;
232
+ sector?: string;
233
+ activationCode: string;
234
+ idToken: string;
235
+ dcrPayload: Record<string, unknown>;
236
+ timeoutSeconds?: number;
237
+ intervalSeconds?: number;
238
+ };
183
239
  /**
184
240
  * Result of device activation flow.
185
241
  *
@@ -217,6 +273,36 @@ export type SubjectOrganizationBootstrapResult = {
217
273
  registration: SubmitAndPollResult;
218
274
  confirmation?: SubmitAndPollResult;
219
275
  };
276
+ export type IndividualOrganizationBootstrapSimpleInput = {
277
+ tenantId?: string;
278
+ jurisdiction?: string;
279
+ sector?: string;
280
+ alternateName: string;
281
+ controllerEmail?: string;
282
+ controllerTelephone?: string;
283
+ controllerRole?: string;
284
+ timeoutSeconds?: number;
285
+ intervalSeconds?: number;
286
+ additionalClaims?: Record<string, unknown>;
287
+ };
288
+ export type IndividualOrganizationBootstrapSimpleResult = {
289
+ registration: SubmitAndPollResult;
290
+ offerId: string;
291
+ confirmation: SubmitAndPollResult;
292
+ };
293
+ export type IndividualOrganizationStartSimpleResult = {
294
+ registration: SubmitAndPollResult;
295
+ offerId: string;
296
+ offerPreview: OfferPreview;
297
+ };
298
+ export type IndividualOrganizationConfirmOrderSimpleInput = {
299
+ tenantId?: string;
300
+ jurisdiction?: string;
301
+ sector?: string;
302
+ offerId: string;
303
+ timeoutSeconds?: number;
304
+ intervalSeconds?: number;
305
+ };
220
306
  /**
221
307
  * Input for UC 5.5 IPS/FHIR import and index update.
222
308
  */
@@ -367,6 +453,8 @@ export type ClientOptions = {
367
453
  bearerToken?: string;
368
454
  defaultHeaders?: Record<string, string>;
369
455
  wallet?: WalletProvider;
456
+ /** Optional default tenant context so calls can omit ctx repeatedly. */
457
+ ctx?: RouteContext;
370
458
  };
371
459
  /**
372
460
  * Options for identity-exchange.v1 backend PKCE + token exchange flow.
@@ -390,6 +478,8 @@ export type BackendPkceAuthOptions = {
390
478
  /** Requested scopes for the SMART bearer token. */
391
479
  scopes: string[];
392
480
  /** Cache key for the resulting bearer token. Defaults to `pkce:<apiKey prefix>`. */
481
+ tokenCacheKey?: string;
482
+ /** @deprecated Use `tokenCacheKey`. */
393
483
  endpointId?: string;
394
484
  /** PKCE code verifier. Auto-generated with randomUUID if not provided. */
395
485
  codeVerifier?: string;
@@ -399,6 +489,8 @@ export type BackendPkceAuthOptions = {
399
489
  export type BackendPkceAuthResult = {
400
490
  /** `fetched`: new token obtained. `cached`: valid token already in cache. `failed`: flow error. */
401
491
  status: 'fetched' | 'cached' | 'failed';
492
+ tokenCacheKey: string;
493
+ /** @deprecated Use `tokenCacheKey`. */
402
494
  endpointId: string;
403
495
  accessToken: string;
404
496
  tokenType: string;
@@ -409,6 +501,8 @@ export type BackendPkceAuthResult = {
409
501
  export type BackendSmartAuthOptions = {
410
502
  clientId: string;
411
503
  scopes: string[];
504
+ tokenCacheKey?: string;
505
+ /** @deprecated Use `tokenCacheKey`. */
412
506
  endpointId?: string;
413
507
  tokenUrl?: string;
414
508
  tokenPath?: string;
@@ -421,6 +515,8 @@ export type BackendSmartAuthOptions = {
421
515
  export type BackendSmartAuthResult = {
422
516
  status: 'fetched' | 'cached' | 'failed';
423
517
  profile: 'smart-backend.v1';
518
+ tokenCacheKey: string;
519
+ /** @deprecated Use `tokenCacheKey`. */
424
520
  endpointId: string;
425
521
  accessToken?: string;
426
522
  tokenType?: string;
@@ -430,11 +526,26 @@ export type BackendSmartAuthResult = {
430
526
  response?: unknown;
431
527
  };
432
528
  export type SmartTokenExchangeInput = {
433
- endpointId: string;
529
+ tokenCacheKey: string;
530
+ /** @deprecated Use `tokenCacheKey`. */
531
+ endpointId?: string;
434
532
  scopes: string[];
435
533
  exchangePayload: Record<string, unknown>;
436
534
  path?: string;
437
535
  };
536
+ export type SmartTokenRequestSimpleInput = {
537
+ tenantId?: string;
538
+ jurisdiction?: string;
539
+ sector?: string;
540
+ idToken: string;
541
+ scopes: string[];
542
+ tokenCacheKey?: string;
543
+ /** @deprecated Use `tokenCacheKey`. */
544
+ endpointId?: string;
545
+ timeoutSeconds?: number;
546
+ intervalSeconds?: number;
547
+ additionalClaims?: Record<string, unknown>;
548
+ };
438
549
  export type SmartTokenExchangeResult = {
439
550
  status: 'fetched' | 'cached' | 'failed';
440
551
  accessToken?: string;
@@ -0,0 +1,37 @@
1
+ export type VpTokenHeader = {
2
+ alg: string;
3
+ typ?: string;
4
+ kid?: string;
5
+ [key: string]: unknown;
6
+ };
7
+ export type VpTokenPayload = {
8
+ iss: string;
9
+ sub?: string;
10
+ aud?: string;
11
+ jti?: string;
12
+ iat?: number;
13
+ exp?: number;
14
+ nonce?: string;
15
+ vp: {
16
+ '@context'?: unknown;
17
+ type?: unknown;
18
+ holder?: string;
19
+ verifiableCredential: string[];
20
+ [key: string]: unknown;
21
+ };
22
+ [key: string]: unknown;
23
+ };
24
+ export declare function generateUuidLike(): string;
25
+ export declare function buildEpochWindow(ttlSeconds?: number): {
26
+ iat: number;
27
+ exp: number;
28
+ };
29
+ export declare function createVP(input?: Partial<VpTokenPayload>): VpTokenPayload;
30
+ export declare function addVC(vpPayload: VpTokenPayload, vcJwt: string): VpTokenPayload;
31
+ export declare function prepareForSignature(header: VpTokenHeader, payload: VpTokenPayload): {
32
+ encodedHeader: string;
33
+ encodedPayload: string;
34
+ signingInput: string;
35
+ };
36
+ export declare function prepareBytesForSignature(header: VpTokenHeader, payload: VpTokenPayload): Uint8Array;
37
+ export declare function buildVpTokenCompact(encodedHeader: string, encodedPayload: string, signatureBase64Url: string): string;
@@ -0,0 +1,56 @@
1
+ function toB64UrlJson(input) {
2
+ return Buffer.from(JSON.stringify(input), 'utf-8').toString('base64url');
3
+ }
4
+ function fallbackId() {
5
+ const rand = Math.random().toString(36).slice(2, 10);
6
+ return `id-${Date.now()}-${rand}`;
7
+ }
8
+ export function generateUuidLike() {
9
+ const fn = globalThis?.crypto?.randomUUID;
10
+ if (typeof fn === 'function')
11
+ return fn.call(globalThis.crypto);
12
+ return fallbackId();
13
+ }
14
+ export function buildEpochWindow(ttlSeconds = 300) {
15
+ const iat = Math.floor(Date.now() / 1000);
16
+ return { iat, exp: iat + Math.max(1, Math.floor(ttlSeconds)) };
17
+ }
18
+ export function createVP(input) {
19
+ const ttl = input?.exp && input?.iat ? undefined : buildEpochWindow(300);
20
+ const jti = input?.jti || generateUuidLike();
21
+ const nonce = input?.nonce || generateUuidLike();
22
+ return {
23
+ iss: String(input?.iss || ''),
24
+ sub: input?.sub,
25
+ aud: input?.aud,
26
+ jti,
27
+ iat: input?.iat ?? ttl?.iat,
28
+ exp: input?.exp ?? ttl?.exp,
29
+ nonce,
30
+ vp: {
31
+ '@context': ['https://www.w3.org/2018/credentials/v1'],
32
+ type: ['VerifiablePresentation'],
33
+ holder: input?.vp?.holder || input?.iss || '',
34
+ verifiableCredential: [],
35
+ ...(input?.vp || {}),
36
+ },
37
+ };
38
+ }
39
+ export function addVC(vpPayload, vcJwt) {
40
+ const v = String(vcJwt || '').trim();
41
+ if (v)
42
+ vpPayload.vp.verifiableCredential.push(v);
43
+ return vpPayload;
44
+ }
45
+ export function prepareForSignature(header, payload) {
46
+ const encodedHeader = toB64UrlJson(header);
47
+ const encodedPayload = toB64UrlJson(payload);
48
+ return { encodedHeader, encodedPayload, signingInput: `${encodedHeader}.${encodedPayload}` };
49
+ }
50
+ export function prepareBytesForSignature(header, payload) {
51
+ const { signingInput } = prepareForSignature(header, payload);
52
+ return new TextEncoder().encode(signingInput);
53
+ }
54
+ export function buildVpTokenCompact(encodedHeader, encodedPayload, signatureBase64Url) {
55
+ return `${encodedHeader}.${encodedPayload}.${String(signatureBase64Url || '').trim()}`;
56
+ }
package/docs/API.md CHANGED
@@ -478,22 +478,33 @@ const token = client.getCachedBearerToken('pkce:my-api-key');
478
478
 
479
479
  ## Transport methods
480
480
 
481
- ### `submitBatch`
481
+ ### `submitBundle`
482
482
 
483
- POST a DIDComm plaintext payload (`application/didcomm-plaintext+json`).
483
+ POST a DIDComm bundle payload. Preferred API for FHIR/API bundle operations.
484
484
 
485
485
  ```ts
486
- submitBatch(path: string, payload: unknown): Promise<SubmitResponse>
486
+ submitBundle(
487
+ path: string,
488
+ payload: { thid?: string } & Record<string, unknown>,
489
+ options?: {
490
+ mode?: 'plain' | 'strict';
491
+ recipientEncryptionJwk?: PublicJwk;
492
+ walletContext?: WalletContext;
493
+ },
494
+ ): Promise<SubmitResponse>
487
495
  ```
488
496
 
489
497
  ```ts
490
- const { status, location } = await client.submitBatch(
498
+ const { status, location } = await client.submitBundle(
491
499
  client.individualTaskBatchPath(ctx),
492
500
  payload,
501
+ { mode: 'plain' },
493
502
  );
494
503
  // location header contains the poll URL
495
504
  ```
496
505
 
506
+ `submitBatch(...)` remains available as a legacy alias for compatibility.
507
+
497
508
  ---
498
509
 
499
510
  ### `submitBatchEncrypted`
@@ -531,6 +542,10 @@ POST a plain JSON body (`application/json`). Use for token endpoints and API key
531
542
  postJson(path: string, payload: unknown): Promise<SubmitResponse>
532
543
  ```
533
544
 
545
+ ### `submitLegacyJson`
546
+
547
+ Alias of `postJson(...)` for explicit non-bundle JSON submits (openid/token/resource payloads).
548
+
534
549
  ```ts
535
550
  const response = await client.postJson('/host/admin/api-keys', {
536
551
  agent: { email: 'service@example.com' },
@@ -0,0 +1,249 @@
1
+ # Backend Node Integration
2
+
3
+ This guide is backend-only and uses `dataspace-client-sdk-node`.
4
+
5
+ ## Secure communications intent
6
+
7
+ - `backendWallet` is used for secure API communications in the dataspace.
8
+ - User authentication and authorization still apply (for example `client_assertion` for SMART token issuance, scopes, consent).
9
+ - Message protection can run in addition to HTTPS:
10
+ - signature (embedded JWS)
11
+ - encryption (JWE with nested JWS)
12
+ - Apply by environment policy (`plain` / `strict` / `auto-detect`).
13
+ - This is layered security (transport + message), not a literal VPN.
14
+
15
+ ## Credential map (do not mix)
16
+
17
+ | Credential | Plane | Purpose | Not used for |
18
+ |---|---|---|---|
19
+ | transport bearer / mTLS / API-gateway credential | Transport | Allow backend HTTP access to GW deployment | User identity, `_exchange`, SMART scopes |
20
+ | `vp_token` | Identity/business | Onboarding proof for activation | Catalog discovery auth |
21
+ | `idToken` (user session) | Identity/business | User session proof | Transport gateway access policy |
22
+ | `_exchange` token flow | Identity/business | Activation code exchange for initial access | Multi-ICA/operator catalog traversal |
23
+ | SMART access token | Identity/business | Authorized protected operations via scopes | Infrastructure/operator-plane control |
24
+
25
+ If your deployment does not require a transport credential, omit `bearerToken` entirely.
26
+
27
+ Recommended initialization:
28
+
29
+ ```ts
30
+ import { randomBytes } from 'node:crypto';
31
+ import { DataspaceNodeClient, SeedWalletProvider } from 'dataspace-client-sdk-node';
32
+
33
+ const backendWalletSeedBytes = process.env.WALLET_SEED_BASE64URL
34
+ ? Buffer.from(process.env.WALLET_SEED_BASE64URL, 'base64url')
35
+ : randomBytes(32); // if not provided, generate random 32 bytes for this runtime
36
+
37
+ const backendWallet = new SeedWalletProvider(
38
+ Buffer.from(backendWalletSeedBytes).toString('base64url'),
39
+ );
40
+
41
+ const client = new DataspaceNodeClient({
42
+ baseUrl,
43
+ bearerToken, // optional transport-plane credential if deployment requires it
44
+ wallet: backendWallet,
45
+ ctx: { tenantId, jurisdiction, sector },
46
+ });
47
+ ```
48
+
49
+ Alternative (mutable context):
50
+
51
+ ```ts
52
+ const client = new DataspaceNodeClient({ baseUrl, bearerToken, wallet: backendWallet });
53
+ client.setContextOrg({ tenantId, jurisdiction, sector });
54
+ client.setDefaultTimeoutSeconds(12);
55
+ client.setDefaultIntervalSeconds(2);
56
+ ```
57
+
58
+ ### Communication mode (integration-level policy)
59
+
60
+ SDK does not currently expose a single constructor flag for communication mode.
61
+ Use an integration policy variable and choose the submit method explicitly:
62
+
63
+ - `plain`: always `submitBundle(..., { mode: 'plain' })` (or `submitBatch(...)` for legacy compatibility)
64
+ - `strict`: always `submitBatchEncrypted(...)`
65
+ - `auto-detect` (default): use encrypted when backend wallet + recipient encryption JWK are available, otherwise plaintext
66
+
67
+ GW compatibility (current):
68
+ - supported request envelopes:
69
+ - `application/didcomm-plaintext+json` (demo/compat only)
70
+ - `application/json` (legacy/demo paths only)
71
+ - `application/x-www-form-urlencoded` with `request=<jwe>` (secure/FAPI style)
72
+ - no standalone `didcomm-signed+json` HTTP submit mode is currently exposed as a first-class request content type.
73
+ - signed-only JWS exists as an internal layer pattern, but secure mode entrypoint is form-encoded JWE.
74
+
75
+ ```ts
76
+ type CommunicationMode = 'plain' | 'strict' | 'auto-detect';
77
+ const communicationMode: CommunicationMode = (process.env.GW_COMMUNICATION_MODE as CommunicationMode) || 'auto-detect';
78
+
79
+ async function submitDidcomm(
80
+ client: DataspaceNodeClient,
81
+ path: string,
82
+ payload: Record<string, unknown>,
83
+ opts: {
84
+ mode: CommunicationMode;
85
+ walletContext: { tenantId: string; jurisdiction: string; sector: string; walletId?: string };
86
+ recipientEncryptionJwk?: any;
87
+ },
88
+ ) {
89
+ const canEncrypt = !!opts.recipientEncryptionJwk;
90
+ if (opts.mode === 'strict' && !canEncrypt) {
91
+ throw new Error('strict mode requires recipientEncryptionJwk');
92
+ }
93
+ if (opts.mode === 'strict' || (opts.mode === 'auto-detect' && canEncrypt)) {
94
+ return client.submitBatchEncrypted(path, payload as any, opts.recipientEncryptionJwk, opts.walletContext as any);
95
+ }
96
+ return client.submitBundle(path, payload, { mode: 'plain' });
97
+ }
98
+ ```
99
+
100
+ ## Flow A. Legal Organization Onboarding (B2B)
101
+
102
+ 1. Receive from frontend: `jurisdiction`, `sector`, `vpToken`.
103
+ 2. Activate in GW:
104
+ - SDK method: `activateOrganizationInGatewaySimple(...)` (recommended)
105
+ - SDK method: `activateOrganizationInGatewayFromIcaProof(...)` (advanced)
106
+ 3. Complete legal organization order (always; amount may be `0`) using `offerId` returned by activation response.
107
+ - `hostRegistryOrderBatchPath(...)`
108
+ - `hostRegistryOrderPollPath(...)`
109
+ - `submitAndPoll(...)`
110
+ 4. Run DCR/token bootstrap:
111
+ - `activateEmployeeDeviceWithActivationCodeSimple(...)` (recommended)
112
+ - `activateEmployeeDeviceWithActivationCode(...)` (advanced)
113
+ - `requestSmartTokenSimple(...)` (recommended)
114
+ - `requestSmartToken(...)` (advanced)
115
+ - `authenticateBackendPkceAndExchange(...)` or `authenticateBackendSmartStandard(...)`
116
+
117
+ ### Custom backend code example: activate endpoint
118
+
119
+ ```ts
120
+ app.post('/api/onboarding/legal/activate', async (req, res) => {
121
+ const client = new DataspaceNodeClient({ baseUrl, bearerToken });
122
+ client.setContextOrg({
123
+ tenantId: req.body.tenantId,
124
+ jurisdiction: req.body.jurisdiction,
125
+ sector: req.body.sector,
126
+ });
127
+ const activation = await client.activateOrganizationInGatewaySimple({
128
+ vpToken: req.body.vpToken,
129
+ serviceProviderDidWeb: req.body.serviceProviderDidWeb, // or serviceProviderUrl
130
+ serviceProviderUrl: req.body.serviceProviderUrl,
131
+ controllerEmail: req.body.controllerEmail,
132
+ controllerTelephone: req.body.controllerTelephone, // optional alternative to email
133
+ controllerRole: req.body.controllerRole,
134
+ numberOfMembers: req.body.numberOfMembers ?? 2,
135
+ });
136
+ const offerId = client.getOfferIdFromResponse(activation);
137
+ if (!offerId) throw new Error('Offer id missing in activation response');
138
+ const offer = client.getOfferPreviewFromResponse(activation);
139
+ // Use `offer` to render amount/currency/description to user before acceptance.
140
+ const order = await client.confirmLegalOrganizationOrderSimple({
141
+ offerId,
142
+ });
143
+ res.json({ activation, offerId, order });
144
+ });
145
+ ```
146
+
147
+ Async UX note:
148
+ - `submitAndPoll` is convenient but returns final state.
149
+ - If you need progress updates, call lower-level `submitBatch` + `pollUntilComplete`
150
+ and stream step transitions to frontend.
151
+
152
+ ## Flow B. Personal Organization Onboarding (individual/family)
153
+
154
+ 1. Receive registration payload from frontend.
155
+ 2. Start registration and extract offer:
156
+ - SDK method: `startIndividualOrganizationSimple(...)` (recommended)
157
+ 3. Frontend shows offer and user accepts.
158
+ 4. Confirm order:
159
+ - SDK method: `confirmIndividualOrganizationOrderSimple(...)` (recommended)
160
+ 5. Provisional one-shot helper (auto-order, for legacy/fork scenarios):
161
+ - SDK method: `bootstrapIndividualOrganizationSimple(...)` (provisional)
162
+ 3. Continue consent/clinical lifecycle as needed:
163
+ - `grantProfessionalAccessSimple(...)`
164
+ - `importIpsOrFhirAndUpdateIndex(...)`
165
+ - `requestSmartTokenSimple(...)` (recommended)
166
+ - `requestSmartToken(...)` (advanced)
167
+ - `generateDigitalTwinFromSubjectData(...)`
168
+
169
+ ### Custom backend code example: personal register endpoint
170
+
171
+ ```ts
172
+ app.post('/api/onboarding/personal/register', async (req, res) => {
173
+ const client = new DataspaceNodeClient({ baseUrl, bearerToken });
174
+ const ctx = {
175
+ tenantId: req.body.tenantId,
176
+ jurisdiction: req.body.jurisdiction,
177
+ sector: req.body.sector,
178
+ };
179
+
180
+ const started = await client.startIndividualOrganizationSimple({
181
+ alternateName: req.body.alternateName,
182
+ controllerEmail: req.body.controllerEmail,
183
+ controllerTelephone: req.body.controllerTelephone,
184
+ controllerRole: req.body.controllerRole || 'org.hl7.v3.RoleCode|RESPRSN',
185
+ });
186
+ // Return offer to frontend for explicit acceptance UX.
187
+ // Later, call confirmIndividualOrganizationOrderSimple({ offerId: started.offerId }).
188
+ res.json(started);
189
+ });
190
+ ```
191
+
192
+ ## References
193
+
194
+ - `examples/e2e-bootstrap-tenant.mjs`
195
+ - `examples/host-activate-and-employee.mjs`
196
+ - `examples/e2e-individual-flow.mjs`
197
+ - `tests/uc5-org-onboarding.flow.test.mjs`
198
+ - `tests/uc5-subject-data.flow.test.mjs`
199
+
200
+ ## Deterministic Wallet Profile (seed-based, integrator-managed)
201
+
202
+ Use this section for all flows (controller/professional/personal) when the backend signs/encrypts DIDComm messages without external per-operation KMS signing.
203
+
204
+ Core rule:
205
+ - key custody and seed storage are integrator responsibility
206
+ - SDK requires signing/encryption capability, not local private-key assumptions
207
+
208
+ Recommended derivation contract:
209
+
210
+ ```ts
211
+ type DeriveKeyMaterialInput = {
212
+ seed: Uint8Array; // decrypted by integrator runtime
213
+ tenantId: string;
214
+ jurisdiction: string;
215
+ sector: string;
216
+ walletId?: string;
217
+ purpose: 'signing' | 'encryption';
218
+ algorithm: 'ES384' | 'ES256K' | 'ML-DSA-65' | 'ML-KEM-768';
219
+ kdfVersion: 'v1';
220
+ };
221
+ ```
222
+
223
+ Domain separation (mandatory):
224
+ - derive per `purpose` and `algorithm`
225
+ - never reuse derived bytes across different purposes or algorithms
226
+
227
+ Canonical context string:
228
+ - `tenantId|jurisdiction|sector|walletId|purpose|algorithm|kdfVersion`
229
+
230
+ KDF profile (example):
231
+ - `scrypt(seed, salt=context, N=2^15, r=8, p=1, dkLen=64)`
232
+ - split derived bytes by algorithm requirements
233
+ - keep parameters versioned and immutable per `kdfVersion`
234
+
235
+ Why:
236
+ - deterministic reproducibility across runs/devices
237
+ - safe algorithm migration (ES384 now, ML-DSA later) without collisions
238
+ - portable across Node backend and mobile/web SDK profiles
239
+
240
+ Strict DIDComm mode:
241
+ - sign payload as embedded JWS
242
+ - encrypt as JWE for recipient
243
+ - use the derived keypair for the active `purpose+algorithm`
244
+
245
+ Flow links:
246
+ - `CONTROLLER_FLOW_STEP_BY_STEP.md`
247
+ - `LEGAL_ORGANIZATION_FLOW_STEP_BY_STEP.md`
248
+ - `PRACTITIONER_FLOW_STEP_BY_STEP.md`
249
+ - `PERSONAL_FLOW_STEP_BY_STEP.md`