dataspace-client-sdk-node 0.1.1 → 0.1.2
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 +28 -0
- package/README.md +40 -0
- package/dist/client.d.ts +114 -32
- package/dist/client.js +482 -30
- package/dist/types.d.ts +92 -0
- package/docs/BACKEND_NODE_INTEGRATION.md +122 -0
- package/docs/DATA_MODEL_ALIGNMENT.md +37 -13
- package/docs/PORTAL_BACKEND_INTEGRATION_HANDOVER.md +335 -0
- package/docs/REACT_WEB_INTEGRATION.md +72 -0
- package/examples/smoke-legal-org-local.mjs +40 -0
- package/package.json +1 -1
- package/src/client.ts +628 -66
- package/src/types.ts +101 -0
- package/tests/client.test.mjs +491 -0
- package/SDK_PARITY_MAP.md +0 -120
package/src/client.ts
CHANGED
|
@@ -17,10 +17,12 @@ import type {
|
|
|
17
17
|
GrantProfessionalAccessSimpleResult,
|
|
18
18
|
DigitalTwinGenerationInput,
|
|
19
19
|
EmployeeDeviceActivationInput,
|
|
20
|
+
EmployeeDeviceActivationSimpleInput,
|
|
20
21
|
EmployeeDeviceActivationResult,
|
|
21
22
|
FamilyOrganizationSummary,
|
|
22
23
|
FamilyRegistrationStatus,
|
|
23
24
|
GatewayOrganizationActivationInput,
|
|
25
|
+
GatewayOrganizationActivationSimpleInput,
|
|
24
26
|
HostRouteContext,
|
|
25
27
|
IpsOrFhirImportInput,
|
|
26
28
|
MedicationOverlapCheckInput,
|
|
@@ -28,11 +30,18 @@ import type {
|
|
|
28
30
|
OrganizationEmployeeCreationInput,
|
|
29
31
|
PollOptions,
|
|
30
32
|
PollResult,
|
|
33
|
+
OfferPreview,
|
|
31
34
|
RouteContext,
|
|
32
35
|
SmartTokenExchangeInput,
|
|
33
36
|
SmartTokenExchangeResult,
|
|
37
|
+
SmartTokenRequestSimpleInput,
|
|
38
|
+
LegalOrganizationOrderSimpleInput,
|
|
34
39
|
SubjectOrganizationBootstrapInput,
|
|
35
40
|
SubjectOrganizationBootstrapResult,
|
|
41
|
+
IndividualOrganizationBootstrapSimpleInput,
|
|
42
|
+
IndividualOrganizationBootstrapSimpleResult,
|
|
43
|
+
IndividualOrganizationStartSimpleResult,
|
|
44
|
+
IndividualOrganizationConfirmOrderSimpleInput,
|
|
36
45
|
SubmitAndPollResult,
|
|
37
46
|
SubmitResponse,
|
|
38
47
|
V1Action,
|
|
@@ -49,6 +58,35 @@ function encode(value: string): string {
|
|
|
49
58
|
return encodeURIComponent(value);
|
|
50
59
|
}
|
|
51
60
|
|
|
61
|
+
function toDidWebFromUrlOrHost(raw: string): string | undefined {
|
|
62
|
+
const v = String(raw || '').trim();
|
|
63
|
+
if (!v) return undefined;
|
|
64
|
+
if (v.startsWith('did:web:')) return v;
|
|
65
|
+
const host = v
|
|
66
|
+
.replace(/^https?:\/\//i, '')
|
|
67
|
+
.replace(/\/.*$/, '')
|
|
68
|
+
.trim()
|
|
69
|
+
.toLowerCase();
|
|
70
|
+
if (!host) return undefined;
|
|
71
|
+
return `did:web:${host}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function parseRetryAfterMs(header: string | null): number | undefined {
|
|
75
|
+
if (!header) return undefined;
|
|
76
|
+
const raw = header.trim();
|
|
77
|
+
if (!raw) return undefined;
|
|
78
|
+
const seconds = Number(raw);
|
|
79
|
+
if (Number.isFinite(seconds) && seconds >= 0) {
|
|
80
|
+
return Math.floor(seconds * 1000);
|
|
81
|
+
}
|
|
82
|
+
const epochMs = Date.parse(raw);
|
|
83
|
+
if (Number.isFinite(epochMs)) {
|
|
84
|
+
const delta = epochMs - Date.now();
|
|
85
|
+
return delta > 0 ? delta : 0;
|
|
86
|
+
}
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
|
|
52
90
|
type CachedToken = {
|
|
53
91
|
accessToken: string;
|
|
54
92
|
tokenType: string;
|
|
@@ -61,6 +99,9 @@ export class DataspaceNodeClient {
|
|
|
61
99
|
private readonly bearerToken?: string;
|
|
62
100
|
private readonly defaultHeaders: Record<string, string>;
|
|
63
101
|
private readonly wallet?: WalletProvider;
|
|
102
|
+
private defaultCtx?: RouteContext;
|
|
103
|
+
private defaultTimeoutMs?: number;
|
|
104
|
+
private defaultIntervalMs?: number;
|
|
64
105
|
private readonly _tokenCache = new Map<string, CachedToken>();
|
|
65
106
|
|
|
66
107
|
constructor(options: ClientOptions) {
|
|
@@ -68,12 +109,95 @@ export class DataspaceNodeClient {
|
|
|
68
109
|
this.bearerToken = options.bearerToken;
|
|
69
110
|
this.defaultHeaders = options.defaultHeaders ?? {};
|
|
70
111
|
this.wallet = options.wallet;
|
|
112
|
+
this.defaultCtx = options.ctx;
|
|
71
113
|
}
|
|
72
114
|
|
|
73
115
|
public getWallet(): WalletProvider | undefined {
|
|
74
116
|
return this.wallet;
|
|
75
117
|
}
|
|
76
118
|
|
|
119
|
+
/**
|
|
120
|
+
* Set default route context for subsequent calls.
|
|
121
|
+
*/
|
|
122
|
+
public setContext(ctx: RouteContext): this {
|
|
123
|
+
this.defaultCtx = { ...ctx };
|
|
124
|
+
return this;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Preferred alias for organization/tenant integration context.
|
|
129
|
+
*/
|
|
130
|
+
public setContextOrg(ctx: RouteContext): this {
|
|
131
|
+
return this.setContext(ctx);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
public setTenantId(tenantId: string): this {
|
|
135
|
+
const current = this.defaultCtx ?? { tenantId: '', jurisdiction: '', sector: '' };
|
|
136
|
+
this.defaultCtx = { ...current, tenantId };
|
|
137
|
+
return this;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
public setJurisdiction(jurisdiction: string): this {
|
|
141
|
+
const current = this.defaultCtx ?? { tenantId: '', jurisdiction: '', sector: '' };
|
|
142
|
+
this.defaultCtx = { ...current, jurisdiction };
|
|
143
|
+
return this;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
public setSector(sector: string): this {
|
|
147
|
+
const current = this.defaultCtx ?? { tenantId: '', jurisdiction: '', sector: '' };
|
|
148
|
+
this.defaultCtx = { ...current, sector };
|
|
149
|
+
return this;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
public setDefaultTimeoutSeconds(seconds: number): this {
|
|
153
|
+
if (Number.isFinite(Number(seconds))) {
|
|
154
|
+
this.defaultTimeoutMs = Math.max(1, Math.floor(Number(seconds) * 1000));
|
|
155
|
+
}
|
|
156
|
+
return this;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
public setDefaultIntervalSeconds(seconds: number): this {
|
|
160
|
+
if (Number.isFinite(Number(seconds))) {
|
|
161
|
+
this.defaultIntervalMs = Math.max(1, Math.floor(Number(seconds) * 1000));
|
|
162
|
+
}
|
|
163
|
+
return this;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
private resolveSimplePollOptions(timeoutSeconds?: number, intervalSeconds?: number): PollOptions | undefined {
|
|
167
|
+
const pollOptions: PollOptions = {};
|
|
168
|
+
if (Number.isFinite(Number(timeoutSeconds))) {
|
|
169
|
+
pollOptions.timeoutMs = Math.max(1, Math.floor(Number(timeoutSeconds) * 1000));
|
|
170
|
+
} else if (this.defaultTimeoutMs) {
|
|
171
|
+
pollOptions.timeoutMs = this.defaultTimeoutMs;
|
|
172
|
+
}
|
|
173
|
+
if (Number.isFinite(Number(intervalSeconds))) {
|
|
174
|
+
pollOptions.intervalMs = Math.max(1, Math.floor(Number(intervalSeconds) * 1000));
|
|
175
|
+
} else if (this.defaultIntervalMs) {
|
|
176
|
+
pollOptions.intervalMs = this.defaultIntervalMs;
|
|
177
|
+
}
|
|
178
|
+
return Object.keys(pollOptions).length ? pollOptions : undefined;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
private requireRouteContext(ctx?: RouteContext): RouteContext {
|
|
182
|
+
const resolved = ctx ?? this.defaultCtx;
|
|
183
|
+
const tenantId = String(resolved?.tenantId || '').trim();
|
|
184
|
+
const jurisdiction = String(resolved?.jurisdiction || '').trim();
|
|
185
|
+
const sector = String(resolved?.sector || '').trim();
|
|
186
|
+
if (!tenantId || !jurisdiction || !sector) {
|
|
187
|
+
throw new Error('Route context is required. Provide `ctx` in method call or constructor options.');
|
|
188
|
+
}
|
|
189
|
+
return { tenantId, jurisdiction, sector };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
private requireHostRouteContext(ctx?: HostRouteContext): HostRouteContext {
|
|
193
|
+
const jurisdiction = String(ctx?.jurisdiction || this.defaultCtx?.jurisdiction || '').trim();
|
|
194
|
+
const sector = String(ctx?.sector || this.defaultCtx?.sector || '').trim();
|
|
195
|
+
if (jurisdiction && sector) {
|
|
196
|
+
return { jurisdiction, sector };
|
|
197
|
+
}
|
|
198
|
+
throw new Error('Host route context is required. Provide `ctx` in method call or constructor options.ctx.');
|
|
199
|
+
}
|
|
200
|
+
|
|
77
201
|
// ---- Path helpers -------------------------------------------------------
|
|
78
202
|
|
|
79
203
|
/**
|
|
@@ -88,13 +212,14 @@ export class DataspaceNodeClient {
|
|
|
88
212
|
* // → /acme/cds-ES/v1/health-care/individual/org.schema/Organization/_batch
|
|
89
213
|
*/
|
|
90
214
|
public v1Path(
|
|
91
|
-
ctx: RouteContext,
|
|
215
|
+
ctx: RouteContext | undefined,
|
|
92
216
|
section: V1Section,
|
|
93
217
|
format: string,
|
|
94
218
|
resourceType: string,
|
|
95
219
|
action: V1Action,
|
|
96
220
|
): string {
|
|
97
|
-
|
|
221
|
+
const routeCtx = this.requireRouteContext(ctx);
|
|
222
|
+
return `/${encode(routeCtx.tenantId)}/cds-${encode(routeCtx.jurisdiction)}/v1/${encode(routeCtx.sector)}/${encode(section)}/${encode(format)}/${encode(resourceType)}/${encode(action)}`;
|
|
98
223
|
}
|
|
99
224
|
|
|
100
225
|
/**
|
|
@@ -104,8 +229,9 @@ export class DataspaceNodeClient {
|
|
|
104
229
|
* The `prefix` is service-specific: `host` for GW, `publisher` for DataConv, `ica` for ICA.
|
|
105
230
|
* Dedicated path methods in this SDK use `host` (GW convention).
|
|
106
231
|
*/
|
|
107
|
-
public tenantIdentityPath(ctx: RouteContext, prefix: string, action: string): string {
|
|
108
|
-
|
|
232
|
+
public tenantIdentityPath(ctx: RouteContext | undefined, prefix: string, action: string): string {
|
|
233
|
+
const routeCtx = this.requireRouteContext(ctx);
|
|
234
|
+
return `/${encode(prefix)}/cds-${encode(routeCtx.jurisdiction)}/v1/${encode(routeCtx.sector)}/${encode(routeCtx.tenantId)}/identity/auth/${encode(action)}`;
|
|
109
235
|
}
|
|
110
236
|
|
|
111
237
|
/**
|
|
@@ -115,40 +241,41 @@ export class DataspaceNodeClient {
|
|
|
115
241
|
* Pattern: `/host/cds-{jurisdiction}/v1/{sector}/registry/org.schema/{resourceType}/{action}`
|
|
116
242
|
*/
|
|
117
243
|
public hostRegistryPath(
|
|
118
|
-
ctx: HostRouteContext,
|
|
244
|
+
ctx: HostRouteContext | undefined,
|
|
119
245
|
resourceType: string,
|
|
120
246
|
action: V1Action,
|
|
121
247
|
): string {
|
|
122
|
-
|
|
248
|
+
const hostCtx = this.requireHostRouteContext(ctx);
|
|
249
|
+
return `/host/cds-${encode(hostCtx.jurisdiction)}/v1/${encode(hostCtx.sector)}/registry/org.schema/${encode(resourceType)}/${encode(action)}`;
|
|
123
250
|
}
|
|
124
251
|
|
|
125
252
|
/** Submit path: host registry Organization batch (controller-level org registration). */
|
|
126
|
-
public hostRegistryOrganizationBatchPath(ctx
|
|
253
|
+
public hostRegistryOrganizationBatchPath(ctx?: HostRouteContext): string {
|
|
127
254
|
return this.hostRegistryPath(ctx, 'Organization', '_batch');
|
|
128
255
|
}
|
|
129
256
|
|
|
130
257
|
/** Poll path: host registry Organization batch. Pair with `hostRegistryOrganizationBatchPath`. */
|
|
131
|
-
public hostRegistryOrganizationPollPath(ctx
|
|
258
|
+
public hostRegistryOrganizationPollPath(ctx?: HostRouteContext): string {
|
|
132
259
|
return this.hostRegistryPath(ctx, 'Organization', '_batch-response');
|
|
133
260
|
}
|
|
134
261
|
|
|
135
262
|
/** Submit path: activate a tenant Organization in the GW registry using a VC from ICA. */
|
|
136
|
-
public hostRegistryOrganizationActivatePath(ctx
|
|
263
|
+
public hostRegistryOrganizationActivatePath(ctx?: HostRouteContext): string {
|
|
137
264
|
return this.hostRegistryPath(ctx, 'Organization', '_activate');
|
|
138
265
|
}
|
|
139
266
|
|
|
140
267
|
/** Poll path: `_activate` response. Pair with `hostRegistryOrganizationActivatePath`. */
|
|
141
|
-
public hostRegistryOrganizationActivatePollPath(ctx
|
|
268
|
+
public hostRegistryOrganizationActivatePollPath(ctx?: HostRouteContext): string {
|
|
142
269
|
return this.hostRegistryPath(ctx, 'Organization', '_activate-response');
|
|
143
270
|
}
|
|
144
271
|
|
|
145
272
|
/** Submit path: host registry Order batch (controller-level order submission). */
|
|
146
|
-
public hostRegistryOrderBatchPath(ctx
|
|
273
|
+
public hostRegistryOrderBatchPath(ctx?: HostRouteContext): string {
|
|
147
274
|
return this.hostRegistryPath(ctx, 'Order', '_batch');
|
|
148
275
|
}
|
|
149
276
|
|
|
150
277
|
/** Poll path: host registry Order batch. Pair with `hostRegistryOrderBatchPath`. */
|
|
151
|
-
public hostRegistryOrderPollPath(ctx
|
|
278
|
+
public hostRegistryOrderPollPath(ctx?: HostRouteContext): string {
|
|
152
279
|
return this.hostRegistryPath(ctx, 'Order', '_batch-response');
|
|
153
280
|
}
|
|
154
281
|
|
|
@@ -156,33 +283,37 @@ export class DataspaceNodeClient {
|
|
|
156
283
|
* Submit path: individual/family Organization onboarding (`org.schema/Organization/_batch`).
|
|
157
284
|
* Use for `family-registration/_create-or-resume` DIDComm payloads.
|
|
158
285
|
*/
|
|
159
|
-
public individualFamilyOrganizationBatchPath(ctx
|
|
160
|
-
|
|
286
|
+
public individualFamilyOrganizationBatchPath(ctx?: RouteContext): string {
|
|
287
|
+
const routeCtx = this.requireRouteContext(ctx);
|
|
288
|
+
return `/${encode(routeCtx.tenantId)}/cds-${encode(routeCtx.jurisdiction)}/v1/${encode(routeCtx.sector)}/individual/org.schema/Organization/_batch`;
|
|
161
289
|
}
|
|
162
290
|
|
|
163
291
|
/** Poll path: individual/family Organization. Pair with `individualFamilyOrganizationBatchPath`. */
|
|
164
|
-
public individualFamilyOrganizationPollPath(ctx
|
|
165
|
-
|
|
292
|
+
public individualFamilyOrganizationPollPath(ctx?: RouteContext): string {
|
|
293
|
+
const routeCtx = this.requireRouteContext(ctx);
|
|
294
|
+
return `/${encode(routeCtx.tenantId)}/cds-${encode(routeCtx.jurisdiction)}/v1/${encode(routeCtx.sector)}/individual/org.schema/Organization/_batch-response`;
|
|
166
295
|
}
|
|
167
296
|
|
|
168
297
|
/** Submit path: individual/family Organization search (`org.schema/Organization/_search`). */
|
|
169
|
-
public individualFamilyOrganizationSearchPath(ctx
|
|
298
|
+
public individualFamilyOrganizationSearchPath(ctx?: RouteContext): string {
|
|
170
299
|
return this.v1Path(ctx, 'individual', 'org.schema', 'Organization', '_search');
|
|
171
300
|
}
|
|
172
301
|
|
|
173
302
|
/** Poll path: individual/family Organization search. Pair with `individualFamilyOrganizationSearchPath`. */
|
|
174
|
-
public individualFamilyOrganizationSearchPollPath(ctx
|
|
303
|
+
public individualFamilyOrganizationSearchPollPath(ctx?: RouteContext): string {
|
|
175
304
|
return this.v1Path(ctx, 'individual', 'org.schema', 'Organization', '_search-response');
|
|
176
305
|
}
|
|
177
306
|
|
|
178
307
|
/** Submit path: individual/family Order batch (`org.schema/Order/_batch`). */
|
|
179
|
-
public individualFamilyOrderBatchPath(ctx
|
|
180
|
-
|
|
308
|
+
public individualFamilyOrderBatchPath(ctx?: RouteContext): string {
|
|
309
|
+
const routeCtx = this.requireRouteContext(ctx);
|
|
310
|
+
return `/${encode(routeCtx.tenantId)}/cds-${encode(routeCtx.jurisdiction)}/v1/${encode(routeCtx.sector)}/individual/org.schema/Order/_batch`;
|
|
181
311
|
}
|
|
182
312
|
|
|
183
313
|
/** Poll path: individual/family Order. Pair with `individualFamilyOrderBatchPath`. */
|
|
184
|
-
public individualFamilyOrderPollPath(ctx
|
|
185
|
-
|
|
314
|
+
public individualFamilyOrderPollPath(ctx?: RouteContext): string {
|
|
315
|
+
const routeCtx = this.requireRouteContext(ctx);
|
|
316
|
+
return `/${encode(routeCtx.tenantId)}/cds-${encode(routeCtx.jurisdiction)}/v1/${encode(routeCtx.sector)}/individual/org.schema/Order/_batch-response`;
|
|
186
317
|
}
|
|
187
318
|
|
|
188
319
|
/** Submit path: individual RelatedPerson (FHIR R4 API, `org.hl7.fhir.api/RelatedPerson/_batch`). */
|
|
@@ -226,12 +357,12 @@ export class DataspaceNodeClient {
|
|
|
226
357
|
}
|
|
227
358
|
|
|
228
359
|
/** Submit path: entity Employee (`entity/org.schema/Employee/_batch`). */
|
|
229
|
-
public employeeBatchPath(ctx
|
|
360
|
+
public employeeBatchPath(ctx?: RouteContext): string {
|
|
230
361
|
return this.v1Path(ctx, 'entity', 'org.schema', 'Employee', '_batch');
|
|
231
362
|
}
|
|
232
363
|
|
|
233
364
|
/** Poll path: entity Employee. Pair with `employeeBatchPath`. */
|
|
234
|
-
public employeePollPath(ctx
|
|
365
|
+
public employeePollPath(ctx?: RouteContext): string {
|
|
235
366
|
return this.v1Path(ctx, 'entity', 'org.schema', 'Employee', '_batch-response');
|
|
236
367
|
}
|
|
237
368
|
|
|
@@ -284,12 +415,12 @@ export class DataspaceNodeClient {
|
|
|
284
415
|
* Submit path: identity DCR step — binds API key to service public JWK.
|
|
285
416
|
* Used internally by `authenticateBackendPkceAndExchange` (step 1 of identity-exchange.v1).
|
|
286
417
|
*/
|
|
287
|
-
public identityDeviceDcrPath(ctx
|
|
418
|
+
public identityDeviceDcrPath(ctx?: RouteContext): string {
|
|
288
419
|
return this.tenantIdentityPath(ctx, 'host', '_dcr');
|
|
289
420
|
}
|
|
290
421
|
|
|
291
422
|
/** Poll path: identity DCR. Pair with `identityDeviceDcrPath`. */
|
|
292
|
-
public identityDeviceDcrPollPath(ctx
|
|
423
|
+
public identityDeviceDcrPollPath(ctx?: RouteContext): string {
|
|
293
424
|
return this.tenantIdentityPath(ctx, 'host', '_dcr-response');
|
|
294
425
|
}
|
|
295
426
|
|
|
@@ -297,17 +428,17 @@ export class DataspaceNodeClient {
|
|
|
297
428
|
* Submit path: identity token exchange — id_token → SMART bearer.
|
|
298
429
|
* Used internally by `authenticateBackendPkceAndExchange` (step 4 of identity-exchange.v1).
|
|
299
430
|
*/
|
|
300
|
-
public identityTokenExchangePath(ctx
|
|
431
|
+
public identityTokenExchangePath(ctx?: RouteContext): string {
|
|
301
432
|
return this.tenantIdentityPath(ctx, 'host', '_exchange');
|
|
302
433
|
}
|
|
303
434
|
|
|
304
435
|
/** Poll path: identity token exchange. Pair with `identityTokenExchangePath`. */
|
|
305
|
-
public identityTokenExchangePollPath(ctx
|
|
436
|
+
public identityTokenExchangePollPath(ctx?: RouteContext): string {
|
|
306
437
|
return this.tenantIdentityPath(ctx, 'host', '_exchange-response');
|
|
307
438
|
}
|
|
308
439
|
|
|
309
440
|
/** Submit path: identity license issue (`identity/auth/_issue`). */
|
|
310
|
-
public identityLicenseIssuePath(ctx
|
|
441
|
+
public identityLicenseIssuePath(ctx?: RouteContext): string {
|
|
311
442
|
return this.tenantIdentityPath(ctx, 'host', '_issue');
|
|
312
443
|
}
|
|
313
444
|
|
|
@@ -650,6 +781,73 @@ export class DataspaceNodeClient {
|
|
|
650
781
|
};
|
|
651
782
|
}
|
|
652
783
|
|
|
784
|
+
/**
|
|
785
|
+
* Friendly wrapper for SMART token request via GW identity/auth token-exchange route.
|
|
786
|
+
* Uses one object, seconds-based polling, and constructor ctx fallback.
|
|
787
|
+
*/
|
|
788
|
+
public async requestSmartTokenSimple(
|
|
789
|
+
input: SmartTokenRequestSimpleInput,
|
|
790
|
+
): Promise<SmartTokenExchangeResult> {
|
|
791
|
+
const routeCtx = this.requireRouteContext(
|
|
792
|
+
input.tenantId && input.jurisdiction && input.sector
|
|
793
|
+
? { tenantId: input.tenantId, jurisdiction: input.jurisdiction, sector: input.sector }
|
|
794
|
+
: undefined,
|
|
795
|
+
);
|
|
796
|
+
const normalizedScopes = Array.from(new Set((input.scopes || []).filter(Boolean))).sort();
|
|
797
|
+
const endpointId = String(input.endpointId || `smart:${routeCtx.tenantId}:${normalizedScopes.join(',')}`).trim();
|
|
798
|
+
if (!endpointId) {
|
|
799
|
+
throw new Error('requestSmartTokenSimple requires endpointId (or non-empty scopes).');
|
|
800
|
+
}
|
|
801
|
+
const pollOptions = this.resolveSimplePollOptions(input.timeoutSeconds, input.intervalSeconds);
|
|
802
|
+
|
|
803
|
+
const payload: Record<string, unknown> = {
|
|
804
|
+
thid: `exchange-${randomUUID()}`,
|
|
805
|
+
grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange',
|
|
806
|
+
subject_token_type: 'urn:ietf:params:oauth:token-type:id_token',
|
|
807
|
+
subject_token: input.idToken,
|
|
808
|
+
scope: normalizedScopes.join(' '),
|
|
809
|
+
organization: routeCtx.tenantId,
|
|
810
|
+
...(input.additionalClaims || {}),
|
|
811
|
+
};
|
|
812
|
+
|
|
813
|
+
const exchange = await this.submitAndPoll(
|
|
814
|
+
this.identityTokenExchangePath(routeCtx),
|
|
815
|
+
this.identityTokenExchangePollPath(routeCtx),
|
|
816
|
+
payload,
|
|
817
|
+
pollOptions,
|
|
818
|
+
);
|
|
819
|
+
|
|
820
|
+
const exchangeBody = (exchange.poll.body as Record<string, unknown>) ?? {};
|
|
821
|
+
const accessToken = String(exchangeBody.access_token || '').trim();
|
|
822
|
+
if (exchange.poll.status >= 400 || !accessToken) {
|
|
823
|
+
return {
|
|
824
|
+
status: 'failed',
|
|
825
|
+
statusCode: exchange.poll.status,
|
|
826
|
+
response: exchange.poll.body,
|
|
827
|
+
};
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
const tokenType = String(exchangeBody.token_type || 'Bearer');
|
|
831
|
+
const grantedScopes = String(exchangeBody.scope || '').trim().split(' ').filter(Boolean);
|
|
832
|
+
const resolvedScopes = grantedScopes.length ? grantedScopes : normalizedScopes;
|
|
833
|
+
const expiresIn = Number(exchangeBody.expires_in ?? 0);
|
|
834
|
+
this._tokenCache.set(endpointId, {
|
|
835
|
+
accessToken,
|
|
836
|
+
tokenType,
|
|
837
|
+
scopes: resolvedScopes,
|
|
838
|
+
expiresAt: Date.now() + expiresIn * 1000,
|
|
839
|
+
});
|
|
840
|
+
|
|
841
|
+
return {
|
|
842
|
+
status: 'fetched',
|
|
843
|
+
accessToken,
|
|
844
|
+
tokenType,
|
|
845
|
+
scopes: resolvedScopes,
|
|
846
|
+
statusCode: exchange.poll.status,
|
|
847
|
+
response: exchange.poll.body,
|
|
848
|
+
};
|
|
849
|
+
}
|
|
850
|
+
|
|
653
851
|
// ---- Private auth helpers ----------------------------------------------
|
|
654
852
|
|
|
655
853
|
private _pkceS256Challenge(verifier: string): string {
|
|
@@ -959,11 +1157,15 @@ export class DataspaceNodeClient {
|
|
|
959
1157
|
* Returns HTTP 202 while the job is still processing, 200 (or other) when done.
|
|
960
1158
|
* Prefer `pollUntilComplete` for automatic retry loops.
|
|
961
1159
|
*/
|
|
962
|
-
public async pollBatchResponse(
|
|
1160
|
+
public async pollBatchResponse(
|
|
1161
|
+
path: string,
|
|
1162
|
+
request: AsyncPollRequest,
|
|
1163
|
+
): Promise<{ status: number; body: unknown; retryAfterMs?: number }> {
|
|
963
1164
|
const response = await this.doPost(path, request, 'application/json');
|
|
964
1165
|
return {
|
|
965
1166
|
status: response.status,
|
|
966
1167
|
body: await this.parseResponseBody(response),
|
|
1168
|
+
retryAfterMs: parseRetryAfterMs(response.headers.get('retry-after')),
|
|
967
1169
|
};
|
|
968
1170
|
}
|
|
969
1171
|
|
|
@@ -1010,10 +1212,11 @@ export class DataspaceNodeClient {
|
|
|
1010
1212
|
* (appointment, medication schedule, or another event), mapped to `based-on-display`.
|
|
1011
1213
|
*/
|
|
1012
1214
|
public async createPhoneReminderTasks(
|
|
1013
|
-
ctx: RouteContext,
|
|
1215
|
+
ctx: RouteContext | undefined,
|
|
1014
1216
|
input: CreatePhoneReminderTasksInput,
|
|
1015
1217
|
options?: PollOptions,
|
|
1016
1218
|
): Promise<SubmitAndPollResult> {
|
|
1219
|
+
const routeCtx = this.requireRouteContext(ctx);
|
|
1017
1220
|
const windows = Array.isArray(input.windows) ? input.windows : [];
|
|
1018
1221
|
if (!windows.length) {
|
|
1019
1222
|
throw new Error('createPhoneReminderTasks requires at least one reminder window.');
|
|
@@ -1036,9 +1239,9 @@ export class DataspaceNodeClient {
|
|
|
1036
1239
|
}
|
|
1037
1240
|
|
|
1038
1241
|
const taskIdSeed = [
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1242
|
+
routeCtx.tenantId,
|
|
1243
|
+
routeCtx.jurisdiction,
|
|
1244
|
+
routeCtx.sector,
|
|
1042
1245
|
input.subjectRef,
|
|
1043
1246
|
input.ownerRef,
|
|
1044
1247
|
input.focusRef,
|
|
@@ -1081,15 +1284,15 @@ export class DataspaceNodeClient {
|
|
|
1081
1284
|
});
|
|
1082
1285
|
|
|
1083
1286
|
const payload = createDidcommPlainMessage({
|
|
1084
|
-
iss:
|
|
1085
|
-
aud:
|
|
1287
|
+
iss: routeCtx.tenantId,
|
|
1288
|
+
aud: routeCtx.tenantId,
|
|
1086
1289
|
thid,
|
|
1087
1290
|
body: { data },
|
|
1088
1291
|
});
|
|
1089
1292
|
|
|
1090
1293
|
return this.submitAndPoll(
|
|
1091
|
-
this.individualTaskBatchPath(
|
|
1092
|
-
this.individualTaskPollPath(
|
|
1294
|
+
this.individualTaskBatchPath(routeCtx),
|
|
1295
|
+
this.individualTaskPollPath(routeCtx),
|
|
1093
1296
|
payload,
|
|
1094
1297
|
options ?? { timeoutMs: 20_000, intervalMs: 1_000 },
|
|
1095
1298
|
);
|
|
@@ -1218,15 +1421,16 @@ export class DataspaceNodeClient {
|
|
|
1218
1421
|
* Returns `null` when no matching registration exists.
|
|
1219
1422
|
*/
|
|
1220
1423
|
public async searchFamilyOrganization(
|
|
1221
|
-
ctx: RouteContext,
|
|
1424
|
+
ctx: RouteContext | undefined,
|
|
1222
1425
|
filters: { controllerPhone: string; usualname: string; birthDate?: string },
|
|
1223
1426
|
options?: PollOptions,
|
|
1224
1427
|
): Promise<FamilyOrganizationSummary | null> {
|
|
1428
|
+
const routeCtx = this.requireRouteContext(ctx);
|
|
1225
1429
|
const thid = `search-${randomUUID()}`;
|
|
1226
1430
|
const claims: Record<string, unknown> = {
|
|
1227
1431
|
'org.schema.Organization.owner.telephone': filters.controllerPhone,
|
|
1228
1432
|
'org.schema.Organization.alternateName': filters.usualname,
|
|
1229
|
-
'org.schema.Service.category':
|
|
1433
|
+
'org.schema.Service.category': routeCtx.sector,
|
|
1230
1434
|
};
|
|
1231
1435
|
if (filters.birthDate) {
|
|
1232
1436
|
claims['org.schema.Organization.foundingDate'] = filters.birthDate;
|
|
@@ -1235,8 +1439,8 @@ export class DataspaceNodeClient {
|
|
|
1235
1439
|
const payload = {
|
|
1236
1440
|
jti: randomUUID(),
|
|
1237
1441
|
thid,
|
|
1238
|
-
iss:
|
|
1239
|
-
aud:
|
|
1442
|
+
iss: routeCtx.tenantId,
|
|
1443
|
+
aud: routeCtx.tenantId,
|
|
1240
1444
|
type: 'application/api+json',
|
|
1241
1445
|
body: {
|
|
1242
1446
|
data: [{
|
|
@@ -1248,8 +1452,8 @@ export class DataspaceNodeClient {
|
|
|
1248
1452
|
};
|
|
1249
1453
|
|
|
1250
1454
|
const result = await this.submitAndPoll(
|
|
1251
|
-
this.individualFamilyOrganizationSearchPath(
|
|
1252
|
-
this.individualFamilyOrganizationSearchPollPath(
|
|
1455
|
+
this.individualFamilyOrganizationSearchPath(routeCtx),
|
|
1456
|
+
this.individualFamilyOrganizationSearchPollPath(routeCtx),
|
|
1253
1457
|
payload,
|
|
1254
1458
|
options ?? { timeoutMs: 20_000, intervalMs: 1_000 },
|
|
1255
1459
|
);
|
|
@@ -1281,7 +1485,7 @@ export class DataspaceNodeClient {
|
|
|
1281
1485
|
* Activate tenant organization in GW from ICA-derived proof.
|
|
1282
1486
|
*/
|
|
1283
1487
|
public async activateOrganizationInGatewayFromIcaProof(
|
|
1284
|
-
ctx: HostRouteContext,
|
|
1488
|
+
ctx: HostRouteContext | undefined,
|
|
1285
1489
|
input: GatewayOrganizationActivationInput,
|
|
1286
1490
|
options?: PollOptions,
|
|
1287
1491
|
): Promise<SubmitAndPollResult> {
|
|
@@ -1294,6 +1498,11 @@ export class DataspaceNodeClient {
|
|
|
1294
1498
|
vp_token: input.vpToken,
|
|
1295
1499
|
...(input.additionalClaims || {}),
|
|
1296
1500
|
};
|
|
1501
|
+
const requestedMembers = Number.isFinite(Number(input.numberOfMembers))
|
|
1502
|
+
? Math.max(1, Math.floor(Number(input.numberOfMembers)))
|
|
1503
|
+
: 2;
|
|
1504
|
+
// Keep gateway-facing claim stable while exposing a generic SDK input.
|
|
1505
|
+
claims['org.schema.Organization.numberOfEmployees'] = requestedMembers;
|
|
1297
1506
|
if (input.organizationVc) claims['org.schema.OrganizationCredential.jwt'] = input.organizationVc;
|
|
1298
1507
|
if (input.legalRepresentativeVc) {
|
|
1299
1508
|
claims['org.schema.LegalRepresentativeCredential.jwt'] = input.legalRepresentativeVc;
|
|
@@ -1304,6 +1513,10 @@ export class DataspaceNodeClient {
|
|
|
1304
1513
|
iss: 'did:web:controller.example.com',
|
|
1305
1514
|
aud: 'did:web:host.example.com',
|
|
1306
1515
|
body: {
|
|
1516
|
+
// GW activation parser expects proof material at top-level DIDComm body.
|
|
1517
|
+
vp_token: input.vpToken,
|
|
1518
|
+
...(input.organizationVc ? { organizationCredential: input.organizationVc } : {}),
|
|
1519
|
+
...(input.legalRepresentativeVc ? { representativeCredential: input.legalRepresentativeVc } : {}),
|
|
1307
1520
|
data: [
|
|
1308
1521
|
{
|
|
1309
1522
|
type: 'Organization-activation-request-v1.0',
|
|
@@ -1322,6 +1535,199 @@ export class DataspaceNodeClient {
|
|
|
1322
1535
|
);
|
|
1323
1536
|
}
|
|
1324
1537
|
|
|
1538
|
+
/**
|
|
1539
|
+
* Friendly wrapper for legal organization activation.
|
|
1540
|
+
* Accepts one object and seconds-based polling options for integrator ergonomics.
|
|
1541
|
+
*/
|
|
1542
|
+
public async activateOrganizationInGatewaySimple(
|
|
1543
|
+
input: GatewayOrganizationActivationSimpleInput,
|
|
1544
|
+
): Promise<SubmitAndPollResult> {
|
|
1545
|
+
const serviceProviderDidWeb = String(input.serviceProviderDidWeb || '').trim();
|
|
1546
|
+
const serviceProviderUrl = String(input.serviceProviderUrl || '').trim();
|
|
1547
|
+
const controllerEmail = String(input.controllerEmail || '').trim();
|
|
1548
|
+
const controllerTelephone = String(input.controllerTelephone || '').trim();
|
|
1549
|
+
const controllerRole = String(input.controllerRole || '').trim();
|
|
1550
|
+
const resolvedServiceDid = toDidWebFromUrlOrHost(serviceProviderDidWeb || serviceProviderUrl);
|
|
1551
|
+
if (!resolvedServiceDid) {
|
|
1552
|
+
throw new Error('activateOrganizationInGatewaySimple requires serviceProviderDidWeb or serviceProviderUrl.');
|
|
1553
|
+
}
|
|
1554
|
+
if (!controllerEmail && !controllerTelephone) {
|
|
1555
|
+
throw new Error('activateOrganizationInGatewaySimple requires controllerEmail or controllerTelephone.');
|
|
1556
|
+
}
|
|
1557
|
+
if (!controllerRole) {
|
|
1558
|
+
throw new Error('activateOrganizationInGatewaySimple requires controllerRole.');
|
|
1559
|
+
}
|
|
1560
|
+
const pollOptions = this.resolveSimplePollOptions(input.timeoutSeconds, input.intervalSeconds);
|
|
1561
|
+
|
|
1562
|
+
const hostCtx = this.requireHostRouteContext(
|
|
1563
|
+
input.jurisdiction && input.sector
|
|
1564
|
+
? { jurisdiction: input.jurisdiction, sector: input.sector }
|
|
1565
|
+
: undefined,
|
|
1566
|
+
);
|
|
1567
|
+
const implicitClaims: Record<string, unknown> = {
|
|
1568
|
+
'org.schema.Service.category': hostCtx.sector,
|
|
1569
|
+
'org.schema.Service.identifier': resolvedServiceDid,
|
|
1570
|
+
...(serviceProviderUrl ? { 'org.schema.Service.url': serviceProviderUrl } : {}),
|
|
1571
|
+
'org.schema.Person.hasOccupation': controllerRole,
|
|
1572
|
+
...(controllerEmail ? { 'org.schema.Person.email': controllerEmail } : {}),
|
|
1573
|
+
...(controllerTelephone ? { 'org.schema.Person.telephone': controllerTelephone } : {}),
|
|
1574
|
+
};
|
|
1575
|
+
|
|
1576
|
+
const activation = await this.activateOrganizationInGatewayFromIcaProof(
|
|
1577
|
+
hostCtx,
|
|
1578
|
+
{
|
|
1579
|
+
vpToken: input.vpToken,
|
|
1580
|
+
numberOfMembers: input.numberOfMembers,
|
|
1581
|
+
organizationVc: input.organizationVc,
|
|
1582
|
+
legalRepresentativeVc: input.legalRepresentativeVc,
|
|
1583
|
+
regulatoryEvidence: input.regulatoryEvidence,
|
|
1584
|
+
additionalClaims: { ...implicitClaims, ...(input.additionalClaims || {}) },
|
|
1585
|
+
},
|
|
1586
|
+
pollOptions,
|
|
1587
|
+
);
|
|
1588
|
+
this.assertFirstDidcommEntrySuccess(activation, 'activateOrganizationInGatewaySimple');
|
|
1589
|
+
return activation;
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
/**
|
|
1593
|
+
* Friendly wrapper for legal organization Order confirmation.
|
|
1594
|
+
* Accepts one object and builds payload/paths internally.
|
|
1595
|
+
*/
|
|
1596
|
+
public async confirmLegalOrganizationOrderSimple(
|
|
1597
|
+
input: LegalOrganizationOrderSimpleInput,
|
|
1598
|
+
): Promise<SubmitAndPollResult> {
|
|
1599
|
+
if (!String(input.offerId || '').trim()) {
|
|
1600
|
+
throw new Error('confirmLegalOrganizationOrderSimple requires offerId.');
|
|
1601
|
+
}
|
|
1602
|
+
const pollOptions = this.resolveSimplePollOptions(input.timeoutSeconds, input.intervalSeconds);
|
|
1603
|
+
const hostCtx = this.requireHostRouteContext(
|
|
1604
|
+
input.jurisdiction && input.sector
|
|
1605
|
+
? { jurisdiction: input.jurisdiction, sector: input.sector }
|
|
1606
|
+
: undefined,
|
|
1607
|
+
);
|
|
1608
|
+
|
|
1609
|
+
const claims: Record<string, unknown> = {
|
|
1610
|
+
'@context': 'org.schema',
|
|
1611
|
+
'Order.acceptedOffer.identifier': input.offerId,
|
|
1612
|
+
...(input.additionalClaims || {}),
|
|
1613
|
+
};
|
|
1614
|
+
const payload = createDidcommPlainMessage({
|
|
1615
|
+
iss: 'did:web:controller.example.com',
|
|
1616
|
+
aud: 'did:web:host.example.com',
|
|
1617
|
+
thid: `order-${randomUUID()}`,
|
|
1618
|
+
body: {
|
|
1619
|
+
data: [{
|
|
1620
|
+
type: input.dataType || 'Organization-order-request-v1.0',
|
|
1621
|
+
meta: { claims }, // legacy compatibility
|
|
1622
|
+
resource: { meta: { claims } },
|
|
1623
|
+
}],
|
|
1624
|
+
},
|
|
1625
|
+
});
|
|
1626
|
+
|
|
1627
|
+
const order = await this.submitAndPoll(
|
|
1628
|
+
this.hostRegistryOrderBatchPath(hostCtx),
|
|
1629
|
+
this.hostRegistryOrderPollPath(hostCtx),
|
|
1630
|
+
payload,
|
|
1631
|
+
pollOptions,
|
|
1632
|
+
);
|
|
1633
|
+
this.assertFirstDidcommEntrySuccess(order, 'confirmLegalOrganizationOrderSimple');
|
|
1634
|
+
return order;
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
/**
|
|
1638
|
+
* Normalize GW async response into DIDComm message body.
|
|
1639
|
+
*
|
|
1640
|
+
* Transport note:
|
|
1641
|
+
* - GW poll responses are HTTP JSON envelopes
|
|
1642
|
+
* - business payload lives inside DIDComm `body`
|
|
1643
|
+
*
|
|
1644
|
+
* This helper abstracts envelope differences so consumers do not depend on
|
|
1645
|
+
* raw `poll.body.body` paths.
|
|
1646
|
+
*/
|
|
1647
|
+
public getDidcommMessageBodyFromResponse(
|
|
1648
|
+
result: SubmitAndPollResult | PollResult | unknown,
|
|
1649
|
+
): Record<string, unknown> | undefined {
|
|
1650
|
+
const pollBody = (result as any)?.poll?.body ?? (result as any)?.body ?? result;
|
|
1651
|
+
const didcommBody = (pollBody as any)?.body;
|
|
1652
|
+
if (didcommBody && typeof didcommBody === 'object') return didcommBody as Record<string, unknown>;
|
|
1653
|
+
if (pollBody && typeof pollBody === 'object' && Array.isArray((pollBody as any)?.data)) {
|
|
1654
|
+
return pollBody as Record<string, unknown>;
|
|
1655
|
+
}
|
|
1656
|
+
return undefined;
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
/**
|
|
1660
|
+
* Return first DIDComm business entry from a submit/poll result.
|
|
1661
|
+
*/
|
|
1662
|
+
public getFirstDidcommDataEntryFromResponse(
|
|
1663
|
+
result: SubmitAndPollResult | PollResult | unknown,
|
|
1664
|
+
): Record<string, unknown> | undefined {
|
|
1665
|
+
const body = this.getDidcommMessageBodyFromResponse(result);
|
|
1666
|
+
const entry = (body as any)?.data?.[0];
|
|
1667
|
+
return entry && typeof entry === 'object' ? (entry as Record<string, unknown>) : undefined;
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
/**
|
|
1671
|
+
* Extract `org.schema.Offer.identifier` from a submit/poll result.
|
|
1672
|
+
*
|
|
1673
|
+
* This helper normalizes canonical and legacy claim locations.
|
|
1674
|
+
*/
|
|
1675
|
+
public getOfferIdFromResponse(result: SubmitAndPollResult | PollResult | unknown): string | undefined {
|
|
1676
|
+
const entry = this.getFirstDidcommDataEntryFromResponse(result);
|
|
1677
|
+
const offerId = String(
|
|
1678
|
+
(entry as any)?.meta?.claims?.['org.schema.Offer.identifier']
|
|
1679
|
+
|| (entry as any)?.resource?.meta?.claims?.['org.schema.Offer.identifier']
|
|
1680
|
+
|| '',
|
|
1681
|
+
).trim();
|
|
1682
|
+
return offerId || undefined;
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
/**
|
|
1686
|
+
* Extract a UI-ready Offer preview from activation/registration responses.
|
|
1687
|
+
*/
|
|
1688
|
+
public getOfferPreviewFromResponse(result: SubmitAndPollResult | PollResult | unknown): OfferPreview {
|
|
1689
|
+
const entry = this.getFirstDidcommDataEntryFromResponse(result) as any;
|
|
1690
|
+
const claims = entry?.meta?.claims || entry?.resource?.meta?.claims || {};
|
|
1691
|
+
const seatsRaw = claims['org.schema.Offer.eligibleQuantity.value'];
|
|
1692
|
+
const seats =
|
|
1693
|
+
typeof seatsRaw === 'number'
|
|
1694
|
+
? seatsRaw
|
|
1695
|
+
: (typeof seatsRaw === 'string' && seatsRaw.trim() ? Number(seatsRaw) : undefined);
|
|
1696
|
+
return {
|
|
1697
|
+
offerId: this.getOfferIdFromResponse(result),
|
|
1698
|
+
amount: claims['org.schema.Offer.price'],
|
|
1699
|
+
currency: claims['org.schema.Offer.priceCurrency'],
|
|
1700
|
+
seats: Number.isFinite(seats as number) ? seats : undefined,
|
|
1701
|
+
planName: claims['org.schema.Offer.itemOffered.name'],
|
|
1702
|
+
sku: claims['org.schema.Offer.itemOffered.sku'],
|
|
1703
|
+
paymentMethod: claims['org.schema.Offer.acceptedPaymentMethod'],
|
|
1704
|
+
checkoutUrl: claims['org.schema.Offer.checkoutPageURLTemplate'],
|
|
1705
|
+
};
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1708
|
+
/**
|
|
1709
|
+
* Throws when first DIDComm entry contains a business-level error status.
|
|
1710
|
+
*/
|
|
1711
|
+
public assertFirstDidcommEntrySuccess(
|
|
1712
|
+
result: SubmitAndPollResult | PollResult | unknown,
|
|
1713
|
+
contextLabel: string,
|
|
1714
|
+
): void {
|
|
1715
|
+
const entry = this.getFirstDidcommDataEntryFromResponse(result) as any;
|
|
1716
|
+
const responseStatusRaw = entry?.response?.status;
|
|
1717
|
+
const responseStatus = Number(responseStatusRaw);
|
|
1718
|
+
if (!Number.isFinite(responseStatus) || responseStatus < 400) return;
|
|
1719
|
+
|
|
1720
|
+
const diagnostics =
|
|
1721
|
+
String(
|
|
1722
|
+
entry?.response?.outcome?.issue?.[0]?.diagnostics
|
|
1723
|
+
|| entry?.response?.outcome?.issue?.[0]?.details?.text
|
|
1724
|
+
|| '',
|
|
1725
|
+
).trim();
|
|
1726
|
+
throw new Error(
|
|
1727
|
+
`${contextLabel} failed (business status=${responseStatus})${diagnostics ? `: ${diagnostics}` : ''}`,
|
|
1728
|
+
);
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1325
1731
|
/**
|
|
1326
1732
|
* Activate employee/member device by activation code exchange + DCR registration.
|
|
1327
1733
|
*
|
|
@@ -1329,7 +1735,7 @@ export class DataspaceNodeClient {
|
|
|
1329
1735
|
* Step 2. Register device keys through Device/_dcr authorized by that initial token.
|
|
1330
1736
|
*/
|
|
1331
1737
|
public async activateEmployeeDeviceWithActivationCode(
|
|
1332
|
-
ctx: RouteContext,
|
|
1738
|
+
ctx: RouteContext | undefined,
|
|
1333
1739
|
input: EmployeeDeviceActivationInput,
|
|
1334
1740
|
): Promise<EmployeeDeviceActivationResult> {
|
|
1335
1741
|
const exchangePayload = {
|
|
@@ -1385,17 +1791,43 @@ export class DataspaceNodeClient {
|
|
|
1385
1791
|
};
|
|
1386
1792
|
}
|
|
1387
1793
|
|
|
1794
|
+
/**
|
|
1795
|
+
* Friendly wrapper for employee/member device activation.
|
|
1796
|
+
* Uses one object, seconds-based polling, and constructor ctx fallback.
|
|
1797
|
+
*/
|
|
1798
|
+
public async activateEmployeeDeviceWithActivationCodeSimple(
|
|
1799
|
+
input: EmployeeDeviceActivationSimpleInput,
|
|
1800
|
+
): Promise<EmployeeDeviceActivationResult> {
|
|
1801
|
+
const routeCtx = this.requireRouteContext(
|
|
1802
|
+
input.tenantId && input.jurisdiction && input.sector
|
|
1803
|
+
? { tenantId: input.tenantId, jurisdiction: input.jurisdiction, sector: input.sector }
|
|
1804
|
+
: undefined,
|
|
1805
|
+
);
|
|
1806
|
+
const pollOptions = this.resolveSimplePollOptions(input.timeoutSeconds, input.intervalSeconds);
|
|
1807
|
+
|
|
1808
|
+
return this.activateEmployeeDeviceWithActivationCode(
|
|
1809
|
+
routeCtx,
|
|
1810
|
+
{
|
|
1811
|
+
activationCode: input.activationCode,
|
|
1812
|
+
idToken: input.idToken,
|
|
1813
|
+
dcrPayload: input.dcrPayload,
|
|
1814
|
+
pollOptions,
|
|
1815
|
+
},
|
|
1816
|
+
);
|
|
1817
|
+
}
|
|
1818
|
+
|
|
1388
1819
|
/**
|
|
1389
1820
|
* UC 5.3 wrapper: create organization employee in entity Employee batch route.
|
|
1390
1821
|
*/
|
|
1391
1822
|
public async createOrganizationEmployee(
|
|
1392
|
-
ctx: RouteContext,
|
|
1823
|
+
ctx: RouteContext | undefined,
|
|
1393
1824
|
input: OrganizationEmployeeCreationInput,
|
|
1394
1825
|
options?: PollOptions,
|
|
1395
1826
|
): Promise<SubmitAndPollResult> {
|
|
1827
|
+
const routeCtx = this.requireRouteContext(ctx);
|
|
1396
1828
|
const payload = createDidcommPlainMessage({
|
|
1397
|
-
iss:
|
|
1398
|
-
aud:
|
|
1829
|
+
iss: routeCtx.tenantId,
|
|
1830
|
+
aud: routeCtx.tenantId,
|
|
1399
1831
|
thid: `employee-${randomUUID()}`,
|
|
1400
1832
|
body: {
|
|
1401
1833
|
data: [
|
|
@@ -1409,8 +1841,8 @@ export class DataspaceNodeClient {
|
|
|
1409
1841
|
});
|
|
1410
1842
|
|
|
1411
1843
|
return this.submitAndPoll(
|
|
1412
|
-
this.employeeBatchPath(
|
|
1413
|
-
this.employeePollPath(
|
|
1844
|
+
this.employeeBatchPath(routeCtx),
|
|
1845
|
+
this.employeePollPath(routeCtx),
|
|
1414
1846
|
payload,
|
|
1415
1847
|
options,
|
|
1416
1848
|
);
|
|
@@ -1420,7 +1852,7 @@ export class DataspaceNodeClient {
|
|
|
1420
1852
|
* UC 5.1 wrapper: bootstrap subject organization context via registration + optional order confirmation.
|
|
1421
1853
|
*/
|
|
1422
1854
|
public async bootstrapSubjectOrganizationIndex(
|
|
1423
|
-
ctx: RouteContext,
|
|
1855
|
+
ctx: RouteContext | undefined,
|
|
1424
1856
|
input: SubjectOrganizationBootstrapInput,
|
|
1425
1857
|
): Promise<SubjectOrganizationBootstrapResult> {
|
|
1426
1858
|
const registrationPayload = {
|
|
@@ -1454,24 +1886,151 @@ export class DataspaceNodeClient {
|
|
|
1454
1886
|
return { registration, confirmation };
|
|
1455
1887
|
}
|
|
1456
1888
|
|
|
1889
|
+
/**
|
|
1890
|
+
* Friendly wrapper (recommended step 1): register individual organization and return Offer.
|
|
1891
|
+
*/
|
|
1892
|
+
public async startIndividualOrganizationSimple(
|
|
1893
|
+
input: IndividualOrganizationBootstrapSimpleInput,
|
|
1894
|
+
): Promise<IndividualOrganizationStartSimpleResult> {
|
|
1895
|
+
const routeCtx = this.requireRouteContext(
|
|
1896
|
+
input.tenantId && input.jurisdiction && input.sector
|
|
1897
|
+
? { tenantId: input.tenantId, jurisdiction: input.jurisdiction, sector: input.sector }
|
|
1898
|
+
: undefined,
|
|
1899
|
+
);
|
|
1900
|
+
const alternateName = String(input.alternateName || '').trim();
|
|
1901
|
+
if (!alternateName) {
|
|
1902
|
+
throw new Error('bootstrapIndividualOrganizationSimple requires alternateName.');
|
|
1903
|
+
}
|
|
1904
|
+
const controllerEmail = String(input.controllerEmail || '').trim();
|
|
1905
|
+
const controllerTelephone = String(input.controllerTelephone || '').trim();
|
|
1906
|
+
if (!controllerEmail && !controllerTelephone) {
|
|
1907
|
+
throw new Error('bootstrapIndividualOrganizationSimple requires controllerEmail or controllerTelephone.');
|
|
1908
|
+
}
|
|
1909
|
+
const controllerRole = String(input.controllerRole || 'org.hl7.v3.RoleCode|RESPRSN').trim();
|
|
1910
|
+
|
|
1911
|
+
const claims: Record<string, unknown> = {
|
|
1912
|
+
'@context': 'org.schema',
|
|
1913
|
+
'org.schema.Organization.alternateName': alternateName,
|
|
1914
|
+
'org.schema.Service.category': routeCtx.sector,
|
|
1915
|
+
'org.schema.Person.hasOccupation': controllerRole,
|
|
1916
|
+
...(controllerEmail ? { 'org.schema.Person.email': controllerEmail } : {}),
|
|
1917
|
+
...(controllerTelephone ? { 'org.schema.Person.telephone': controllerTelephone } : {}),
|
|
1918
|
+
...(input.additionalClaims || {}),
|
|
1919
|
+
};
|
|
1920
|
+
const pollOptions = this.resolveSimplePollOptions(input.timeoutSeconds, input.intervalSeconds);
|
|
1921
|
+
const registrationPayload = createDidcommPlainMessage({
|
|
1922
|
+
iss: routeCtx.tenantId,
|
|
1923
|
+
aud: routeCtx.tenantId,
|
|
1924
|
+
thid: `family-org-${randomUUID()}`,
|
|
1925
|
+
body: {
|
|
1926
|
+
data: [{
|
|
1927
|
+
type: 'SubjectOrg-registration-form-v1.0',
|
|
1928
|
+
meta: { claims },
|
|
1929
|
+
resource: { meta: { claims } },
|
|
1930
|
+
}],
|
|
1931
|
+
},
|
|
1932
|
+
});
|
|
1933
|
+
|
|
1934
|
+
const registration = await this.submitAndPoll(
|
|
1935
|
+
this.individualFamilyOrganizationBatchPath(routeCtx),
|
|
1936
|
+
this.individualFamilyOrganizationPollPath(routeCtx),
|
|
1937
|
+
registrationPayload,
|
|
1938
|
+
pollOptions,
|
|
1939
|
+
);
|
|
1940
|
+
this.assertFirstDidcommEntrySuccess(registration, 'startIndividualOrganizationSimple.registration');
|
|
1941
|
+
|
|
1942
|
+
const offerId = this.getOfferIdFromResponse(registration);
|
|
1943
|
+
if (!offerId) {
|
|
1944
|
+
throw new Error('startIndividualOrganizationSimple failed: missing offerId in registration response.');
|
|
1945
|
+
}
|
|
1946
|
+
return { registration, offerId, offerPreview: this.getOfferPreviewFromResponse(registration) };
|
|
1947
|
+
}
|
|
1948
|
+
|
|
1949
|
+
/**
|
|
1950
|
+
* Friendly wrapper (recommended step 2): confirm individual/family order from accepted offerId.
|
|
1951
|
+
*/
|
|
1952
|
+
public async confirmIndividualOrganizationOrderSimple(
|
|
1953
|
+
input: IndividualOrganizationConfirmOrderSimpleInput,
|
|
1954
|
+
): Promise<SubmitAndPollResult> {
|
|
1955
|
+
const routeCtx = this.requireRouteContext(
|
|
1956
|
+
input.tenantId && input.jurisdiction && input.sector
|
|
1957
|
+
? { tenantId: input.tenantId, jurisdiction: input.jurisdiction, sector: input.sector }
|
|
1958
|
+
: undefined,
|
|
1959
|
+
);
|
|
1960
|
+
const offerId = String(input.offerId || '').trim();
|
|
1961
|
+
if (!offerId) {
|
|
1962
|
+
throw new Error('confirmIndividualOrganizationOrderSimple requires offerId.');
|
|
1963
|
+
}
|
|
1964
|
+
const pollOptions = this.resolveSimplePollOptions(input.timeoutSeconds, input.intervalSeconds);
|
|
1965
|
+
const orderClaims: Record<string, unknown> = {
|
|
1966
|
+
'@context': 'org.schema',
|
|
1967
|
+
'Order.acceptedOffer.identifier': offerId,
|
|
1968
|
+
};
|
|
1969
|
+
const confirmationPayload = createDidcommPlainMessage({
|
|
1970
|
+
iss: routeCtx.tenantId,
|
|
1971
|
+
aud: routeCtx.tenantId,
|
|
1972
|
+
thid: `family-order-${randomUUID()}`,
|
|
1973
|
+
body: {
|
|
1974
|
+
data: [{
|
|
1975
|
+
type: 'Family-order-request-v1.0',
|
|
1976
|
+
meta: { claims: orderClaims },
|
|
1977
|
+
resource: { meta: { claims: orderClaims } },
|
|
1978
|
+
}],
|
|
1979
|
+
},
|
|
1980
|
+
});
|
|
1981
|
+
|
|
1982
|
+
const confirmation = await this.submitAndPoll(
|
|
1983
|
+
this.individualFamilyOrderBatchPath(routeCtx),
|
|
1984
|
+
this.individualFamilyOrderPollPath(routeCtx),
|
|
1985
|
+
confirmationPayload,
|
|
1986
|
+
pollOptions,
|
|
1987
|
+
);
|
|
1988
|
+
this.assertFirstDidcommEntrySuccess(confirmation, 'confirmIndividualOrganizationOrderSimple');
|
|
1989
|
+
return confirmation;
|
|
1990
|
+
}
|
|
1991
|
+
|
|
1992
|
+
/**
|
|
1993
|
+
* Friendly wrapper (provisional): register + auto-confirm individual order.
|
|
1994
|
+
* Prefer `startIndividualOrganizationSimple` + `confirmIndividualOrganizationOrderSimple`.
|
|
1995
|
+
*/
|
|
1996
|
+
public async bootstrapIndividualOrganizationSimple(
|
|
1997
|
+
input: IndividualOrganizationBootstrapSimpleInput,
|
|
1998
|
+
): Promise<IndividualOrganizationBootstrapSimpleResult> {
|
|
1999
|
+
const started = await this.startIndividualOrganizationSimple(input);
|
|
2000
|
+
const confirmation = await this.confirmIndividualOrganizationOrderSimple({
|
|
2001
|
+
tenantId: input.tenantId,
|
|
2002
|
+
jurisdiction: input.jurisdiction,
|
|
2003
|
+
sector: input.sector,
|
|
2004
|
+
offerId: started.offerId,
|
|
2005
|
+
timeoutSeconds: input.timeoutSeconds,
|
|
2006
|
+
intervalSeconds: input.intervalSeconds,
|
|
2007
|
+
});
|
|
2008
|
+
return {
|
|
2009
|
+
registration: started.registration,
|
|
2010
|
+
offerId: started.offerId,
|
|
2011
|
+
confirmation,
|
|
2012
|
+
};
|
|
2013
|
+
}
|
|
2014
|
+
|
|
1457
2015
|
/**
|
|
1458
2016
|
* UC 5.5 wrapper: import IPS/FHIR composition and update subject index context.
|
|
1459
2017
|
*/
|
|
1460
2018
|
public async importIpsOrFhirAndUpdateIndex(
|
|
1461
|
-
ctx: RouteContext,
|
|
2019
|
+
ctx: RouteContext | undefined,
|
|
1462
2020
|
input: IpsOrFhirImportInput,
|
|
1463
2021
|
): Promise<SubmitAndPollResult> {
|
|
2022
|
+
const routeCtx = this.requireRouteContext(ctx);
|
|
1464
2023
|
const payload = {
|
|
1465
2024
|
thid: input.compositionPayload.thid || `composition-${randomUUID()}`,
|
|
1466
2025
|
...input.compositionPayload,
|
|
1467
2026
|
};
|
|
1468
2027
|
|
|
1469
2028
|
const submitPath = (input.format || 'r4') === 'api'
|
|
1470
|
-
? this.individualCompositionR4BatchPath(
|
|
1471
|
-
: this.individualCompositionR4BatchPath(
|
|
2029
|
+
? this.individualCompositionR4BatchPath(routeCtx).replace('/org.hl7.fhir.r4/', '/org.hl7.fhir.api/')
|
|
2030
|
+
: this.individualCompositionR4BatchPath(routeCtx);
|
|
1472
2031
|
const pollPath = (input.format || 'r4') === 'api'
|
|
1473
|
-
? this.individualCompositionR4PollPath(
|
|
1474
|
-
: this.individualCompositionR4PollPath(
|
|
2032
|
+
? this.individualCompositionR4PollPath(routeCtx).replace('/org.hl7.fhir.r4/', '/org.hl7.fhir.api/')
|
|
2033
|
+
: this.individualCompositionR4PollPath(routeCtx);
|
|
1475
2034
|
|
|
1476
2035
|
return this.submitAndPoll(submitPath, pollPath, payload, input.pollOptions);
|
|
1477
2036
|
}
|
|
@@ -1481,9 +2040,10 @@ export class DataspaceNodeClient {
|
|
|
1481
2040
|
* Builds canonical Consent claims and submits/polls the Consent batch.
|
|
1482
2041
|
*/
|
|
1483
2042
|
public async grantProfessionalAccessSimple(
|
|
1484
|
-
ctx: RouteContext,
|
|
2043
|
+
ctx: RouteContext | undefined,
|
|
1485
2044
|
input: GrantProfessionalAccessSimpleInput,
|
|
1486
2045
|
): Promise<GrantProfessionalAccessSimpleResult> {
|
|
2046
|
+
const routeCtx = this.requireRouteContext(ctx);
|
|
1487
2047
|
const built = buildConsentClaimsSimpleWithCid(
|
|
1488
2048
|
{
|
|
1489
2049
|
subjectDid: input.subjectDid,
|
|
@@ -1518,8 +2078,8 @@ export class DataspaceNodeClient {
|
|
|
1518
2078
|
},
|
|
1519
2079
|
};
|
|
1520
2080
|
const consent = await this.submitAndPoll(
|
|
1521
|
-
this.individualConsentR4BatchPath(
|
|
1522
|
-
this.individualConsentR4PollPath(
|
|
2081
|
+
this.individualConsentR4BatchPath(routeCtx),
|
|
2082
|
+
this.individualConsentR4PollPath(routeCtx),
|
|
1523
2083
|
consentPayload,
|
|
1524
2084
|
input.pollOptions,
|
|
1525
2085
|
);
|
|
@@ -1538,20 +2098,21 @@ export class DataspaceNodeClient {
|
|
|
1538
2098
|
* UC 5.7 wrapper: generate digital twin composition from subject data.
|
|
1539
2099
|
*/
|
|
1540
2100
|
public async generateDigitalTwinFromSubjectData(
|
|
1541
|
-
ctx: RouteContext,
|
|
2101
|
+
ctx: RouteContext | undefined,
|
|
1542
2102
|
input: DigitalTwinGenerationInput,
|
|
1543
2103
|
): Promise<SubmitAndPollResult> {
|
|
2104
|
+
const routeCtx = this.requireRouteContext(ctx);
|
|
1544
2105
|
const payload = {
|
|
1545
2106
|
thid: input.compositionPayload.thid || `digital-twin-${randomUUID()}`,
|
|
1546
2107
|
...input.compositionPayload,
|
|
1547
2108
|
};
|
|
1548
2109
|
|
|
1549
2110
|
const submitPath = (input.format || 'r4') === 'api'
|
|
1550
|
-
? this.digitalTwinCompositionApiBatchPath(
|
|
1551
|
-
: this.digitalTwinCompositionR4BatchPath(
|
|
2111
|
+
? this.digitalTwinCompositionApiBatchPath(routeCtx)
|
|
2112
|
+
: this.digitalTwinCompositionR4BatchPath(routeCtx);
|
|
1552
2113
|
const pollPath = (input.format || 'r4') === 'api'
|
|
1553
|
-
? this.digitalTwinCompositionApiPollPath(
|
|
1554
|
-
: this.digitalTwinCompositionR4PollPath(
|
|
2114
|
+
? this.digitalTwinCompositionApiPollPath(routeCtx)
|
|
2115
|
+
: this.digitalTwinCompositionR4PollPath(routeCtx);
|
|
1555
2116
|
|
|
1556
2117
|
return this.submitAndPoll(submitPath, pollPath, payload, input.pollOptions);
|
|
1557
2118
|
}
|
|
@@ -1587,7 +2148,8 @@ export class DataspaceNodeClient {
|
|
|
1587
2148
|
throw new Error(`Polling timeout after ${attempts} attempts (${timeoutMs}ms).`);
|
|
1588
2149
|
}
|
|
1589
2150
|
|
|
1590
|
-
|
|
2151
|
+
const waitMs = options?.intervalMs ?? result.retryAfterMs ?? intervalMs;
|
|
2152
|
+
await new Promise((resolve) => setTimeout(resolve, waitMs));
|
|
1591
2153
|
}
|
|
1592
2154
|
}
|
|
1593
2155
|
|