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
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
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
|
-
|
|
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;
|
package/dist/vp-token.js
ADDED
|
@@ -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
|
-
### `
|
|
481
|
+
### `submitBundle`
|
|
482
482
|
|
|
483
|
-
POST a DIDComm
|
|
483
|
+
POST a DIDComm bundle payload. Preferred API for FHIR/API bundle operations.
|
|
484
484
|
|
|
485
485
|
```ts
|
|
486
|
-
|
|
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.
|
|
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`
|