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/src/client.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { createHash, randomUUID } from 'node:crypto';
|
|
2
2
|
import { createDidcommPlainMessage } from './builders.js';
|
|
3
3
|
import { buildConsentClaimsSimpleWithCid } from 'gdc-common-utils-ts/utils/consent';
|
|
4
|
+
import { generateServiceId } from 'gdc-common-utils-ts/utils/did';
|
|
5
|
+
import { submitDidcomm, type DidcommFetchInit } from 'gdc-common-utils-ts/utils/didcomm-submit';
|
|
6
|
+
import { ClaimsOfferSchemaorg, ClaimsPersonSchemaorg } from 'gdc-common-utils-ts/constants/schemaorg';
|
|
4
7
|
import {
|
|
5
8
|
MedicationStatementClaimsFhirApi,
|
|
6
9
|
MedicationStatementClaimsFhirApiExtended,
|
|
@@ -17,10 +20,13 @@ import type {
|
|
|
17
20
|
GrantProfessionalAccessSimpleResult,
|
|
18
21
|
DigitalTwinGenerationInput,
|
|
19
22
|
EmployeeDeviceActivationInput,
|
|
23
|
+
EndpointSelector,
|
|
24
|
+
EmployeeDeviceActivationSimpleInput,
|
|
20
25
|
EmployeeDeviceActivationResult,
|
|
21
26
|
FamilyOrganizationSummary,
|
|
22
27
|
FamilyRegistrationStatus,
|
|
23
28
|
GatewayOrganizationActivationInput,
|
|
29
|
+
GatewayOrganizationActivationSimpleInput,
|
|
24
30
|
HostRouteContext,
|
|
25
31
|
IpsOrFhirImportInput,
|
|
26
32
|
MedicationOverlapCheckInput,
|
|
@@ -28,11 +34,19 @@ import type {
|
|
|
28
34
|
OrganizationEmployeeCreationInput,
|
|
29
35
|
PollOptions,
|
|
30
36
|
PollResult,
|
|
37
|
+
OfferPreview,
|
|
38
|
+
OfferInfo,
|
|
31
39
|
RouteContext,
|
|
32
40
|
SmartTokenExchangeInput,
|
|
33
41
|
SmartTokenExchangeResult,
|
|
42
|
+
SmartTokenRequestSimpleInput,
|
|
43
|
+
LegalOrganizationOrderSimpleInput,
|
|
34
44
|
SubjectOrganizationBootstrapInput,
|
|
35
45
|
SubjectOrganizationBootstrapResult,
|
|
46
|
+
IndividualOrganizationBootstrapSimpleInput,
|
|
47
|
+
IndividualOrganizationBootstrapSimpleResult,
|
|
48
|
+
IndividualOrganizationStartSimpleResult,
|
|
49
|
+
IndividualOrganizationConfirmOrderSimpleInput,
|
|
36
50
|
SubmitAndPollResult,
|
|
37
51
|
SubmitResponse,
|
|
38
52
|
V1Action,
|
|
@@ -49,6 +63,35 @@ function encode(value: string): string {
|
|
|
49
63
|
return encodeURIComponent(value);
|
|
50
64
|
}
|
|
51
65
|
|
|
66
|
+
function toDidWebFromUrlOrHost(raw: string): string | undefined {
|
|
67
|
+
const v = String(raw || '').trim();
|
|
68
|
+
if (!v) return undefined;
|
|
69
|
+
if (v.startsWith('did:web:')) return v;
|
|
70
|
+
const host = v
|
|
71
|
+
.replace(/^https?:\/\//i, '')
|
|
72
|
+
.replace(/\/.*$/, '')
|
|
73
|
+
.trim()
|
|
74
|
+
.toLowerCase();
|
|
75
|
+
if (!host) return undefined;
|
|
76
|
+
return `did:web:${host}`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function parseRetryAfterMs(header: string | null): number | undefined {
|
|
80
|
+
if (!header) return undefined;
|
|
81
|
+
const raw = header.trim();
|
|
82
|
+
if (!raw) return undefined;
|
|
83
|
+
const seconds = Number(raw);
|
|
84
|
+
if (Number.isFinite(seconds) && seconds >= 0) {
|
|
85
|
+
return Math.floor(seconds * 1000);
|
|
86
|
+
}
|
|
87
|
+
const epochMs = Date.parse(raw);
|
|
88
|
+
if (Number.isFinite(epochMs)) {
|
|
89
|
+
const delta = epochMs - Date.now();
|
|
90
|
+
return delta > 0 ? delta : 0;
|
|
91
|
+
}
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
|
|
52
95
|
type CachedToken = {
|
|
53
96
|
accessToken: string;
|
|
54
97
|
tokenType: string;
|
|
@@ -61,6 +104,9 @@ export class DataspaceNodeClient {
|
|
|
61
104
|
private readonly bearerToken?: string;
|
|
62
105
|
private readonly defaultHeaders: Record<string, string>;
|
|
63
106
|
private readonly wallet?: WalletProvider;
|
|
107
|
+
private defaultCtx?: RouteContext;
|
|
108
|
+
private defaultTimeoutMs?: number;
|
|
109
|
+
private defaultIntervalMs?: number;
|
|
64
110
|
private readonly _tokenCache = new Map<string, CachedToken>();
|
|
65
111
|
|
|
66
112
|
constructor(options: ClientOptions) {
|
|
@@ -68,12 +114,108 @@ export class DataspaceNodeClient {
|
|
|
68
114
|
this.bearerToken = options.bearerToken;
|
|
69
115
|
this.defaultHeaders = options.defaultHeaders ?? {};
|
|
70
116
|
this.wallet = options.wallet;
|
|
117
|
+
this.defaultCtx = options.ctx;
|
|
71
118
|
}
|
|
72
119
|
|
|
73
120
|
public getWallet(): WalletProvider | undefined {
|
|
74
121
|
return this.wallet;
|
|
75
122
|
}
|
|
76
123
|
|
|
124
|
+
/**
|
|
125
|
+
* Set default route context for subsequent calls.
|
|
126
|
+
*/
|
|
127
|
+
public setContext(ctx: RouteContext): this {
|
|
128
|
+
this.defaultCtx = { ...ctx };
|
|
129
|
+
return this;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Preferred alias for organization/tenant integration context.
|
|
134
|
+
*/
|
|
135
|
+
public setContextOrg(ctx: RouteContext): this {
|
|
136
|
+
return this.setContext(ctx);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
public setTenantId(tenantId: string): this {
|
|
140
|
+
const current = this.defaultCtx ?? { tenantId: '', jurisdiction: '', sector: '' };
|
|
141
|
+
this.defaultCtx = { ...current, tenantId };
|
|
142
|
+
return this;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
public setJurisdiction(jurisdiction: string): this {
|
|
146
|
+
const current = this.defaultCtx ?? { tenantId: '', jurisdiction: '', sector: '' };
|
|
147
|
+
this.defaultCtx = { ...current, jurisdiction };
|
|
148
|
+
return this;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
public setSector(sector: string): this {
|
|
152
|
+
const current = this.defaultCtx ?? { tenantId: '', jurisdiction: '', sector: '' };
|
|
153
|
+
this.defaultCtx = { ...current, sector };
|
|
154
|
+
return this;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
public setDefaultTimeoutSeconds(seconds: number): this {
|
|
158
|
+
if (Number.isFinite(Number(seconds))) {
|
|
159
|
+
this.defaultTimeoutMs = Math.max(1, Math.floor(Number(seconds) * 1000));
|
|
160
|
+
}
|
|
161
|
+
return this;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
public setDefaultIntervalSeconds(seconds: number): this {
|
|
165
|
+
if (Number.isFinite(Number(seconds))) {
|
|
166
|
+
this.defaultIntervalMs = Math.max(1, Math.floor(Number(seconds) * 1000));
|
|
167
|
+
}
|
|
168
|
+
return this;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Builds a deterministic endpoint id for token cache and auth/session reuse.
|
|
173
|
+
* If `providerDid` is provided, returns a full DID service id:
|
|
174
|
+
* did:web:...#section:format:resourceType:action
|
|
175
|
+
* Otherwise returns the canonical fragment without '#':
|
|
176
|
+
* section:format:resourceType:action
|
|
177
|
+
*/
|
|
178
|
+
public getEndpointId(selector: EndpointSelector, providerDid?: string): string {
|
|
179
|
+
const fragment = generateServiceId(selector); // #section:format:resourceType:action
|
|
180
|
+
if (providerDid) return `${providerDid}${fragment}`;
|
|
181
|
+
return fragment.replace(/^#/, '');
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
private resolveSimplePollOptions(timeoutSeconds?: number, intervalSeconds?: number): PollOptions | undefined {
|
|
185
|
+
const pollOptions: PollOptions = {};
|
|
186
|
+
if (Number.isFinite(Number(timeoutSeconds))) {
|
|
187
|
+
pollOptions.timeoutMs = Math.max(1, Math.floor(Number(timeoutSeconds) * 1000));
|
|
188
|
+
} else if (this.defaultTimeoutMs) {
|
|
189
|
+
pollOptions.timeoutMs = this.defaultTimeoutMs;
|
|
190
|
+
}
|
|
191
|
+
if (Number.isFinite(Number(intervalSeconds))) {
|
|
192
|
+
pollOptions.intervalMs = Math.max(1, Math.floor(Number(intervalSeconds) * 1000));
|
|
193
|
+
} else if (this.defaultIntervalMs) {
|
|
194
|
+
pollOptions.intervalMs = this.defaultIntervalMs;
|
|
195
|
+
}
|
|
196
|
+
return Object.keys(pollOptions).length ? pollOptions : undefined;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
private requireRouteContext(ctx?: RouteContext): RouteContext {
|
|
200
|
+
const resolved = ctx ?? this.defaultCtx;
|
|
201
|
+
const tenantId = String(resolved?.tenantId || '').trim();
|
|
202
|
+
const jurisdiction = String(resolved?.jurisdiction || '').trim();
|
|
203
|
+
const sector = String(resolved?.sector || '').trim();
|
|
204
|
+
if (!tenantId || !jurisdiction || !sector) {
|
|
205
|
+
throw new Error('Route context is required. Provide `ctx` in method call or constructor options.');
|
|
206
|
+
}
|
|
207
|
+
return { tenantId, jurisdiction, sector };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
private requireHostRouteContext(ctx?: HostRouteContext): HostRouteContext {
|
|
211
|
+
const jurisdiction = String(ctx?.jurisdiction || this.defaultCtx?.jurisdiction || '').trim();
|
|
212
|
+
const sector = String(ctx?.sector || this.defaultCtx?.sector || '').trim();
|
|
213
|
+
if (jurisdiction && sector) {
|
|
214
|
+
return { jurisdiction, sector };
|
|
215
|
+
}
|
|
216
|
+
throw new Error('Host route context is required. Provide `ctx` in method call or constructor options.ctx.');
|
|
217
|
+
}
|
|
218
|
+
|
|
77
219
|
// ---- Path helpers -------------------------------------------------------
|
|
78
220
|
|
|
79
221
|
/**
|
|
@@ -88,13 +230,14 @@ export class DataspaceNodeClient {
|
|
|
88
230
|
* // → /acme/cds-ES/v1/health-care/individual/org.schema/Organization/_batch
|
|
89
231
|
*/
|
|
90
232
|
public v1Path(
|
|
91
|
-
ctx: RouteContext,
|
|
233
|
+
ctx: RouteContext | undefined,
|
|
92
234
|
section: V1Section,
|
|
93
235
|
format: string,
|
|
94
236
|
resourceType: string,
|
|
95
237
|
action: V1Action,
|
|
96
238
|
): string {
|
|
97
|
-
|
|
239
|
+
const routeCtx = this.requireRouteContext(ctx);
|
|
240
|
+
return `/${encode(routeCtx.tenantId)}/cds-${encode(routeCtx.jurisdiction)}/v1/${encode(routeCtx.sector)}/${encode(section)}/${encode(format)}/${encode(resourceType)}/${encode(action)}`;
|
|
98
241
|
}
|
|
99
242
|
|
|
100
243
|
/**
|
|
@@ -104,8 +247,9 @@ export class DataspaceNodeClient {
|
|
|
104
247
|
* The `prefix` is service-specific: `host` for GW, `publisher` for DataConv, `ica` for ICA.
|
|
105
248
|
* Dedicated path methods in this SDK use `host` (GW convention).
|
|
106
249
|
*/
|
|
107
|
-
public tenantIdentityPath(ctx: RouteContext, prefix: string, action: string): string {
|
|
108
|
-
|
|
250
|
+
public tenantIdentityPath(ctx: RouteContext | undefined, prefix: string, action: string): string {
|
|
251
|
+
const routeCtx = this.requireRouteContext(ctx);
|
|
252
|
+
return `/${encode(prefix)}/cds-${encode(routeCtx.jurisdiction)}/v1/${encode(routeCtx.sector)}/${encode(routeCtx.tenantId)}/identity/auth/${encode(action)}`;
|
|
109
253
|
}
|
|
110
254
|
|
|
111
255
|
/**
|
|
@@ -115,40 +259,41 @@ export class DataspaceNodeClient {
|
|
|
115
259
|
* Pattern: `/host/cds-{jurisdiction}/v1/{sector}/registry/org.schema/{resourceType}/{action}`
|
|
116
260
|
*/
|
|
117
261
|
public hostRegistryPath(
|
|
118
|
-
ctx: HostRouteContext,
|
|
262
|
+
ctx: HostRouteContext | undefined,
|
|
119
263
|
resourceType: string,
|
|
120
264
|
action: V1Action,
|
|
121
265
|
): string {
|
|
122
|
-
|
|
266
|
+
const hostCtx = this.requireHostRouteContext(ctx);
|
|
267
|
+
return `/host/cds-${encode(hostCtx.jurisdiction)}/v1/${encode(hostCtx.sector)}/registry/org.schema/${encode(resourceType)}/${encode(action)}`;
|
|
123
268
|
}
|
|
124
269
|
|
|
125
270
|
/** Submit path: host registry Organization batch (controller-level org registration). */
|
|
126
|
-
public hostRegistryOrganizationBatchPath(ctx
|
|
271
|
+
public hostRegistryOrganizationBatchPath(ctx?: HostRouteContext): string {
|
|
127
272
|
return this.hostRegistryPath(ctx, 'Organization', '_batch');
|
|
128
273
|
}
|
|
129
274
|
|
|
130
275
|
/** Poll path: host registry Organization batch. Pair with `hostRegistryOrganizationBatchPath`. */
|
|
131
|
-
public hostRegistryOrganizationPollPath(ctx
|
|
276
|
+
public hostRegistryOrganizationPollPath(ctx?: HostRouteContext): string {
|
|
132
277
|
return this.hostRegistryPath(ctx, 'Organization', '_batch-response');
|
|
133
278
|
}
|
|
134
279
|
|
|
135
280
|
/** Submit path: activate a tenant Organization in the GW registry using a VC from ICA. */
|
|
136
|
-
public hostRegistryOrganizationActivatePath(ctx
|
|
281
|
+
public hostRegistryOrganizationActivatePath(ctx?: HostRouteContext): string {
|
|
137
282
|
return this.hostRegistryPath(ctx, 'Organization', '_activate');
|
|
138
283
|
}
|
|
139
284
|
|
|
140
285
|
/** Poll path: `_activate` response. Pair with `hostRegistryOrganizationActivatePath`. */
|
|
141
|
-
public hostRegistryOrganizationActivatePollPath(ctx
|
|
286
|
+
public hostRegistryOrganizationActivatePollPath(ctx?: HostRouteContext): string {
|
|
142
287
|
return this.hostRegistryPath(ctx, 'Organization', '_activate-response');
|
|
143
288
|
}
|
|
144
289
|
|
|
145
290
|
/** Submit path: host registry Order batch (controller-level order submission). */
|
|
146
|
-
public hostRegistryOrderBatchPath(ctx
|
|
291
|
+
public hostRegistryOrderBatchPath(ctx?: HostRouteContext): string {
|
|
147
292
|
return this.hostRegistryPath(ctx, 'Order', '_batch');
|
|
148
293
|
}
|
|
149
294
|
|
|
150
295
|
/** Poll path: host registry Order batch. Pair with `hostRegistryOrderBatchPath`. */
|
|
151
|
-
public hostRegistryOrderPollPath(ctx
|
|
296
|
+
public hostRegistryOrderPollPath(ctx?: HostRouteContext): string {
|
|
152
297
|
return this.hostRegistryPath(ctx, 'Order', '_batch-response');
|
|
153
298
|
}
|
|
154
299
|
|
|
@@ -156,33 +301,37 @@ export class DataspaceNodeClient {
|
|
|
156
301
|
* Submit path: individual/family Organization onboarding (`org.schema/Organization/_batch`).
|
|
157
302
|
* Use for `family-registration/_create-or-resume` DIDComm payloads.
|
|
158
303
|
*/
|
|
159
|
-
public individualFamilyOrganizationBatchPath(ctx
|
|
160
|
-
|
|
304
|
+
public individualFamilyOrganizationBatchPath(ctx?: RouteContext): string {
|
|
305
|
+
const routeCtx = this.requireRouteContext(ctx);
|
|
306
|
+
return `/${encode(routeCtx.tenantId)}/cds-${encode(routeCtx.jurisdiction)}/v1/${encode(routeCtx.sector)}/individual/org.schema/Organization/_batch`;
|
|
161
307
|
}
|
|
162
308
|
|
|
163
309
|
/** Poll path: individual/family Organization. Pair with `individualFamilyOrganizationBatchPath`. */
|
|
164
|
-
public individualFamilyOrganizationPollPath(ctx
|
|
165
|
-
|
|
310
|
+
public individualFamilyOrganizationPollPath(ctx?: RouteContext): string {
|
|
311
|
+
const routeCtx = this.requireRouteContext(ctx);
|
|
312
|
+
return `/${encode(routeCtx.tenantId)}/cds-${encode(routeCtx.jurisdiction)}/v1/${encode(routeCtx.sector)}/individual/org.schema/Organization/_batch-response`;
|
|
166
313
|
}
|
|
167
314
|
|
|
168
315
|
/** Submit path: individual/family Organization search (`org.schema/Organization/_search`). */
|
|
169
|
-
public individualFamilyOrganizationSearchPath(ctx
|
|
316
|
+
public individualFamilyOrganizationSearchPath(ctx?: RouteContext): string {
|
|
170
317
|
return this.v1Path(ctx, 'individual', 'org.schema', 'Organization', '_search');
|
|
171
318
|
}
|
|
172
319
|
|
|
173
320
|
/** Poll path: individual/family Organization search. Pair with `individualFamilyOrganizationSearchPath`. */
|
|
174
|
-
public individualFamilyOrganizationSearchPollPath(ctx
|
|
321
|
+
public individualFamilyOrganizationSearchPollPath(ctx?: RouteContext): string {
|
|
175
322
|
return this.v1Path(ctx, 'individual', 'org.schema', 'Organization', '_search-response');
|
|
176
323
|
}
|
|
177
324
|
|
|
178
325
|
/** Submit path: individual/family Order batch (`org.schema/Order/_batch`). */
|
|
179
|
-
public individualFamilyOrderBatchPath(ctx
|
|
180
|
-
|
|
326
|
+
public individualFamilyOrderBatchPath(ctx?: RouteContext): string {
|
|
327
|
+
const routeCtx = this.requireRouteContext(ctx);
|
|
328
|
+
return `/${encode(routeCtx.tenantId)}/cds-${encode(routeCtx.jurisdiction)}/v1/${encode(routeCtx.sector)}/individual/org.schema/Order/_batch`;
|
|
181
329
|
}
|
|
182
330
|
|
|
183
331
|
/** Poll path: individual/family Order. Pair with `individualFamilyOrderBatchPath`. */
|
|
184
|
-
public individualFamilyOrderPollPath(ctx
|
|
185
|
-
|
|
332
|
+
public individualFamilyOrderPollPath(ctx?: RouteContext): string {
|
|
333
|
+
const routeCtx = this.requireRouteContext(ctx);
|
|
334
|
+
return `/${encode(routeCtx.tenantId)}/cds-${encode(routeCtx.jurisdiction)}/v1/${encode(routeCtx.sector)}/individual/org.schema/Order/_batch-response`;
|
|
186
335
|
}
|
|
187
336
|
|
|
188
337
|
/** Submit path: individual RelatedPerson (FHIR R4 API, `org.hl7.fhir.api/RelatedPerson/_batch`). */
|
|
@@ -226,12 +375,12 @@ export class DataspaceNodeClient {
|
|
|
226
375
|
}
|
|
227
376
|
|
|
228
377
|
/** Submit path: entity Employee (`entity/org.schema/Employee/_batch`). */
|
|
229
|
-
public employeeBatchPath(ctx
|
|
378
|
+
public employeeBatchPath(ctx?: RouteContext): string {
|
|
230
379
|
return this.v1Path(ctx, 'entity', 'org.schema', 'Employee', '_batch');
|
|
231
380
|
}
|
|
232
381
|
|
|
233
382
|
/** Poll path: entity Employee. Pair with `employeeBatchPath`. */
|
|
234
|
-
public employeePollPath(ctx
|
|
383
|
+
public employeePollPath(ctx?: RouteContext): string {
|
|
235
384
|
return this.v1Path(ctx, 'entity', 'org.schema', 'Employee', '_batch-response');
|
|
236
385
|
}
|
|
237
386
|
|
|
@@ -284,12 +433,12 @@ export class DataspaceNodeClient {
|
|
|
284
433
|
* Submit path: identity DCR step — binds API key to service public JWK.
|
|
285
434
|
* Used internally by `authenticateBackendPkceAndExchange` (step 1 of identity-exchange.v1).
|
|
286
435
|
*/
|
|
287
|
-
public identityDeviceDcrPath(ctx
|
|
436
|
+
public identityDeviceDcrPath(ctx?: RouteContext): string {
|
|
288
437
|
return this.tenantIdentityPath(ctx, 'host', '_dcr');
|
|
289
438
|
}
|
|
290
439
|
|
|
291
440
|
/** Poll path: identity DCR. Pair with `identityDeviceDcrPath`. */
|
|
292
|
-
public identityDeviceDcrPollPath(ctx
|
|
441
|
+
public identityDeviceDcrPollPath(ctx?: RouteContext): string {
|
|
293
442
|
return this.tenantIdentityPath(ctx, 'host', '_dcr-response');
|
|
294
443
|
}
|
|
295
444
|
|
|
@@ -297,17 +446,17 @@ export class DataspaceNodeClient {
|
|
|
297
446
|
* Submit path: identity token exchange — id_token → SMART bearer.
|
|
298
447
|
* Used internally by `authenticateBackendPkceAndExchange` (step 4 of identity-exchange.v1).
|
|
299
448
|
*/
|
|
300
|
-
public identityTokenExchangePath(ctx
|
|
449
|
+
public identityTokenExchangePath(ctx?: RouteContext): string {
|
|
301
450
|
return this.tenantIdentityPath(ctx, 'host', '_exchange');
|
|
302
451
|
}
|
|
303
452
|
|
|
304
453
|
/** Poll path: identity token exchange. Pair with `identityTokenExchangePath`. */
|
|
305
|
-
public identityTokenExchangePollPath(ctx
|
|
454
|
+
public identityTokenExchangePollPath(ctx?: RouteContext): string {
|
|
306
455
|
return this.tenantIdentityPath(ctx, 'host', '_exchange-response');
|
|
307
456
|
}
|
|
308
457
|
|
|
309
458
|
/** Submit path: identity license issue (`identity/auth/_issue`). */
|
|
310
|
-
public identityLicenseIssuePath(ctx
|
|
459
|
+
public identityLicenseIssuePath(ctx?: RouteContext): string {
|
|
311
460
|
return this.tenantIdentityPath(ctx, 'host', '_issue');
|
|
312
461
|
}
|
|
313
462
|
|
|
@@ -387,14 +536,16 @@ export class DataspaceNodeClient {
|
|
|
387
536
|
ctx,
|
|
388
537
|
apiKey,
|
|
389
538
|
scopes,
|
|
390
|
-
|
|
539
|
+
tokenCacheKey = `pkce:${apiKey.slice(0, 8)}`,
|
|
540
|
+
endpointId,
|
|
391
541
|
codeVerifier = randomUUID(),
|
|
392
542
|
pollOptions,
|
|
393
543
|
} = options;
|
|
394
544
|
|
|
395
|
-
const
|
|
545
|
+
const cacheKey = String(tokenCacheKey || endpointId || '').trim() || `pkce:${apiKey.slice(0, 8)}`;
|
|
546
|
+
const cached = this._tokenCache.get(cacheKey);
|
|
396
547
|
if (cached && cached.expiresAt > Date.now() + 30_000) {
|
|
397
|
-
return { status: 'cached', endpointId, accessToken: cached.accessToken, tokenType: cached.tokenType, scopes: cached.scopes };
|
|
548
|
+
return { status: 'cached', tokenCacheKey: cacheKey, endpointId: cacheKey, accessToken: cached.accessToken, tokenType: cached.tokenType, scopes: cached.scopes };
|
|
398
549
|
}
|
|
399
550
|
|
|
400
551
|
const controllerPublicJwk = await this.resolveControllerPublicJwk(options);
|
|
@@ -413,7 +564,7 @@ export class DataspaceNodeClient {
|
|
|
413
564
|
pollOptions,
|
|
414
565
|
);
|
|
415
566
|
if (dcrPoll.status !== 200) {
|
|
416
|
-
return { status: 'failed', step: '_dcr', endpointId, accessToken: '', tokenType: 'Bearer', scopes };
|
|
567
|
+
return { status: 'failed', step: '_dcr', tokenCacheKey: cacheKey, endpointId: cacheKey, accessToken: '', tokenType: 'Bearer', scopes };
|
|
417
568
|
}
|
|
418
569
|
|
|
419
570
|
// Step 2: Code – PKCE S256 challenge
|
|
@@ -434,7 +585,7 @@ export class DataspaceNodeClient {
|
|
|
434
585
|
const codeBody = (codePoll.body as Record<string, unknown>) ?? {};
|
|
435
586
|
const code = String(codeBody['code'] ?? '').trim();
|
|
436
587
|
if (codePoll.status !== 200 || !code) {
|
|
437
|
-
return { status: 'failed', step: '_code', endpointId, accessToken: '', tokenType: 'Bearer', scopes };
|
|
588
|
+
return { status: 'failed', step: '_code', tokenCacheKey: cacheKey, endpointId: cacheKey, accessToken: '', tokenType: 'Bearer', scopes };
|
|
438
589
|
}
|
|
439
590
|
|
|
440
591
|
// Step 3: Token – exchange code + verifier for id_token
|
|
@@ -454,7 +605,7 @@ export class DataspaceNodeClient {
|
|
|
454
605
|
const tokenBody = (tokenPoll.body as Record<string, unknown>) ?? {};
|
|
455
606
|
const idToken = String(tokenBody['id_token'] ?? '').trim();
|
|
456
607
|
if (tokenPoll.status !== 200 || !idToken) {
|
|
457
|
-
return { status: 'failed', step: '_token', endpointId, accessToken: '', tokenType: 'Bearer', scopes };
|
|
608
|
+
return { status: 'failed', step: '_token', tokenCacheKey: cacheKey, endpointId: cacheKey, accessToken: '', tokenType: 'Bearer', scopes };
|
|
458
609
|
}
|
|
459
610
|
|
|
460
611
|
// Step 4: Exchange – id_token → SMART bearer
|
|
@@ -477,7 +628,7 @@ export class DataspaceNodeClient {
|
|
|
477
628
|
const exchangeBody = (exchangePoll.body as Record<string, unknown>) ?? {};
|
|
478
629
|
const accessToken = String(exchangeBody['access_token'] ?? '').trim();
|
|
479
630
|
if (exchangePoll.status !== 200 || !accessToken) {
|
|
480
|
-
return { status: 'failed', step: '_exchange', endpointId, accessToken: '', tokenType: 'Bearer', scopes };
|
|
631
|
+
return { status: 'failed', step: '_exchange', tokenCacheKey: cacheKey, endpointId: cacheKey, accessToken: '', tokenType: 'Bearer', scopes };
|
|
481
632
|
}
|
|
482
633
|
|
|
483
634
|
const tokenType = String(exchangeBody['token_type'] ?? 'Bearer');
|
|
@@ -485,22 +636,22 @@ export class DataspaceNodeClient {
|
|
|
485
636
|
const grantedScopes = grantedScope ? grantedScope.split(' ').filter(Boolean) : scopes;
|
|
486
637
|
const expiresIn = Number(exchangeBody['expires_in'] ?? 0);
|
|
487
638
|
|
|
488
|
-
this._tokenCache.set(
|
|
639
|
+
this._tokenCache.set(cacheKey, {
|
|
489
640
|
accessToken,
|
|
490
641
|
tokenType,
|
|
491
642
|
scopes: grantedScopes,
|
|
492
643
|
expiresAt: Date.now() + expiresIn * 1000,
|
|
493
644
|
});
|
|
494
645
|
|
|
495
|
-
return { status: 'fetched', endpointId, accessToken, tokenType, scopes: grantedScopes };
|
|
646
|
+
return { status: 'fetched', tokenCacheKey: cacheKey, endpointId: cacheKey, accessToken, tokenType, scopes: grantedScopes };
|
|
496
647
|
}
|
|
497
648
|
|
|
498
649
|
/**
|
|
499
650
|
* Returns the cached SMART bearer for the given endpointId if still valid (>30s remaining).
|
|
500
651
|
* Returns `undefined` if not cached or expired.
|
|
501
652
|
*/
|
|
502
|
-
public getCachedBearerToken(
|
|
503
|
-
const cached = this._tokenCache.get(
|
|
653
|
+
public getCachedBearerToken(tokenCacheKey: string): string | undefined {
|
|
654
|
+
const cached = this._tokenCache.get(tokenCacheKey);
|
|
504
655
|
if (cached && cached.expiresAt > Date.now() + 30_000) {
|
|
505
656
|
return cached.accessToken;
|
|
506
657
|
}
|
|
@@ -516,7 +667,8 @@ export class DataspaceNodeClient {
|
|
|
516
667
|
const {
|
|
517
668
|
clientId,
|
|
518
669
|
scopes,
|
|
519
|
-
|
|
670
|
+
tokenCacheKey = `smart-backend:${clientId}`,
|
|
671
|
+
endpointId,
|
|
520
672
|
tokenUrl,
|
|
521
673
|
tokenPath = '/token',
|
|
522
674
|
audience,
|
|
@@ -524,12 +676,14 @@ export class DataspaceNodeClient {
|
|
|
524
676
|
additionalTokenFields,
|
|
525
677
|
} = options;
|
|
526
678
|
|
|
527
|
-
const
|
|
679
|
+
const cacheKey = String(tokenCacheKey || endpointId || '').trim() || `smart-backend:${clientId}`;
|
|
680
|
+
const cached = this._tokenCache.get(cacheKey);
|
|
528
681
|
if (cached && cached.expiresAt > Date.now() + 30_000) {
|
|
529
682
|
return {
|
|
530
683
|
status: 'cached',
|
|
531
684
|
profile: 'smart-backend.v1',
|
|
532
|
-
|
|
685
|
+
tokenCacheKey: cacheKey,
|
|
686
|
+
endpointId: cacheKey,
|
|
533
687
|
accessToken: cached.accessToken,
|
|
534
688
|
tokenType: cached.tokenType,
|
|
535
689
|
scopes: cached.scopes,
|
|
@@ -563,7 +717,8 @@ export class DataspaceNodeClient {
|
|
|
563
717
|
return {
|
|
564
718
|
status: 'failed',
|
|
565
719
|
profile: 'smart-backend.v1',
|
|
566
|
-
|
|
720
|
+
tokenCacheKey: cacheKey,
|
|
721
|
+
endpointId: cacheKey,
|
|
567
722
|
statusCode: response.status,
|
|
568
723
|
response,
|
|
569
724
|
};
|
|
@@ -575,7 +730,7 @@ export class DataspaceNodeClient {
|
|
|
575
730
|
const expiresIn = Number(body.expires_in ?? 0);
|
|
576
731
|
const expiresAt = Date.now() + expiresIn * 1000;
|
|
577
732
|
|
|
578
|
-
this._tokenCache.set(
|
|
733
|
+
this._tokenCache.set(cacheKey, {
|
|
579
734
|
accessToken,
|
|
580
735
|
tokenType,
|
|
581
736
|
scopes: grantedScopes,
|
|
@@ -585,7 +740,8 @@ export class DataspaceNodeClient {
|
|
|
585
740
|
return {
|
|
586
741
|
status: 'fetched',
|
|
587
742
|
profile: 'smart-backend.v1',
|
|
588
|
-
|
|
743
|
+
tokenCacheKey: cacheKey,
|
|
744
|
+
endpointId: cacheKey,
|
|
589
745
|
statusCode: response.status,
|
|
590
746
|
accessToken,
|
|
591
747
|
tokenType,
|
|
@@ -599,13 +755,13 @@ export class DataspaceNodeClient {
|
|
|
599
755
|
* Exchange token payload against gateway token endpoint and cache the result.
|
|
600
756
|
*/
|
|
601
757
|
public async requestSmartToken(input: SmartTokenExchangeInput): Promise<SmartTokenExchangeResult> {
|
|
602
|
-
const
|
|
603
|
-
if (!
|
|
604
|
-
throw new Error('requestSmartToken requires
|
|
758
|
+
const tokenCacheKey = String(input.tokenCacheKey || input.endpointId || '').trim();
|
|
759
|
+
if (!tokenCacheKey) {
|
|
760
|
+
throw new Error('requestSmartToken requires tokenCacheKey.');
|
|
605
761
|
}
|
|
606
762
|
|
|
607
763
|
const normalizedScopes = Array.from(new Set((input.scopes || []).filter(Boolean))).sort();
|
|
608
|
-
const cached = this._tokenCache.get(
|
|
764
|
+
const cached = this._tokenCache.get(tokenCacheKey);
|
|
609
765
|
if (cached && cached.expiresAt > Date.now() + 30_000) {
|
|
610
766
|
return {
|
|
611
767
|
status: 'cached',
|
|
@@ -633,7 +789,7 @@ export class DataspaceNodeClient {
|
|
|
633
789
|
: String(body.scope ?? '').trim().split(' ').filter(Boolean);
|
|
634
790
|
const resolvedScopes = grantedScopes.length ? grantedScopes : normalizedScopes;
|
|
635
791
|
const expiresIn = Number(body.expires_in ?? 0);
|
|
636
|
-
this._tokenCache.set(
|
|
792
|
+
this._tokenCache.set(tokenCacheKey, {
|
|
637
793
|
accessToken,
|
|
638
794
|
tokenType,
|
|
639
795
|
scopes: resolvedScopes,
|
|
@@ -650,6 +806,73 @@ export class DataspaceNodeClient {
|
|
|
650
806
|
};
|
|
651
807
|
}
|
|
652
808
|
|
|
809
|
+
/**
|
|
810
|
+
* Friendly wrapper for SMART token request via GW identity/auth token-exchange route.
|
|
811
|
+
* Uses one object, seconds-based polling, and constructor ctx fallback.
|
|
812
|
+
*/
|
|
813
|
+
public async requestSmartTokenSimple(
|
|
814
|
+
input: SmartTokenRequestSimpleInput,
|
|
815
|
+
): Promise<SmartTokenExchangeResult> {
|
|
816
|
+
const routeCtx = this.requireRouteContext(
|
|
817
|
+
input.tenantId && input.jurisdiction && input.sector
|
|
818
|
+
? { tenantId: input.tenantId, jurisdiction: input.jurisdiction, sector: input.sector }
|
|
819
|
+
: undefined,
|
|
820
|
+
);
|
|
821
|
+
const normalizedScopes = Array.from(new Set((input.scopes || []).filter(Boolean))).sort();
|
|
822
|
+
const tokenCacheKey = String(input.tokenCacheKey || input.endpointId || `smart:${routeCtx.tenantId}:${normalizedScopes.join(',')}`).trim();
|
|
823
|
+
if (!tokenCacheKey) {
|
|
824
|
+
throw new Error('requestSmartTokenSimple requires tokenCacheKey (or non-empty scopes).');
|
|
825
|
+
}
|
|
826
|
+
const pollOptions = this.resolveSimplePollOptions(input.timeoutSeconds, input.intervalSeconds);
|
|
827
|
+
|
|
828
|
+
const payload: Record<string, unknown> = {
|
|
829
|
+
thid: `exchange-${randomUUID()}`,
|
|
830
|
+
grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange',
|
|
831
|
+
subject_token_type: 'urn:ietf:params:oauth:token-type:id_token',
|
|
832
|
+
subject_token: input.idToken,
|
|
833
|
+
scope: normalizedScopes.join(' '),
|
|
834
|
+
organization: routeCtx.tenantId,
|
|
835
|
+
...(input.additionalClaims || {}),
|
|
836
|
+
};
|
|
837
|
+
|
|
838
|
+
const exchange = await this.submitAndPoll(
|
|
839
|
+
this.identityTokenExchangePath(routeCtx),
|
|
840
|
+
this.identityTokenExchangePollPath(routeCtx),
|
|
841
|
+
payload,
|
|
842
|
+
pollOptions,
|
|
843
|
+
);
|
|
844
|
+
|
|
845
|
+
const exchangeBody = (exchange.poll.body as Record<string, unknown>) ?? {};
|
|
846
|
+
const accessToken = String(exchangeBody.access_token || '').trim();
|
|
847
|
+
if (exchange.poll.status >= 400 || !accessToken) {
|
|
848
|
+
return {
|
|
849
|
+
status: 'failed',
|
|
850
|
+
statusCode: exchange.poll.status,
|
|
851
|
+
response: exchange.poll.body,
|
|
852
|
+
};
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
const tokenType = String(exchangeBody.token_type || 'Bearer');
|
|
856
|
+
const grantedScopes = String(exchangeBody.scope || '').trim().split(' ').filter(Boolean);
|
|
857
|
+
const resolvedScopes = grantedScopes.length ? grantedScopes : normalizedScopes;
|
|
858
|
+
const expiresIn = Number(exchangeBody.expires_in ?? 0);
|
|
859
|
+
this._tokenCache.set(tokenCacheKey, {
|
|
860
|
+
accessToken,
|
|
861
|
+
tokenType,
|
|
862
|
+
scopes: resolvedScopes,
|
|
863
|
+
expiresAt: Date.now() + expiresIn * 1000,
|
|
864
|
+
});
|
|
865
|
+
|
|
866
|
+
return {
|
|
867
|
+
status: 'fetched',
|
|
868
|
+
accessToken,
|
|
869
|
+
tokenType,
|
|
870
|
+
scopes: resolvedScopes,
|
|
871
|
+
statusCode: exchange.poll.status,
|
|
872
|
+
response: exchange.poll.body,
|
|
873
|
+
};
|
|
874
|
+
}
|
|
875
|
+
|
|
653
876
|
// ---- Private auth helpers ----------------------------------------------
|
|
654
877
|
|
|
655
878
|
private _pkceS256Challenge(verifier: string): string {
|
|
@@ -801,6 +1024,34 @@ export class DataspaceNodeClient {
|
|
|
801
1024
|
// ---- Generic batch API --------------------------------------------------
|
|
802
1025
|
|
|
803
1026
|
/**
|
|
1027
|
+
* POST a DIDComm bundle payload.
|
|
1028
|
+
* This is the preferred high-level method for DIDComm submission of
|
|
1029
|
+
* FHIR/API bundles (batch, transaction, message, etc.).
|
|
1030
|
+
*
|
|
1031
|
+
* Returns immediately with the HTTP response — pair with `pollUntilComplete` or use `submitAndPoll`.
|
|
1032
|
+
*/
|
|
1033
|
+
public async submitBundle(
|
|
1034
|
+
path: string,
|
|
1035
|
+
payload: { thid?: string } & Record<string, unknown>,
|
|
1036
|
+
options?: {
|
|
1037
|
+
mode?: 'plain' | 'strict';
|
|
1038
|
+
recipientEncryptionJwk?: PublicJwk;
|
|
1039
|
+
walletContext?: WalletContext;
|
|
1040
|
+
},
|
|
1041
|
+
): Promise<SubmitResponse> {
|
|
1042
|
+
const mode = options?.mode ?? 'plain';
|
|
1043
|
+
if (mode === 'strict') {
|
|
1044
|
+
if (!options?.recipientEncryptionJwk || !options?.walletContext) {
|
|
1045
|
+
throw new Error('submitBundle strict mode requires recipientEncryptionJwk and walletContext.');
|
|
1046
|
+
}
|
|
1047
|
+
return this.submitBatchEncrypted(path, payload, options.recipientEncryptionJwk, options.walletContext);
|
|
1048
|
+
}
|
|
1049
|
+
return this.submitBatch(path, payload);
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
/**
|
|
1053
|
+
* @deprecated Use `submitBundle` instead.
|
|
1054
|
+
*
|
|
804
1055
|
* POST a DIDComm plaintext payload to a batch submit path.
|
|
805
1056
|
* Use this for all `_batch` routes (family registration, observations, tasks, etc.).
|
|
806
1057
|
* Content-Type: `application/didcomm-plaintext+json`.
|
|
@@ -808,14 +1059,18 @@ export class DataspaceNodeClient {
|
|
|
808
1059
|
* Returns immediately with the HTTP response — pair with `pollUntilComplete` or use `submitAndPoll`.
|
|
809
1060
|
*/
|
|
810
1061
|
public async submitBatch(path: string, payload: unknown): Promise<SubmitResponse> {
|
|
811
|
-
const
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
1062
|
+
const url = /^https?:\/\//.test(path)
|
|
1063
|
+
? path
|
|
1064
|
+
: `${this.baseUrl}${path.startsWith('/') ? path : `/${path}`}`;
|
|
1065
|
+
const result = await submitDidcomm({
|
|
1066
|
+
mode: 'plain',
|
|
1067
|
+
url,
|
|
1068
|
+
payload: payload as Record<string, unknown>,
|
|
1069
|
+
defaultHeaders: this.defaultHeaders,
|
|
1070
|
+
bearerToken: this.bearerToken,
|
|
1071
|
+
fetcher: (requestUrl: string, init: DidcommFetchInit) => fetch(requestUrl, init),
|
|
1072
|
+
});
|
|
1073
|
+
return { status: result.status, location: result.location, body: result.body };
|
|
819
1074
|
}
|
|
820
1075
|
|
|
821
1076
|
/**
|
|
@@ -840,42 +1095,34 @@ export class DataspaceNodeClient {
|
|
|
840
1095
|
const publicJwks = await this.wallet.getPublicJwks(walletContext);
|
|
841
1096
|
const signingJwk = publicJwks.find((jwk) => jwk.use === 'sig' || jwk.alg === 'ES384') ?? publicJwks[0];
|
|
842
1097
|
|
|
843
|
-
// Step 1: sign payload claims as compact JWS
|
|
844
|
-
const compactJws = await this.wallet.signCompactJws(walletContext, {
|
|
845
|
-
header: {
|
|
846
|
-
typ: 'application/didcomm-signed+json',
|
|
847
|
-
alg: 'ES384',
|
|
848
|
-
...(signingJwk?.kid ? { kid: signingJwk.kid } : {}),
|
|
849
|
-
},
|
|
850
|
-
claims: payload as Record<string, unknown>,
|
|
851
|
-
});
|
|
852
|
-
|
|
853
|
-
// Step 2: encrypt JWS string as compact JWE (RSA-OAEP-256 + A256GCM, cty: JWS)
|
|
854
|
-
const compactJwe = await this.wallet.buildCompactJwe(walletContext, {
|
|
855
|
-
plaintext: compactJws,
|
|
856
|
-
recipientJwk: recipientEncryptionJwk,
|
|
857
|
-
contentType: 'JWS',
|
|
858
|
-
});
|
|
859
|
-
|
|
860
|
-
// Step 3: POST the JWE token directly (not JSON-serialised)
|
|
861
1098
|
const url = /^https?:\/\//.test(path)
|
|
862
1099
|
? path
|
|
863
1100
|
: `${this.baseUrl}${path.startsWith('/') ? path : `/${path}`}`;
|
|
864
|
-
const
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
1101
|
+
const result = await submitDidcomm({
|
|
1102
|
+
mode: 'strict',
|
|
1103
|
+
url,
|
|
1104
|
+
payload,
|
|
1105
|
+
defaultHeaders: this.defaultHeaders,
|
|
1106
|
+
bearerToken: this.bearerToken,
|
|
1107
|
+
recipientEncryptionJwk,
|
|
1108
|
+
signCompactJws: async (claims: Record<string, unknown>) =>
|
|
1109
|
+
this.wallet!.signCompactJws(walletContext, {
|
|
1110
|
+
header: {
|
|
1111
|
+
typ: 'application/didcomm-signed+json',
|
|
1112
|
+
alg: 'ES384',
|
|
1113
|
+
...(signingJwk?.kid ? { kid: signingJwk.kid } : {}),
|
|
1114
|
+
},
|
|
1115
|
+
claims,
|
|
1116
|
+
}),
|
|
1117
|
+
encryptCompactJwe: async (compactJws: string, recipientJwk: unknown) =>
|
|
1118
|
+
this.wallet!.buildCompactJwe(walletContext, {
|
|
1119
|
+
plaintext: compactJws,
|
|
1120
|
+
recipientJwk: recipientJwk as PublicJwk,
|
|
1121
|
+
contentType: 'JWS',
|
|
1122
|
+
}),
|
|
1123
|
+
fetcher: (requestUrl: string, init: DidcommFetchInit) => fetch(requestUrl, init),
|
|
1124
|
+
});
|
|
1125
|
+
return { status: result.status, location: result.location, body: result.body };
|
|
879
1126
|
}
|
|
880
1127
|
|
|
881
1128
|
/**
|
|
@@ -893,6 +1140,14 @@ export class DataspaceNodeClient {
|
|
|
893
1140
|
};
|
|
894
1141
|
}
|
|
895
1142
|
|
|
1143
|
+
/**
|
|
1144
|
+
* Legacy JSON submit for non-bundle payloads (openid/token/resource JSON bodies).
|
|
1145
|
+
* Keeps JSON flows explicit and semantically separated from DIDComm bundle flows.
|
|
1146
|
+
*/
|
|
1147
|
+
public async submitLegacyJson(path: string, payload: unknown): Promise<SubmitResponse> {
|
|
1148
|
+
return this.postJson(path, payload);
|
|
1149
|
+
}
|
|
1150
|
+
|
|
896
1151
|
/**
|
|
897
1152
|
* POST a multipart/form-data payload.
|
|
898
1153
|
* Use for file upload endpoints. Prefer `uploadConversionFile` for DataConversion uploads.
|
|
@@ -959,11 +1214,15 @@ export class DataspaceNodeClient {
|
|
|
959
1214
|
* Returns HTTP 202 while the job is still processing, 200 (or other) when done.
|
|
960
1215
|
* Prefer `pollUntilComplete` for automatic retry loops.
|
|
961
1216
|
*/
|
|
962
|
-
public async pollBatchResponse(
|
|
1217
|
+
public async pollBatchResponse(
|
|
1218
|
+
path: string,
|
|
1219
|
+
request: AsyncPollRequest,
|
|
1220
|
+
): Promise<{ status: number; body: unknown; retryAfterMs?: number }> {
|
|
963
1221
|
const response = await this.doPost(path, request, 'application/json');
|
|
964
1222
|
return {
|
|
965
1223
|
status: response.status,
|
|
966
1224
|
body: await this.parseResponseBody(response),
|
|
1225
|
+
retryAfterMs: parseRetryAfterMs(response.headers.get('retry-after')),
|
|
967
1226
|
};
|
|
968
1227
|
}
|
|
969
1228
|
|
|
@@ -1010,10 +1269,11 @@ export class DataspaceNodeClient {
|
|
|
1010
1269
|
* (appointment, medication schedule, or another event), mapped to `based-on-display`.
|
|
1011
1270
|
*/
|
|
1012
1271
|
public async createPhoneReminderTasks(
|
|
1013
|
-
ctx: RouteContext,
|
|
1272
|
+
ctx: RouteContext | undefined,
|
|
1014
1273
|
input: CreatePhoneReminderTasksInput,
|
|
1015
1274
|
options?: PollOptions,
|
|
1016
1275
|
): Promise<SubmitAndPollResult> {
|
|
1276
|
+
const routeCtx = this.requireRouteContext(ctx);
|
|
1017
1277
|
const windows = Array.isArray(input.windows) ? input.windows : [];
|
|
1018
1278
|
if (!windows.length) {
|
|
1019
1279
|
throw new Error('createPhoneReminderTasks requires at least one reminder window.');
|
|
@@ -1036,9 +1296,9 @@ export class DataspaceNodeClient {
|
|
|
1036
1296
|
}
|
|
1037
1297
|
|
|
1038
1298
|
const taskIdSeed = [
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1299
|
+
routeCtx.tenantId,
|
|
1300
|
+
routeCtx.jurisdiction,
|
|
1301
|
+
routeCtx.sector,
|
|
1042
1302
|
input.subjectRef,
|
|
1043
1303
|
input.ownerRef,
|
|
1044
1304
|
input.focusRef,
|
|
@@ -1081,15 +1341,15 @@ export class DataspaceNodeClient {
|
|
|
1081
1341
|
});
|
|
1082
1342
|
|
|
1083
1343
|
const payload = createDidcommPlainMessage({
|
|
1084
|
-
iss:
|
|
1085
|
-
aud:
|
|
1344
|
+
iss: routeCtx.tenantId,
|
|
1345
|
+
aud: routeCtx.tenantId,
|
|
1086
1346
|
thid,
|
|
1087
1347
|
body: { data },
|
|
1088
1348
|
});
|
|
1089
1349
|
|
|
1090
1350
|
return this.submitAndPoll(
|
|
1091
|
-
this.individualTaskBatchPath(
|
|
1092
|
-
this.individualTaskPollPath(
|
|
1351
|
+
this.individualTaskBatchPath(routeCtx),
|
|
1352
|
+
this.individualTaskPollPath(routeCtx),
|
|
1093
1353
|
payload,
|
|
1094
1354
|
options ?? { timeoutMs: 20_000, intervalMs: 1_000 },
|
|
1095
1355
|
);
|
|
@@ -1218,15 +1478,16 @@ export class DataspaceNodeClient {
|
|
|
1218
1478
|
* Returns `null` when no matching registration exists.
|
|
1219
1479
|
*/
|
|
1220
1480
|
public async searchFamilyOrganization(
|
|
1221
|
-
ctx: RouteContext,
|
|
1481
|
+
ctx: RouteContext | undefined,
|
|
1222
1482
|
filters: { controllerPhone: string; usualname: string; birthDate?: string },
|
|
1223
1483
|
options?: PollOptions,
|
|
1224
1484
|
): Promise<FamilyOrganizationSummary | null> {
|
|
1485
|
+
const routeCtx = this.requireRouteContext(ctx);
|
|
1225
1486
|
const thid = `search-${randomUUID()}`;
|
|
1226
1487
|
const claims: Record<string, unknown> = {
|
|
1227
1488
|
'org.schema.Organization.owner.telephone': filters.controllerPhone,
|
|
1228
1489
|
'org.schema.Organization.alternateName': filters.usualname,
|
|
1229
|
-
'org.schema.Service.category':
|
|
1490
|
+
'org.schema.Service.category': routeCtx.sector,
|
|
1230
1491
|
};
|
|
1231
1492
|
if (filters.birthDate) {
|
|
1232
1493
|
claims['org.schema.Organization.foundingDate'] = filters.birthDate;
|
|
@@ -1235,8 +1496,8 @@ export class DataspaceNodeClient {
|
|
|
1235
1496
|
const payload = {
|
|
1236
1497
|
jti: randomUUID(),
|
|
1237
1498
|
thid,
|
|
1238
|
-
iss:
|
|
1239
|
-
aud:
|
|
1499
|
+
iss: routeCtx.tenantId,
|
|
1500
|
+
aud: routeCtx.tenantId,
|
|
1240
1501
|
type: 'application/api+json',
|
|
1241
1502
|
body: {
|
|
1242
1503
|
data: [{
|
|
@@ -1248,8 +1509,8 @@ export class DataspaceNodeClient {
|
|
|
1248
1509
|
};
|
|
1249
1510
|
|
|
1250
1511
|
const result = await this.submitAndPoll(
|
|
1251
|
-
this.individualFamilyOrganizationSearchPath(
|
|
1252
|
-
this.individualFamilyOrganizationSearchPollPath(
|
|
1512
|
+
this.individualFamilyOrganizationSearchPath(routeCtx),
|
|
1513
|
+
this.individualFamilyOrganizationSearchPollPath(routeCtx),
|
|
1253
1514
|
payload,
|
|
1254
1515
|
options ?? { timeoutMs: 20_000, intervalMs: 1_000 },
|
|
1255
1516
|
);
|
|
@@ -1281,7 +1542,7 @@ export class DataspaceNodeClient {
|
|
|
1281
1542
|
* Activate tenant organization in GW from ICA-derived proof.
|
|
1282
1543
|
*/
|
|
1283
1544
|
public async activateOrganizationInGatewayFromIcaProof(
|
|
1284
|
-
ctx: HostRouteContext,
|
|
1545
|
+
ctx: HostRouteContext | undefined,
|
|
1285
1546
|
input: GatewayOrganizationActivationInput,
|
|
1286
1547
|
options?: PollOptions,
|
|
1287
1548
|
): Promise<SubmitAndPollResult> {
|
|
@@ -1294,6 +1555,11 @@ export class DataspaceNodeClient {
|
|
|
1294
1555
|
vp_token: input.vpToken,
|
|
1295
1556
|
...(input.additionalClaims || {}),
|
|
1296
1557
|
};
|
|
1558
|
+
const requestedMembers = Number.isFinite(Number(input.numberOfMembers))
|
|
1559
|
+
? Math.max(1, Math.floor(Number(input.numberOfMembers)))
|
|
1560
|
+
: 2;
|
|
1561
|
+
// Keep gateway-facing claim stable while exposing a generic SDK input.
|
|
1562
|
+
claims['org.schema.Organization.numberOfEmployees'] = requestedMembers;
|
|
1297
1563
|
if (input.organizationVc) claims['org.schema.OrganizationCredential.jwt'] = input.organizationVc;
|
|
1298
1564
|
if (input.legalRepresentativeVc) {
|
|
1299
1565
|
claims['org.schema.LegalRepresentativeCredential.jwt'] = input.legalRepresentativeVc;
|
|
@@ -1304,6 +1570,10 @@ export class DataspaceNodeClient {
|
|
|
1304
1570
|
iss: 'did:web:controller.example.com',
|
|
1305
1571
|
aud: 'did:web:host.example.com',
|
|
1306
1572
|
body: {
|
|
1573
|
+
// GW activation parser expects proof material at top-level DIDComm body.
|
|
1574
|
+
vp_token: input.vpToken,
|
|
1575
|
+
...(input.organizationVc ? { organizationCredential: input.organizationVc } : {}),
|
|
1576
|
+
...(input.legalRepresentativeVc ? { representativeCredential: input.legalRepresentativeVc } : {}),
|
|
1307
1577
|
data: [
|
|
1308
1578
|
{
|
|
1309
1579
|
type: 'Organization-activation-request-v1.0',
|
|
@@ -1322,6 +1592,227 @@ export class DataspaceNodeClient {
|
|
|
1322
1592
|
);
|
|
1323
1593
|
}
|
|
1324
1594
|
|
|
1595
|
+
/**
|
|
1596
|
+
* Friendly wrapper for legal organization activation.
|
|
1597
|
+
* Accepts one object and seconds-based polling options for integrator ergonomics.
|
|
1598
|
+
*/
|
|
1599
|
+
public async activateOrganizationInGatewaySimple(
|
|
1600
|
+
input: GatewayOrganizationActivationSimpleInput,
|
|
1601
|
+
): Promise<SubmitAndPollResult> {
|
|
1602
|
+
const serviceProviderDidWeb = String(input.serviceProviderDidWeb || '').trim();
|
|
1603
|
+
const serviceProviderUrl = String(input.serviceProviderUrl || '').trim();
|
|
1604
|
+
const controllerEmail = String(input.controllerEmail || '').trim();
|
|
1605
|
+
const controllerTelephone = String(input.controllerTelephone || '').trim();
|
|
1606
|
+
const controllerRole = String(input.controllerRole || '').trim();
|
|
1607
|
+
const resolvedServiceDid = toDidWebFromUrlOrHost(serviceProviderDidWeb || serviceProviderUrl);
|
|
1608
|
+
if (!resolvedServiceDid) {
|
|
1609
|
+
throw new Error('activateOrganizationInGatewaySimple requires serviceProviderDidWeb or serviceProviderUrl.');
|
|
1610
|
+
}
|
|
1611
|
+
if (!controllerEmail && !controllerTelephone) {
|
|
1612
|
+
throw new Error('activateOrganizationInGatewaySimple requires controllerEmail or controllerTelephone.');
|
|
1613
|
+
}
|
|
1614
|
+
if (!controllerRole) {
|
|
1615
|
+
throw new Error('activateOrganizationInGatewaySimple requires controllerRole.');
|
|
1616
|
+
}
|
|
1617
|
+
const pollOptions = this.resolveSimplePollOptions(input.timeoutSeconds, input.intervalSeconds);
|
|
1618
|
+
|
|
1619
|
+
const hostCtx = this.requireHostRouteContext(
|
|
1620
|
+
input.jurisdiction && input.sector
|
|
1621
|
+
? { jurisdiction: input.jurisdiction, sector: input.sector }
|
|
1622
|
+
: undefined,
|
|
1623
|
+
);
|
|
1624
|
+
const implicitClaims: Record<string, unknown> = {
|
|
1625
|
+
'org.schema.Service.category': hostCtx.sector,
|
|
1626
|
+
'org.schema.Service.identifier': resolvedServiceDid,
|
|
1627
|
+
...(serviceProviderUrl ? { 'org.schema.Service.url': serviceProviderUrl } : {}),
|
|
1628
|
+
[ClaimsPersonSchemaorg.hasOccupation]: controllerRole,
|
|
1629
|
+
...(controllerEmail ? { [ClaimsPersonSchemaorg.email]: controllerEmail } : {}),
|
|
1630
|
+
...(controllerTelephone ? { [ClaimsPersonSchemaorg.telephone]: controllerTelephone } : {}),
|
|
1631
|
+
};
|
|
1632
|
+
|
|
1633
|
+
const activation = await this.activateOrganizationInGatewayFromIcaProof(
|
|
1634
|
+
hostCtx,
|
|
1635
|
+
{
|
|
1636
|
+
vpToken: input.vpToken,
|
|
1637
|
+
numberOfMembers: input.numberOfMembers,
|
|
1638
|
+
organizationVc: input.organizationVc,
|
|
1639
|
+
legalRepresentativeVc: input.legalRepresentativeVc,
|
|
1640
|
+
regulatoryEvidence: input.regulatoryEvidence,
|
|
1641
|
+
additionalClaims: { ...implicitClaims, ...(input.additionalClaims || {}) },
|
|
1642
|
+
},
|
|
1643
|
+
pollOptions,
|
|
1644
|
+
);
|
|
1645
|
+
this.assertFirstDidcommEntrySuccess(activation, 'activateOrganizationInGatewaySimple');
|
|
1646
|
+
return activation;
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
/**
|
|
1650
|
+
* Friendly wrapper for legal organization Order confirmation.
|
|
1651
|
+
* Accepts one object and builds payload/paths internally.
|
|
1652
|
+
*/
|
|
1653
|
+
public async confirmLegalOrganizationOrderSimple(
|
|
1654
|
+
input: LegalOrganizationOrderSimpleInput,
|
|
1655
|
+
): Promise<SubmitAndPollResult> {
|
|
1656
|
+
if (!String(input.offerId || '').trim()) {
|
|
1657
|
+
throw new Error('confirmLegalOrganizationOrderSimple requires offerId.');
|
|
1658
|
+
}
|
|
1659
|
+
const pollOptions = this.resolveSimplePollOptions(input.timeoutSeconds, input.intervalSeconds);
|
|
1660
|
+
const hostCtx = this.requireHostRouteContext(
|
|
1661
|
+
input.jurisdiction && input.sector
|
|
1662
|
+
? { jurisdiction: input.jurisdiction, sector: input.sector }
|
|
1663
|
+
: undefined,
|
|
1664
|
+
);
|
|
1665
|
+
|
|
1666
|
+
const claims: Record<string, unknown> = {
|
|
1667
|
+
'@context': 'org.schema',
|
|
1668
|
+
'Order.acceptedOffer.identifier': input.offerId,
|
|
1669
|
+
...(input.additionalClaims || {}),
|
|
1670
|
+
};
|
|
1671
|
+
const payload = createDidcommPlainMessage({
|
|
1672
|
+
iss: 'did:web:controller.example.com',
|
|
1673
|
+
aud: 'did:web:host.example.com',
|
|
1674
|
+
thid: `order-${randomUUID()}`,
|
|
1675
|
+
body: {
|
|
1676
|
+
data: [{
|
|
1677
|
+
type: input.dataType || 'Organization-order-request-v1.0',
|
|
1678
|
+
meta: { claims }, // legacy compatibility
|
|
1679
|
+
resource: { meta: { claims } },
|
|
1680
|
+
}],
|
|
1681
|
+
},
|
|
1682
|
+
});
|
|
1683
|
+
|
|
1684
|
+
const order = await this.submitAndPoll(
|
|
1685
|
+
this.hostRegistryOrderBatchPath(hostCtx),
|
|
1686
|
+
this.hostRegistryOrderPollPath(hostCtx),
|
|
1687
|
+
payload,
|
|
1688
|
+
pollOptions,
|
|
1689
|
+
);
|
|
1690
|
+
this.assertFirstDidcommEntrySuccess(order, 'confirmLegalOrganizationOrderSimple');
|
|
1691
|
+
return order;
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
/**
|
|
1695
|
+
* Normalize GW async response into DIDComm message body.
|
|
1696
|
+
*
|
|
1697
|
+
* Transport note:
|
|
1698
|
+
* - GW poll responses are HTTP JSON envelopes
|
|
1699
|
+
* - business payload lives inside DIDComm `body`
|
|
1700
|
+
*
|
|
1701
|
+
* This helper abstracts envelope differences so consumers do not depend on
|
|
1702
|
+
* raw `poll.body.body` paths.
|
|
1703
|
+
*/
|
|
1704
|
+
public getDidcommMessageBodyFromResponse(
|
|
1705
|
+
result: SubmitAndPollResult | PollResult | unknown,
|
|
1706
|
+
): Record<string, unknown> | undefined {
|
|
1707
|
+
const pollBody = (result as any)?.poll?.body ?? (result as any)?.body ?? result;
|
|
1708
|
+
const didcommBody = (pollBody as any)?.body;
|
|
1709
|
+
if (didcommBody && typeof didcommBody === 'object') return didcommBody as Record<string, unknown>;
|
|
1710
|
+
if (pollBody && typeof pollBody === 'object' && Array.isArray((pollBody as any)?.data)) {
|
|
1711
|
+
return pollBody as Record<string, unknown>;
|
|
1712
|
+
}
|
|
1713
|
+
return undefined;
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
/**
|
|
1717
|
+
* Return first DIDComm business entry from a submit/poll result.
|
|
1718
|
+
*/
|
|
1719
|
+
public getFirstDidcommDataEntryFromResponse(
|
|
1720
|
+
result: SubmitAndPollResult | PollResult | unknown,
|
|
1721
|
+
): Record<string, unknown> | undefined {
|
|
1722
|
+
const body = this.getDidcommMessageBodyFromResponse(result);
|
|
1723
|
+
const entry = (body as any)?.data?.[0];
|
|
1724
|
+
return entry && typeof entry === 'object' ? (entry as Record<string, unknown>) : undefined;
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1727
|
+
/**
|
|
1728
|
+
* Extract `org.schema.Offer.identifier` from a submit/poll result.
|
|
1729
|
+
*
|
|
1730
|
+
* This helper normalizes canonical and legacy claim locations.
|
|
1731
|
+
*/
|
|
1732
|
+
public getOfferIdFromResponse(result: SubmitAndPollResult | PollResult | unknown): string | undefined {
|
|
1733
|
+
const entry = this.getFirstDidcommDataEntryFromResponse(result);
|
|
1734
|
+
const offerId = String(
|
|
1735
|
+
(entry as any)?.meta?.claims?.[ClaimsOfferSchemaorg.identifier]
|
|
1736
|
+
|| (entry as any)?.resource?.meta?.claims?.[ClaimsOfferSchemaorg.identifier]
|
|
1737
|
+
|| '',
|
|
1738
|
+
).trim();
|
|
1739
|
+
return offerId || undefined;
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
/**
|
|
1743
|
+
* Extract a UI-ready Offer preview from activation/registration responses.
|
|
1744
|
+
*/
|
|
1745
|
+
public getOfferPreviewFromResponse(result: SubmitAndPollResult | PollResult | unknown): OfferPreview {
|
|
1746
|
+
const entry = this.getFirstDidcommDataEntryFromResponse(result) as any;
|
|
1747
|
+
const claims = entry?.meta?.claims || entry?.resource?.meta?.claims || {};
|
|
1748
|
+
const seatsRaw = claims[ClaimsOfferSchemaorg.eligibleQuantityValue];
|
|
1749
|
+
const seats =
|
|
1750
|
+
typeof seatsRaw === 'number'
|
|
1751
|
+
? seatsRaw
|
|
1752
|
+
: (typeof seatsRaw === 'string' && seatsRaw.trim() ? Number(seatsRaw) : undefined);
|
|
1753
|
+
return {
|
|
1754
|
+
offerId: this.getOfferIdFromResponse(result),
|
|
1755
|
+
amount: claims[ClaimsOfferSchemaorg.price],
|
|
1756
|
+
currency: claims[ClaimsOfferSchemaorg.priceCurrency],
|
|
1757
|
+
seats: Number.isFinite(seats as number) ? seats : undefined,
|
|
1758
|
+
planName: claims[ClaimsOfferSchemaorg.itemOfferedName],
|
|
1759
|
+
sku: claims[ClaimsOfferSchemaorg.itemOfferedSku],
|
|
1760
|
+
paymentMethod: claims[ClaimsOfferSchemaorg.acceptedPaymentMethod],
|
|
1761
|
+
checkoutUrl: claims[ClaimsOfferSchemaorg.checkoutPageURLTemplate],
|
|
1762
|
+
};
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
/**
|
|
1766
|
+
* Alias of `getOfferPreviewFromResponse` with business naming.
|
|
1767
|
+
*/
|
|
1768
|
+
public getOfferInfoFromResponse(result: SubmitAndPollResult | PollResult | unknown): OfferInfo {
|
|
1769
|
+
return this.getOfferPreviewFromResponse(result);
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1772
|
+
/**
|
|
1773
|
+
* Extract activation code from response payload or claims.
|
|
1774
|
+
* Supports common response shapes used in onboarding and license issuance flows.
|
|
1775
|
+
*/
|
|
1776
|
+
public getActivationCodeFromResponse(result: SubmitAndPollResult | PollResult | unknown): string | undefined {
|
|
1777
|
+
const root = (result as any)?.poll?.body || (result as any)?.body || {};
|
|
1778
|
+
const byBody =
|
|
1779
|
+
String(root?.activationCode || root?.body?.activationCode || '').trim();
|
|
1780
|
+
if (byBody) return byBody;
|
|
1781
|
+
|
|
1782
|
+
const entry = this.getFirstDidcommDataEntryFromResponse(result) as any;
|
|
1783
|
+
const claims = entry?.meta?.claims || entry?.resource?.meta?.claims || {};
|
|
1784
|
+
const byClaims = String(
|
|
1785
|
+
claims['org.schema.IndividualProduct.serialNumber']
|
|
1786
|
+
|| claims['org.schema.Offer.serialNumber']
|
|
1787
|
+
|| claims['activationCode']
|
|
1788
|
+
|| '',
|
|
1789
|
+
).trim();
|
|
1790
|
+
return byClaims || undefined;
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1793
|
+
/**
|
|
1794
|
+
* Throws when first DIDComm entry contains a business-level error status.
|
|
1795
|
+
*/
|
|
1796
|
+
public assertFirstDidcommEntrySuccess(
|
|
1797
|
+
result: SubmitAndPollResult | PollResult | unknown,
|
|
1798
|
+
contextLabel: string,
|
|
1799
|
+
): void {
|
|
1800
|
+
const entry = this.getFirstDidcommDataEntryFromResponse(result) as any;
|
|
1801
|
+
const responseStatusRaw = entry?.response?.status;
|
|
1802
|
+
const responseStatus = Number(responseStatusRaw);
|
|
1803
|
+
if (!Number.isFinite(responseStatus) || responseStatus < 400) return;
|
|
1804
|
+
|
|
1805
|
+
const diagnostics =
|
|
1806
|
+
String(
|
|
1807
|
+
entry?.response?.outcome?.issue?.[0]?.diagnostics
|
|
1808
|
+
|| entry?.response?.outcome?.issue?.[0]?.details?.text
|
|
1809
|
+
|| '',
|
|
1810
|
+
).trim();
|
|
1811
|
+
throw new Error(
|
|
1812
|
+
`${contextLabel} failed (business status=${responseStatus})${diagnostics ? `: ${diagnostics}` : ''}`,
|
|
1813
|
+
);
|
|
1814
|
+
}
|
|
1815
|
+
|
|
1325
1816
|
/**
|
|
1326
1817
|
* Activate employee/member device by activation code exchange + DCR registration.
|
|
1327
1818
|
*
|
|
@@ -1329,7 +1820,7 @@ export class DataspaceNodeClient {
|
|
|
1329
1820
|
* Step 2. Register device keys through Device/_dcr authorized by that initial token.
|
|
1330
1821
|
*/
|
|
1331
1822
|
public async activateEmployeeDeviceWithActivationCode(
|
|
1332
|
-
ctx: RouteContext,
|
|
1823
|
+
ctx: RouteContext | undefined,
|
|
1333
1824
|
input: EmployeeDeviceActivationInput,
|
|
1334
1825
|
): Promise<EmployeeDeviceActivationResult> {
|
|
1335
1826
|
const exchangePayload = {
|
|
@@ -1385,17 +1876,43 @@ export class DataspaceNodeClient {
|
|
|
1385
1876
|
};
|
|
1386
1877
|
}
|
|
1387
1878
|
|
|
1879
|
+
/**
|
|
1880
|
+
* Friendly wrapper for employee/member device activation.
|
|
1881
|
+
* Uses one object, seconds-based polling, and constructor ctx fallback.
|
|
1882
|
+
*/
|
|
1883
|
+
public async activateEmployeeDeviceWithActivationCodeSimple(
|
|
1884
|
+
input: EmployeeDeviceActivationSimpleInput,
|
|
1885
|
+
): Promise<EmployeeDeviceActivationResult> {
|
|
1886
|
+
const routeCtx = this.requireRouteContext(
|
|
1887
|
+
input.tenantId && input.jurisdiction && input.sector
|
|
1888
|
+
? { tenantId: input.tenantId, jurisdiction: input.jurisdiction, sector: input.sector }
|
|
1889
|
+
: undefined,
|
|
1890
|
+
);
|
|
1891
|
+
const pollOptions = this.resolveSimplePollOptions(input.timeoutSeconds, input.intervalSeconds);
|
|
1892
|
+
|
|
1893
|
+
return this.activateEmployeeDeviceWithActivationCode(
|
|
1894
|
+
routeCtx,
|
|
1895
|
+
{
|
|
1896
|
+
activationCode: input.activationCode,
|
|
1897
|
+
idToken: input.idToken,
|
|
1898
|
+
dcrPayload: input.dcrPayload,
|
|
1899
|
+
pollOptions,
|
|
1900
|
+
},
|
|
1901
|
+
);
|
|
1902
|
+
}
|
|
1903
|
+
|
|
1388
1904
|
/**
|
|
1389
1905
|
* UC 5.3 wrapper: create organization employee in entity Employee batch route.
|
|
1390
1906
|
*/
|
|
1391
1907
|
public async createOrganizationEmployee(
|
|
1392
|
-
ctx: RouteContext,
|
|
1908
|
+
ctx: RouteContext | undefined,
|
|
1393
1909
|
input: OrganizationEmployeeCreationInput,
|
|
1394
1910
|
options?: PollOptions,
|
|
1395
1911
|
): Promise<SubmitAndPollResult> {
|
|
1912
|
+
const routeCtx = this.requireRouteContext(ctx);
|
|
1396
1913
|
const payload = createDidcommPlainMessage({
|
|
1397
|
-
iss:
|
|
1398
|
-
aud:
|
|
1914
|
+
iss: routeCtx.tenantId,
|
|
1915
|
+
aud: routeCtx.tenantId,
|
|
1399
1916
|
thid: `employee-${randomUUID()}`,
|
|
1400
1917
|
body: {
|
|
1401
1918
|
data: [
|
|
@@ -1409,8 +1926,8 @@ export class DataspaceNodeClient {
|
|
|
1409
1926
|
});
|
|
1410
1927
|
|
|
1411
1928
|
return this.submitAndPoll(
|
|
1412
|
-
this.employeeBatchPath(
|
|
1413
|
-
this.employeePollPath(
|
|
1929
|
+
this.employeeBatchPath(routeCtx),
|
|
1930
|
+
this.employeePollPath(routeCtx),
|
|
1414
1931
|
payload,
|
|
1415
1932
|
options,
|
|
1416
1933
|
);
|
|
@@ -1420,7 +1937,7 @@ export class DataspaceNodeClient {
|
|
|
1420
1937
|
* UC 5.1 wrapper: bootstrap subject organization context via registration + optional order confirmation.
|
|
1421
1938
|
*/
|
|
1422
1939
|
public async bootstrapSubjectOrganizationIndex(
|
|
1423
|
-
ctx: RouteContext,
|
|
1940
|
+
ctx: RouteContext | undefined,
|
|
1424
1941
|
input: SubjectOrganizationBootstrapInput,
|
|
1425
1942
|
): Promise<SubjectOrganizationBootstrapResult> {
|
|
1426
1943
|
const registrationPayload = {
|
|
@@ -1454,24 +1971,151 @@ export class DataspaceNodeClient {
|
|
|
1454
1971
|
return { registration, confirmation };
|
|
1455
1972
|
}
|
|
1456
1973
|
|
|
1974
|
+
/**
|
|
1975
|
+
* Friendly wrapper (recommended step 1): register individual organization and return Offer.
|
|
1976
|
+
*/
|
|
1977
|
+
public async startIndividualOrganizationSimple(
|
|
1978
|
+
input: IndividualOrganizationBootstrapSimpleInput,
|
|
1979
|
+
): Promise<IndividualOrganizationStartSimpleResult> {
|
|
1980
|
+
const routeCtx = this.requireRouteContext(
|
|
1981
|
+
input.tenantId && input.jurisdiction && input.sector
|
|
1982
|
+
? { tenantId: input.tenantId, jurisdiction: input.jurisdiction, sector: input.sector }
|
|
1983
|
+
: undefined,
|
|
1984
|
+
);
|
|
1985
|
+
const alternateName = String(input.alternateName || '').trim();
|
|
1986
|
+
if (!alternateName) {
|
|
1987
|
+
throw new Error('bootstrapIndividualOrganizationSimple requires alternateName.');
|
|
1988
|
+
}
|
|
1989
|
+
const controllerEmail = String(input.controllerEmail || '').trim();
|
|
1990
|
+
const controllerTelephone = String(input.controllerTelephone || '').trim();
|
|
1991
|
+
if (!controllerEmail && !controllerTelephone) {
|
|
1992
|
+
throw new Error('bootstrapIndividualOrganizationSimple requires controllerEmail or controllerTelephone.');
|
|
1993
|
+
}
|
|
1994
|
+
const controllerRole = String(input.controllerRole || 'org.hl7.v3.RoleCode|RESPRSN').trim();
|
|
1995
|
+
|
|
1996
|
+
const claims: Record<string, unknown> = {
|
|
1997
|
+
'@context': 'org.schema',
|
|
1998
|
+
'org.schema.Organization.alternateName': alternateName,
|
|
1999
|
+
'org.schema.Service.category': routeCtx.sector,
|
|
2000
|
+
[ClaimsPersonSchemaorg.hasOccupation]: controllerRole,
|
|
2001
|
+
...(controllerEmail ? { [ClaimsPersonSchemaorg.email]: controllerEmail } : {}),
|
|
2002
|
+
...(controllerTelephone ? { [ClaimsPersonSchemaorg.telephone]: controllerTelephone } : {}),
|
|
2003
|
+
...(input.additionalClaims || {}),
|
|
2004
|
+
};
|
|
2005
|
+
const pollOptions = this.resolveSimplePollOptions(input.timeoutSeconds, input.intervalSeconds);
|
|
2006
|
+
const registrationPayload = createDidcommPlainMessage({
|
|
2007
|
+
iss: routeCtx.tenantId,
|
|
2008
|
+
aud: routeCtx.tenantId,
|
|
2009
|
+
thid: `family-org-${randomUUID()}`,
|
|
2010
|
+
body: {
|
|
2011
|
+
data: [{
|
|
2012
|
+
type: 'SubjectOrg-registration-form-v1.0',
|
|
2013
|
+
meta: { claims },
|
|
2014
|
+
resource: { meta: { claims } },
|
|
2015
|
+
}],
|
|
2016
|
+
},
|
|
2017
|
+
});
|
|
2018
|
+
|
|
2019
|
+
const registration = await this.submitAndPoll(
|
|
2020
|
+
this.individualFamilyOrganizationBatchPath(routeCtx),
|
|
2021
|
+
this.individualFamilyOrganizationPollPath(routeCtx),
|
|
2022
|
+
registrationPayload,
|
|
2023
|
+
pollOptions,
|
|
2024
|
+
);
|
|
2025
|
+
this.assertFirstDidcommEntrySuccess(registration, 'startIndividualOrganizationSimple.registration');
|
|
2026
|
+
|
|
2027
|
+
const offerId = this.getOfferIdFromResponse(registration);
|
|
2028
|
+
if (!offerId) {
|
|
2029
|
+
throw new Error('startIndividualOrganizationSimple failed: missing offerId in registration response.');
|
|
2030
|
+
}
|
|
2031
|
+
return { registration, offerId, offerPreview: this.getOfferPreviewFromResponse(registration) };
|
|
2032
|
+
}
|
|
2033
|
+
|
|
2034
|
+
/**
|
|
2035
|
+
* Friendly wrapper (recommended step 2): confirm individual/family order from accepted offerId.
|
|
2036
|
+
*/
|
|
2037
|
+
public async confirmIndividualOrganizationOrderSimple(
|
|
2038
|
+
input: IndividualOrganizationConfirmOrderSimpleInput,
|
|
2039
|
+
): Promise<SubmitAndPollResult> {
|
|
2040
|
+
const routeCtx = this.requireRouteContext(
|
|
2041
|
+
input.tenantId && input.jurisdiction && input.sector
|
|
2042
|
+
? { tenantId: input.tenantId, jurisdiction: input.jurisdiction, sector: input.sector }
|
|
2043
|
+
: undefined,
|
|
2044
|
+
);
|
|
2045
|
+
const offerId = String(input.offerId || '').trim();
|
|
2046
|
+
if (!offerId) {
|
|
2047
|
+
throw new Error('confirmIndividualOrganizationOrderSimple requires offerId.');
|
|
2048
|
+
}
|
|
2049
|
+
const pollOptions = this.resolveSimplePollOptions(input.timeoutSeconds, input.intervalSeconds);
|
|
2050
|
+
const orderClaims: Record<string, unknown> = {
|
|
2051
|
+
'@context': 'org.schema',
|
|
2052
|
+
'Order.acceptedOffer.identifier': offerId,
|
|
2053
|
+
};
|
|
2054
|
+
const confirmationPayload = createDidcommPlainMessage({
|
|
2055
|
+
iss: routeCtx.tenantId,
|
|
2056
|
+
aud: routeCtx.tenantId,
|
|
2057
|
+
thid: `family-order-${randomUUID()}`,
|
|
2058
|
+
body: {
|
|
2059
|
+
data: [{
|
|
2060
|
+
type: 'Family-order-request-v1.0',
|
|
2061
|
+
meta: { claims: orderClaims },
|
|
2062
|
+
resource: { meta: { claims: orderClaims } },
|
|
2063
|
+
}],
|
|
2064
|
+
},
|
|
2065
|
+
});
|
|
2066
|
+
|
|
2067
|
+
const confirmation = await this.submitAndPoll(
|
|
2068
|
+
this.individualFamilyOrderBatchPath(routeCtx),
|
|
2069
|
+
this.individualFamilyOrderPollPath(routeCtx),
|
|
2070
|
+
confirmationPayload,
|
|
2071
|
+
pollOptions,
|
|
2072
|
+
);
|
|
2073
|
+
this.assertFirstDidcommEntrySuccess(confirmation, 'confirmIndividualOrganizationOrderSimple');
|
|
2074
|
+
return confirmation;
|
|
2075
|
+
}
|
|
2076
|
+
|
|
2077
|
+
/**
|
|
2078
|
+
* Friendly wrapper (provisional): register + auto-confirm individual order.
|
|
2079
|
+
* Prefer `startIndividualOrganizationSimple` + `confirmIndividualOrganizationOrderSimple`.
|
|
2080
|
+
*/
|
|
2081
|
+
public async bootstrapIndividualOrganizationSimple(
|
|
2082
|
+
input: IndividualOrganizationBootstrapSimpleInput,
|
|
2083
|
+
): Promise<IndividualOrganizationBootstrapSimpleResult> {
|
|
2084
|
+
const started = await this.startIndividualOrganizationSimple(input);
|
|
2085
|
+
const confirmation = await this.confirmIndividualOrganizationOrderSimple({
|
|
2086
|
+
tenantId: input.tenantId,
|
|
2087
|
+
jurisdiction: input.jurisdiction,
|
|
2088
|
+
sector: input.sector,
|
|
2089
|
+
offerId: started.offerId,
|
|
2090
|
+
timeoutSeconds: input.timeoutSeconds,
|
|
2091
|
+
intervalSeconds: input.intervalSeconds,
|
|
2092
|
+
});
|
|
2093
|
+
return {
|
|
2094
|
+
registration: started.registration,
|
|
2095
|
+
offerId: started.offerId,
|
|
2096
|
+
confirmation,
|
|
2097
|
+
};
|
|
2098
|
+
}
|
|
2099
|
+
|
|
1457
2100
|
/**
|
|
1458
2101
|
* UC 5.5 wrapper: import IPS/FHIR composition and update subject index context.
|
|
1459
2102
|
*/
|
|
1460
2103
|
public async importIpsOrFhirAndUpdateIndex(
|
|
1461
|
-
ctx: RouteContext,
|
|
2104
|
+
ctx: RouteContext | undefined,
|
|
1462
2105
|
input: IpsOrFhirImportInput,
|
|
1463
2106
|
): Promise<SubmitAndPollResult> {
|
|
2107
|
+
const routeCtx = this.requireRouteContext(ctx);
|
|
1464
2108
|
const payload = {
|
|
1465
2109
|
thid: input.compositionPayload.thid || `composition-${randomUUID()}`,
|
|
1466
2110
|
...input.compositionPayload,
|
|
1467
2111
|
};
|
|
1468
2112
|
|
|
1469
2113
|
const submitPath = (input.format || 'r4') === 'api'
|
|
1470
|
-
? this.individualCompositionR4BatchPath(
|
|
1471
|
-
: this.individualCompositionR4BatchPath(
|
|
2114
|
+
? this.individualCompositionR4BatchPath(routeCtx).replace('/org.hl7.fhir.r4/', '/org.hl7.fhir.api/')
|
|
2115
|
+
: this.individualCompositionR4BatchPath(routeCtx);
|
|
1472
2116
|
const pollPath = (input.format || 'r4') === 'api'
|
|
1473
|
-
? this.individualCompositionR4PollPath(
|
|
1474
|
-
: this.individualCompositionR4PollPath(
|
|
2117
|
+
? this.individualCompositionR4PollPath(routeCtx).replace('/org.hl7.fhir.r4/', '/org.hl7.fhir.api/')
|
|
2118
|
+
: this.individualCompositionR4PollPath(routeCtx);
|
|
1475
2119
|
|
|
1476
2120
|
return this.submitAndPoll(submitPath, pollPath, payload, input.pollOptions);
|
|
1477
2121
|
}
|
|
@@ -1481,9 +2125,10 @@ export class DataspaceNodeClient {
|
|
|
1481
2125
|
* Builds canonical Consent claims and submits/polls the Consent batch.
|
|
1482
2126
|
*/
|
|
1483
2127
|
public async grantProfessionalAccessSimple(
|
|
1484
|
-
ctx: RouteContext,
|
|
2128
|
+
ctx: RouteContext | undefined,
|
|
1485
2129
|
input: GrantProfessionalAccessSimpleInput,
|
|
1486
2130
|
): Promise<GrantProfessionalAccessSimpleResult> {
|
|
2131
|
+
const routeCtx = this.requireRouteContext(ctx);
|
|
1487
2132
|
const built = buildConsentClaimsSimpleWithCid(
|
|
1488
2133
|
{
|
|
1489
2134
|
subjectDid: input.subjectDid,
|
|
@@ -1518,8 +2163,8 @@ export class DataspaceNodeClient {
|
|
|
1518
2163
|
},
|
|
1519
2164
|
};
|
|
1520
2165
|
const consent = await this.submitAndPoll(
|
|
1521
|
-
this.individualConsentR4BatchPath(
|
|
1522
|
-
this.individualConsentR4PollPath(
|
|
2166
|
+
this.individualConsentR4BatchPath(routeCtx),
|
|
2167
|
+
this.individualConsentR4PollPath(routeCtx),
|
|
1523
2168
|
consentPayload,
|
|
1524
2169
|
input.pollOptions,
|
|
1525
2170
|
);
|
|
@@ -1538,20 +2183,21 @@ export class DataspaceNodeClient {
|
|
|
1538
2183
|
* UC 5.7 wrapper: generate digital twin composition from subject data.
|
|
1539
2184
|
*/
|
|
1540
2185
|
public async generateDigitalTwinFromSubjectData(
|
|
1541
|
-
ctx: RouteContext,
|
|
2186
|
+
ctx: RouteContext | undefined,
|
|
1542
2187
|
input: DigitalTwinGenerationInput,
|
|
1543
2188
|
): Promise<SubmitAndPollResult> {
|
|
2189
|
+
const routeCtx = this.requireRouteContext(ctx);
|
|
1544
2190
|
const payload = {
|
|
1545
2191
|
thid: input.compositionPayload.thid || `digital-twin-${randomUUID()}`,
|
|
1546
2192
|
...input.compositionPayload,
|
|
1547
2193
|
};
|
|
1548
2194
|
|
|
1549
2195
|
const submitPath = (input.format || 'r4') === 'api'
|
|
1550
|
-
? this.digitalTwinCompositionApiBatchPath(
|
|
1551
|
-
: this.digitalTwinCompositionR4BatchPath(
|
|
2196
|
+
? this.digitalTwinCompositionApiBatchPath(routeCtx)
|
|
2197
|
+
: this.digitalTwinCompositionR4BatchPath(routeCtx);
|
|
1552
2198
|
const pollPath = (input.format || 'r4') === 'api'
|
|
1553
|
-
? this.digitalTwinCompositionApiPollPath(
|
|
1554
|
-
: this.digitalTwinCompositionR4PollPath(
|
|
2199
|
+
? this.digitalTwinCompositionApiPollPath(routeCtx)
|
|
2200
|
+
: this.digitalTwinCompositionR4PollPath(routeCtx);
|
|
1555
2201
|
|
|
1556
2202
|
return this.submitAndPoll(submitPath, pollPath, payload, input.pollOptions);
|
|
1557
2203
|
}
|
|
@@ -1587,7 +2233,8 @@ export class DataspaceNodeClient {
|
|
|
1587
2233
|
throw new Error(`Polling timeout after ${attempts} attempts (${timeoutMs}ms).`);
|
|
1588
2234
|
}
|
|
1589
2235
|
|
|
1590
|
-
|
|
2236
|
+
const waitMs = options?.intervalMs ?? result.retryAfterMs ?? intervalMs;
|
|
2237
|
+
await new Promise((resolve) => setTimeout(resolve, waitMs));
|
|
1591
2238
|
}
|
|
1592
2239
|
}
|
|
1593
2240
|
|
|
@@ -1617,10 +2264,15 @@ export class DataspaceNodeClient {
|
|
|
1617
2264
|
|
|
1618
2265
|
private async parseResponseBody(response: Response): Promise<unknown> {
|
|
1619
2266
|
const contentType = response.headers.get('content-type') || '';
|
|
2267
|
+
const raw = await response.text();
|
|
2268
|
+
if (!raw) return {};
|
|
1620
2269
|
if (contentType.includes('application/json') || contentType.includes('application/didcomm-plaintext+json')) {
|
|
1621
|
-
|
|
2270
|
+
try {
|
|
2271
|
+
return JSON.parse(raw);
|
|
2272
|
+
} catch {
|
|
2273
|
+
return {};
|
|
2274
|
+
}
|
|
1622
2275
|
}
|
|
1623
|
-
|
|
1624
|
-
return text;
|
|
2276
|
+
return raw;
|
|
1625
2277
|
}
|
|
1626
2278
|
}
|