dataspace-client-sdk-node 0.2.2 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/README.md +13 -14
  2. package/docs/API.md +14 -13
  3. package/docs/DEVELOPER_USE_CASES.md +4 -12
  4. package/package.json +1 -1
  5. package/src/types.ts +8 -0
  6. package/TODO_PROMPT_NEXT_STEPS.md +0 -185
  7. package/artifacts/update-smart-wallet.js +0 -1016
  8. package/dist/builders.d.ts +0 -12
  9. package/dist/builders.js +0 -17
  10. package/dist/client.d.ts +0 -453
  11. package/dist/client.js +0 -1755
  12. package/dist/consent/pdfSignatureVerification.d.ts +0 -18
  13. package/dist/consent/pdfSignatureVerification.js +0 -23
  14. package/dist/index.d.ts +0 -5
  15. package/dist/index.js +0 -9
  16. package/dist/sdk/dataspace-wallet-sdk-node/MultiWalletClient.d.ts +0 -9
  17. package/dist/sdk/dataspace-wallet-sdk-node/MultiWalletClient.js +0 -21
  18. package/dist/sdk/dataspace-wallet-sdk-node/WalletClient.d.ts +0 -26
  19. package/dist/sdk/dataspace-wallet-sdk-node/WalletClient.js +0 -36
  20. package/dist/sdk/dataspace-wallet-sdk-node/index.d.ts +0 -6
  21. package/dist/sdk/dataspace-wallet-sdk-node/index.js +0 -6
  22. package/dist/sdk/dataspace-wallet-sdk-node/provider.d.ts +0 -24
  23. package/dist/sdk/dataspace-wallet-sdk-node/provider.js +0 -1
  24. package/dist/sdk/dataspace-wallet-sdk-node/providers/memory-provider.d.ts +0 -41
  25. package/dist/sdk/dataspace-wallet-sdk-node/providers/memory-provider.js +0 -216
  26. package/dist/sdk/dataspace-wallet-sdk-node/providers/seed-provider.d.ts +0 -22
  27. package/dist/sdk/dataspace-wallet-sdk-node/providers/seed-provider.js +0 -28
  28. package/dist/sdk/dataspace-wallet-sdk-node/types.d.ts +0 -51
  29. package/dist/sdk/dataspace-wallet-sdk-node/types.js +0 -1
  30. package/dist/types.d.ts +0 -556
  31. package/dist/types.js +0 -1
  32. package/dist/vp-token.d.ts +0 -37
  33. package/dist/vp-token.js +0 -56
package/dist/client.js DELETED
@@ -1,1755 +0,0 @@
1
- import { createHash, randomUUID } from 'node:crypto';
2
- import { createDidcommPlainMessage } from './builders.js';
3
- import { buildConsentClaimsSimpleWithCid } from 'gdc-common-utils-ts/utils/consent';
4
- import { generateServiceId } from 'gdc-common-utils-ts/utils/did';
5
- import { submitDidcomm } from 'gdc-common-utils-ts/utils/didcomm-submit';
6
- import { ClaimsOfferSchemaorg, ClaimsPersonSchemaorg } from 'gdc-common-utils-ts/constants/schemaorg';
7
- import { MedicationStatementClaimsFhirApi, MedicationStatementClaimsFhirApiExtended, } from 'gdc-common-utils-ts/models/interoperable-claims/medication-statement-claims';
8
- function trimTrailingSlash(value) {
9
- return value.replace(/\/+$/, '');
10
- }
11
- function encode(value) {
12
- return encodeURIComponent(value);
13
- }
14
- function toDidWebFromUrlOrHost(raw) {
15
- const v = String(raw || '').trim();
16
- if (!v)
17
- return undefined;
18
- if (v.startsWith('did:web:'))
19
- return v;
20
- const host = v
21
- .replace(/^https?:\/\//i, '')
22
- .replace(/\/.*$/, '')
23
- .trim()
24
- .toLowerCase();
25
- if (!host)
26
- return undefined;
27
- return `did:web:${host}`;
28
- }
29
- function parseRetryAfterMs(header) {
30
- if (!header)
31
- return undefined;
32
- const raw = header.trim();
33
- if (!raw)
34
- return undefined;
35
- const seconds = Number(raw);
36
- if (Number.isFinite(seconds) && seconds >= 0) {
37
- return Math.floor(seconds * 1000);
38
- }
39
- const epochMs = Date.parse(raw);
40
- if (Number.isFinite(epochMs)) {
41
- const delta = epochMs - Date.now();
42
- return delta > 0 ? delta : 0;
43
- }
44
- return undefined;
45
- }
46
- export class DataspaceNodeClient {
47
- baseUrl;
48
- bearerToken;
49
- defaultHeaders;
50
- wallet;
51
- defaultCtx;
52
- defaultTimeoutMs;
53
- defaultIntervalMs;
54
- _tokenCache = new Map();
55
- constructor(options) {
56
- this.baseUrl = trimTrailingSlash(options.baseUrl);
57
- this.bearerToken = options.bearerToken;
58
- this.defaultHeaders = options.defaultHeaders ?? {};
59
- this.wallet = options.wallet;
60
- this.defaultCtx = options.ctx;
61
- }
62
- getWallet() {
63
- return this.wallet;
64
- }
65
- /**
66
- * Set default route context for subsequent calls.
67
- */
68
- setContext(ctx) {
69
- this.defaultCtx = { ...ctx };
70
- return this;
71
- }
72
- /**
73
- * Preferred alias for organization/tenant integration context.
74
- */
75
- setContextOrg(ctx) {
76
- return this.setContext(ctx);
77
- }
78
- setTenantId(tenantId) {
79
- const current = this.defaultCtx ?? { tenantId: '', jurisdiction: '', sector: '' };
80
- this.defaultCtx = { ...current, tenantId };
81
- return this;
82
- }
83
- setJurisdiction(jurisdiction) {
84
- const current = this.defaultCtx ?? { tenantId: '', jurisdiction: '', sector: '' };
85
- this.defaultCtx = { ...current, jurisdiction };
86
- return this;
87
- }
88
- setSector(sector) {
89
- const current = this.defaultCtx ?? { tenantId: '', jurisdiction: '', sector: '' };
90
- this.defaultCtx = { ...current, sector };
91
- return this;
92
- }
93
- setDefaultTimeoutSeconds(seconds) {
94
- if (Number.isFinite(Number(seconds))) {
95
- this.defaultTimeoutMs = Math.max(1, Math.floor(Number(seconds) * 1000));
96
- }
97
- return this;
98
- }
99
- setDefaultIntervalSeconds(seconds) {
100
- if (Number.isFinite(Number(seconds))) {
101
- this.defaultIntervalMs = Math.max(1, Math.floor(Number(seconds) * 1000));
102
- }
103
- return this;
104
- }
105
- /**
106
- * Builds a deterministic endpoint id for token cache and auth/session reuse.
107
- * If `providerDid` is provided, returns a full DID service id:
108
- * did:web:...#section:format:resourceType:action
109
- * Otherwise returns the canonical fragment without '#':
110
- * section:format:resourceType:action
111
- */
112
- getEndpointId(selector, providerDid) {
113
- const fragment = generateServiceId(selector); // #section:format:resourceType:action
114
- if (providerDid)
115
- return `${providerDid}${fragment}`;
116
- return fragment.replace(/^#/, '');
117
- }
118
- resolveSimplePollOptions(timeoutSeconds, intervalSeconds) {
119
- const pollOptions = {};
120
- if (Number.isFinite(Number(timeoutSeconds))) {
121
- pollOptions.timeoutMs = Math.max(1, Math.floor(Number(timeoutSeconds) * 1000));
122
- }
123
- else if (this.defaultTimeoutMs) {
124
- pollOptions.timeoutMs = this.defaultTimeoutMs;
125
- }
126
- if (Number.isFinite(Number(intervalSeconds))) {
127
- pollOptions.intervalMs = Math.max(1, Math.floor(Number(intervalSeconds) * 1000));
128
- }
129
- else if (this.defaultIntervalMs) {
130
- pollOptions.intervalMs = this.defaultIntervalMs;
131
- }
132
- return Object.keys(pollOptions).length ? pollOptions : undefined;
133
- }
134
- requireRouteContext(ctx) {
135
- const resolved = ctx ?? this.defaultCtx;
136
- const tenantId = String(resolved?.tenantId || '').trim();
137
- const jurisdiction = String(resolved?.jurisdiction || '').trim();
138
- const sector = String(resolved?.sector || '').trim();
139
- if (!tenantId || !jurisdiction || !sector) {
140
- throw new Error('Route context is required. Provide `ctx` in method call or constructor options.');
141
- }
142
- return { tenantId, jurisdiction, sector };
143
- }
144
- requireHostRouteContext(ctx) {
145
- const jurisdiction = String(ctx?.jurisdiction || this.defaultCtx?.jurisdiction || '').trim();
146
- const sector = String(ctx?.sector || this.defaultCtx?.sector || '').trim();
147
- if (jurisdiction && sector) {
148
- return { jurisdiction, sector };
149
- }
150
- throw new Error('Host route context is required. Provide `ctx` in method call or constructor options.ctx.');
151
- }
152
- // ---- Path helpers -------------------------------------------------------
153
- /**
154
- * Generic GW v1 tenant route builder.
155
- * Use this for any section/format/resourceType/action combination not covered
156
- * by a dedicated convenience method.
157
- *
158
- * Pattern: `/{tenantId}/cds-{jurisdiction}/v1/{sector}/{section}/{format}/{resourceType}/{action}`
159
- *
160
- * @example
161
- * client.v1Path(ctx, 'individual', 'org.schema', 'Organization', '_batch')
162
- * // → /acme/cds-ES/v1/health-care/individual/org.schema/Organization/_batch
163
- */
164
- v1Path(ctx, section, format, resourceType, action) {
165
- const routeCtx = this.requireRouteContext(ctx);
166
- return `/${encode(routeCtx.tenantId)}/cds-${encode(routeCtx.jurisdiction)}/v1/${encode(routeCtx.sector)}/${encode(section)}/${encode(format)}/${encode(resourceType)}/${encode(action)}`;
167
- }
168
- /**
169
- * Generic tenant-scoped identity route builder.
170
- * Pattern: `/{prefix}/cds-{jurisdiction}/v1/{sector}/{tenantId}/identity/auth/{action}`
171
- *
172
- * The `prefix` is service-specific: `host` for GW, `publisher` for DataConv, `ica` for ICA.
173
- * Dedicated path methods in this SDK use `host` (GW convention).
174
- */
175
- tenantIdentityPath(ctx, prefix, action) {
176
- const routeCtx = this.requireRouteContext(ctx);
177
- return `/${encode(prefix)}/cds-${encode(routeCtx.jurisdiction)}/v1/${encode(routeCtx.sector)}/${encode(routeCtx.tenantId)}/identity/auth/${encode(action)}`;
178
- }
179
- /**
180
- * Generic host registry route builder (tenant-agnostic, `host/` prefix).
181
- * Use for controller-level registry operations (Organization activate, Order, etc.).
182
- *
183
- * Pattern: `/host/cds-{jurisdiction}/v1/{sector}/registry/org.schema/{resourceType}/{action}`
184
- */
185
- hostRegistryPath(ctx, resourceType, action) {
186
- const hostCtx = this.requireHostRouteContext(ctx);
187
- return `/host/cds-${encode(hostCtx.jurisdiction)}/v1/${encode(hostCtx.sector)}/registry/org.schema/${encode(resourceType)}/${encode(action)}`;
188
- }
189
- /** Submit path: host registry Organization batch (controller-level org registration). */
190
- hostRegistryOrganizationBatchPath(ctx) {
191
- return this.hostRegistryPath(ctx, 'Organization', '_batch');
192
- }
193
- /** Poll path: host registry Organization batch. Pair with `hostRegistryOrganizationBatchPath`. */
194
- hostRegistryOrganizationPollPath(ctx) {
195
- return this.hostRegistryPath(ctx, 'Organization', '_batch-response');
196
- }
197
- /** Submit path: activate a tenant Organization in the GW registry using a VC from ICA. */
198
- hostRegistryOrganizationActivatePath(ctx) {
199
- return this.hostRegistryPath(ctx, 'Organization', '_activate');
200
- }
201
- /** Poll path: `_activate` response. Pair with `hostRegistryOrganizationActivatePath`. */
202
- hostRegistryOrganizationActivatePollPath(ctx) {
203
- return this.hostRegistryPath(ctx, 'Organization', '_activate-response');
204
- }
205
- /** Submit path: host registry Order batch (controller-level order submission). */
206
- hostRegistryOrderBatchPath(ctx) {
207
- return this.hostRegistryPath(ctx, 'Order', '_batch');
208
- }
209
- /** Poll path: host registry Order batch. Pair with `hostRegistryOrderBatchPath`. */
210
- hostRegistryOrderPollPath(ctx) {
211
- return this.hostRegistryPath(ctx, 'Order', '_batch-response');
212
- }
213
- /**
214
- * Submit path: individual/family Organization onboarding (`org.schema/Organization/_batch`).
215
- * Use for `family-registration/_create-or-resume` DIDComm payloads.
216
- */
217
- individualFamilyOrganizationBatchPath(ctx) {
218
- const routeCtx = this.requireRouteContext(ctx);
219
- return `/${encode(routeCtx.tenantId)}/cds-${encode(routeCtx.jurisdiction)}/v1/${encode(routeCtx.sector)}/individual/org.schema/Organization/_batch`;
220
- }
221
- /** Poll path: individual/family Organization. Pair with `individualFamilyOrganizationBatchPath`. */
222
- individualFamilyOrganizationPollPath(ctx) {
223
- const routeCtx = this.requireRouteContext(ctx);
224
- return `/${encode(routeCtx.tenantId)}/cds-${encode(routeCtx.jurisdiction)}/v1/${encode(routeCtx.sector)}/individual/org.schema/Organization/_batch-response`;
225
- }
226
- /** Submit path: individual/family Organization search (`org.schema/Organization/_search`). */
227
- individualFamilyOrganizationSearchPath(ctx) {
228
- return this.v1Path(ctx, 'individual', 'org.schema', 'Organization', '_search');
229
- }
230
- /** Poll path: individual/family Organization search. Pair with `individualFamilyOrganizationSearchPath`. */
231
- individualFamilyOrganizationSearchPollPath(ctx) {
232
- return this.v1Path(ctx, 'individual', 'org.schema', 'Organization', '_search-response');
233
- }
234
- /** Submit path: individual/family Order batch (`org.schema/Order/_batch`). */
235
- individualFamilyOrderBatchPath(ctx) {
236
- const routeCtx = this.requireRouteContext(ctx);
237
- return `/${encode(routeCtx.tenantId)}/cds-${encode(routeCtx.jurisdiction)}/v1/${encode(routeCtx.sector)}/individual/org.schema/Order/_batch`;
238
- }
239
- /** Poll path: individual/family Order. Pair with `individualFamilyOrderBatchPath`. */
240
- individualFamilyOrderPollPath(ctx) {
241
- const routeCtx = this.requireRouteContext(ctx);
242
- return `/${encode(routeCtx.tenantId)}/cds-${encode(routeCtx.jurisdiction)}/v1/${encode(routeCtx.sector)}/individual/org.schema/Order/_batch-response`;
243
- }
244
- /** Submit path: individual RelatedPerson (FHIR R4 API, `org.hl7.fhir.api/RelatedPerson/_batch`). */
245
- individualRelatedPersonBatchPath(ctx) {
246
- return `/${encode(ctx.tenantId)}/cds-${encode(ctx.jurisdiction)}/v1/${encode(ctx.sector)}/individual/org.hl7.fhir.api/RelatedPerson/_batch`;
247
- }
248
- /** Poll path: individual RelatedPerson. Pair with `individualRelatedPersonBatchPath`. */
249
- individualRelatedPersonPollPath(ctx) {
250
- return `/${encode(ctx.tenantId)}/cds-${encode(ctx.jurisdiction)}/v1/${encode(ctx.sector)}/individual/org.hl7.fhir.api/RelatedPerson/_batch-response`;
251
- }
252
- /** Submit path: individual Observation (FHIR R4 API, `org.hl7.fhir.api/Observation/_batch`). */
253
- individualObservationBatchPath(ctx) {
254
- return `/${encode(ctx.tenantId)}/cds-${encode(ctx.jurisdiction)}/v1/${encode(ctx.sector)}/individual/org.hl7.fhir.api/Observation/_batch`;
255
- }
256
- /** Poll path: individual Observation. Pair with `individualObservationBatchPath`. */
257
- individualObservationPollPath(ctx) {
258
- return `/${encode(ctx.tenantId)}/cds-${encode(ctx.jurisdiction)}/v1/${encode(ctx.sector)}/individual/org.hl7.fhir.api/Observation/_batch-response`;
259
- }
260
- /** Submit path: individual Communication (FHIR R4, `org.hl7.fhir.r4/Communication/_batch`). */
261
- individualCommunicationBatchPath(ctx) {
262
- return `/${encode(ctx.tenantId)}/cds-${encode(ctx.jurisdiction)}/v1/${encode(ctx.sector)}/individual/org.hl7.fhir.r4/Communication/_batch`;
263
- }
264
- /** Poll path: individual Communication. Pair with `individualCommunicationBatchPath`. */
265
- individualCommunicationPollPath(ctx) {
266
- return `/${encode(ctx.tenantId)}/cds-${encode(ctx.jurisdiction)}/v1/${encode(ctx.sector)}/individual/org.hl7.fhir.r4/Communication/_batch-response`;
267
- }
268
- /** Submit path: individual Task (FHIR R4 API, `org.hl7.fhir.api/Task/_batch`). */
269
- individualTaskBatchPath(ctx) {
270
- return this.v1Path(ctx, 'individual', 'org.hl7.fhir.api', 'Task', '_batch');
271
- }
272
- /** Poll path: individual Task. Pair with `individualTaskBatchPath`. */
273
- individualTaskPollPath(ctx) {
274
- return this.v1Path(ctx, 'individual', 'org.hl7.fhir.api', 'Task', '_batch-response');
275
- }
276
- /** Submit path: entity Employee (`entity/org.schema/Employee/_batch`). */
277
- employeeBatchPath(ctx) {
278
- return this.v1Path(ctx, 'entity', 'org.schema', 'Employee', '_batch');
279
- }
280
- /** Poll path: entity Employee. Pair with `employeeBatchPath`. */
281
- employeePollPath(ctx) {
282
- return this.v1Path(ctx, 'entity', 'org.schema', 'Employee', '_batch-response');
283
- }
284
- /** Submit path: individual Person legacy format (`individual/org.schema/Person/_batch`). Use for older flows; prefer Organization for family onboarding. */
285
- individualLegacyPersonBatchPath(ctx) {
286
- return this.v1Path(ctx, 'individual', 'org.schema', 'Person', '_batch');
287
- }
288
- /** Submit path: individual Consent (FHIR R4, `org.hl7.fhir.r4/Consent/_batch`). */
289
- individualConsentR4BatchPath(ctx) {
290
- return this.v1Path(ctx, 'individual', 'org.hl7.fhir.r4', 'Consent', '_batch');
291
- }
292
- /** Poll path: individual Consent R4. Pair with `individualConsentR4BatchPath`. */
293
- individualConsentR4PollPath(ctx) {
294
- return this.v1Path(ctx, 'individual', 'org.hl7.fhir.r4', 'Consent', '_batch-response');
295
- }
296
- /** Submit path: individual Composition (FHIR R4, `org.hl7.fhir.r4/Composition/_batch`). */
297
- individualCompositionR4BatchPath(ctx) {
298
- return this.v1Path(ctx, 'individual', 'org.hl7.fhir.r4', 'Composition', '_batch');
299
- }
300
- /** Poll path: individual Composition R4. Pair with `individualCompositionR4BatchPath`. */
301
- individualCompositionR4PollPath(ctx) {
302
- return this.v1Path(ctx, 'individual', 'org.hl7.fhir.r4', 'Composition', '_batch-response');
303
- }
304
- /** Submit path: digital twin Composition (FHIR API format, `digitaltwin/org.hl7.fhir.api/Composition/_batch`). */
305
- digitalTwinCompositionApiBatchPath(ctx) {
306
- return this.v1Path(ctx, 'digitaltwin', 'org.hl7.fhir.api', 'Composition', '_batch');
307
- }
308
- /** Poll path: digital twin Composition API. Pair with `digitalTwinCompositionApiBatchPath`. */
309
- digitalTwinCompositionApiPollPath(ctx) {
310
- return this.v1Path(ctx, 'digitaltwin', 'org.hl7.fhir.api', 'Composition', '_batch-response');
311
- }
312
- /** Submit path: digital twin Composition (FHIR R4 format, `digitaltwin/org.hl7.fhir.r4/Composition/_batch`). */
313
- digitalTwinCompositionR4BatchPath(ctx) {
314
- return this.v1Path(ctx, 'digitaltwin', 'org.hl7.fhir.r4', 'Composition', '_batch');
315
- }
316
- /** Poll path: digital twin Composition R4. Pair with `digitalTwinCompositionR4BatchPath`. */
317
- digitalTwinCompositionR4PollPath(ctx) {
318
- return this.v1Path(ctx, 'digitaltwin', 'org.hl7.fhir.r4', 'Composition', '_batch-response');
319
- }
320
- /**
321
- * Submit path: identity DCR step — binds API key to service public JWK.
322
- * Used internally by `authenticateBackendPkceAndExchange` (step 1 of identity-exchange.v1).
323
- */
324
- identityDeviceDcrPath(ctx) {
325
- return this.tenantIdentityPath(ctx, 'host', '_dcr');
326
- }
327
- /** Poll path: identity DCR. Pair with `identityDeviceDcrPath`. */
328
- identityDeviceDcrPollPath(ctx) {
329
- return this.tenantIdentityPath(ctx, 'host', '_dcr-response');
330
- }
331
- /**
332
- * Submit path: identity token exchange — id_token → SMART bearer.
333
- * Used internally by `authenticateBackendPkceAndExchange` (step 4 of identity-exchange.v1).
334
- */
335
- identityTokenExchangePath(ctx) {
336
- return this.tenantIdentityPath(ctx, 'host', '_exchange');
337
- }
338
- /** Poll path: identity token exchange. Pair with `identityTokenExchangePath`. */
339
- identityTokenExchangePollPath(ctx) {
340
- return this.tenantIdentityPath(ctx, 'host', '_exchange-response');
341
- }
342
- /** Submit path: identity license issue (`identity/auth/_issue`). */
343
- identityLicenseIssuePath(ctx) {
344
- return this.tenantIdentityPath(ctx, 'host', '_issue');
345
- }
346
- /**
347
- * Submit path: SMART token step — code + code_verifier → id_token.
348
- * Used internally by `authenticateBackendPkceAndExchange` (step 3 of identity-exchange.v1).
349
- */
350
- identitySmartTokenPath(ctx) {
351
- return this.tenantIdentityPath(ctx, 'host', '_token');
352
- }
353
- /** Poll path: SMART token. Pair with `identitySmartTokenPath`. */
354
- identitySmartTokenPollPath(ctx) {
355
- return this.tenantIdentityPath(ctx, 'host', '_token-response');
356
- }
357
- /** Submit path: Firebase custom token exchange (end-user device flow, NOT B2B). */
358
- identityFirebaseCustomPath(ctx) {
359
- return this.tenantIdentityPath(ctx, 'host', '_custom');
360
- }
361
- /** Poll path: Firebase custom token. Pair with `identityFirebaseCustomPath`. */
362
- identityFirebaseCustomPollPath(ctx) {
363
- return this.tenantIdentityPath(ctx, 'host', '_custom-response');
364
- }
365
- /**
366
- * Submit path: identity PKCE code step — sends S256 code_challenge.
367
- * Used internally by `authenticateBackendPkceAndExchange` (step 2 of identity-exchange.v1).
368
- */
369
- identityCodePath(ctx) {
370
- return this.tenantIdentityPath(ctx, 'host', '_code');
371
- }
372
- /** Poll path: identity PKCE code. Pair with `identityCodePath`. */
373
- identityCodePollPath(ctx) {
374
- return this.tenantIdentityPath(ctx, 'host', '_code-response');
375
- }
376
- /** Submit path: UHC debug task call-start (`individual/{format}/Task/_call-start`). For telephony integration testing. */
377
- taskDebugCallStartPath(ctx, format = 'org.hl7.fhir.api') {
378
- return this.v1Path(ctx, 'individual', format, 'Task', '_call-start');
379
- }
380
- /** Path: UHC debug task logs (`individual/{format}/Task/_logs`). Retrieve async task execution logs. */
381
- taskDebugLogsPath(ctx, format = 'org.hl7.fhir.api') {
382
- return this.v1Path(ctx, 'individual', format, 'Task', '_logs');
383
- }
384
- /**
385
- * Submit path: DataConversion file upload.
386
- * Pattern: `/{tenantId}/cds-{jurisdiction}/v1/{sector}/conversion/{softwareId}/{sourceFormat}/_upload`
387
- * Use with `uploadConversionFile` to send a file (e.g. XLSX) for async processing.
388
- */
389
- conversionUploadPath(ctx, softwareId, sourceFormat) {
390
- return `/${encode(ctx.tenantId)}/cds-${encode(ctx.jurisdiction)}/v1/${encode(ctx.sector)}/conversion/${encode(softwareId)}/${encode(sourceFormat)}/_upload`;
391
- }
392
- /** Poll path: DataConversion upload. Pair with `conversionUploadPath`. */
393
- conversionUploadPollPath(ctx, softwareId, sourceFormat) {
394
- return `/${encode(ctx.tenantId)}/cds-${encode(ctx.jurisdiction)}/v1/${encode(ctx.sector)}/conversion/${encode(softwareId)}/${encode(sourceFormat)}/_upload-response`;
395
- }
396
- // ---- Backend PKCE auth (identity-exchange.v1) -------------------------
397
- /**
398
- * Orchestrates the full identity-exchange.v1 backend auth flow:
399
- * DCR binding → PKCE code → token → SMART bearer exchange.
400
- *
401
- * Equivalent to Python connector_sdk `authenticate_backend_pkce_and_exchange`.
402
- * Results are cached in memory; re-runs automatically on expiry.
403
- */
404
- async authenticateBackendPkceAndExchange(options) {
405
- const { ctx, apiKey, scopes, tokenCacheKey = `pkce:${apiKey.slice(0, 8)}`, endpointId, codeVerifier = randomUUID(), pollOptions, } = options;
406
- const cacheKey = String(tokenCacheKey || endpointId || '').trim() || `pkce:${apiKey.slice(0, 8)}`;
407
- const cached = this._tokenCache.get(cacheKey);
408
- if (cached && cached.expiresAt > Date.now() + 30_000) {
409
- return { status: 'cached', tokenCacheKey: cacheKey, endpointId: cacheKey, accessToken: cached.accessToken, tokenType: cached.tokenType, scopes: cached.scopes };
410
- }
411
- const controllerPublicJwk = await this.resolveControllerPublicJwk(options);
412
- // Step 1: DCR – bind API key to service public key
413
- const dcrPayload = this._buildAuthDIDCommRequest({
414
- thid: `dcr-${randomUUID()}`,
415
- clientId: apiKey,
416
- body: {},
417
- controllerPublicJwk,
418
- });
419
- await this.submitBatch(this.identityDeviceDcrPath(ctx), dcrPayload);
420
- const dcrPoll = await this.pollUntilComplete(this.identityDeviceDcrPollPath(ctx), { thid: String(dcrPayload['thid']) }, pollOptions);
421
- if (dcrPoll.status !== 200) {
422
- return { status: 'failed', step: '_dcr', tokenCacheKey: cacheKey, endpointId: cacheKey, accessToken: '', tokenType: 'Bearer', scopes };
423
- }
424
- // Step 2: Code – PKCE S256 challenge
425
- const codeChallenge = this._pkceS256Challenge(codeVerifier);
426
- const codePayload = this._buildAuthDIDCommRequest({
427
- thid: `code-${randomUUID()}`,
428
- clientId: apiKey,
429
- body: {},
430
- controllerPublicJwk,
431
- extra: { code_challenge: codeChallenge, code_challenge_method: 'S256' },
432
- });
433
- await this.submitBatch(this.identityCodePath(ctx), codePayload);
434
- const codePoll = await this.pollUntilComplete(this.identityCodePollPath(ctx), { thid: String(codePayload['thid']) }, pollOptions);
435
- const codeBody = codePoll.body ?? {};
436
- const code = String(codeBody['code'] ?? '').trim();
437
- if (codePoll.status !== 200 || !code) {
438
- return { status: 'failed', step: '_code', tokenCacheKey: cacheKey, endpointId: cacheKey, accessToken: '', tokenType: 'Bearer', scopes };
439
- }
440
- // Step 3: Token – exchange code + verifier for id_token
441
- const tokenPayload = this._buildAuthDIDCommRequest({
442
- thid: `token-${randomUUID()}`,
443
- clientId: apiKey,
444
- body: {},
445
- controllerPublicJwk,
446
- extra: { code, code_verifier: codeVerifier },
447
- });
448
- await this.submitBatch(this.identitySmartTokenPath(ctx), tokenPayload);
449
- const tokenPoll = await this.pollUntilComplete(this.identitySmartTokenPollPath(ctx), { thid: String(tokenPayload['thid']) }, pollOptions);
450
- const tokenBody = tokenPoll.body ?? {};
451
- const idToken = String(tokenBody['id_token'] ?? '').trim();
452
- if (tokenPoll.status !== 200 || !idToken) {
453
- return { status: 'failed', step: '_token', tokenCacheKey: cacheKey, endpointId: cacheKey, accessToken: '', tokenType: 'Bearer', scopes };
454
- }
455
- // Step 4: Exchange – id_token → SMART bearer
456
- const exchangeThid = `exchange-${randomUUID()}`;
457
- const exchangePayload = {
458
- grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange',
459
- subject_token_type: 'urn:ietf:params:oauth:token-type:id_token',
460
- subject_token: idToken,
461
- scope: scopes.join(' '),
462
- api_key: apiKey,
463
- organization: ctx.tenantId,
464
- thid: exchangeThid,
465
- };
466
- await this.submitBatch(this.identityTokenExchangePath(ctx), exchangePayload);
467
- const exchangePoll = await this.pollUntilComplete(this.identityTokenExchangePollPath(ctx), { thid: exchangeThid }, pollOptions);
468
- const exchangeBody = exchangePoll.body ?? {};
469
- const accessToken = String(exchangeBody['access_token'] ?? '').trim();
470
- if (exchangePoll.status !== 200 || !accessToken) {
471
- return { status: 'failed', step: '_exchange', tokenCacheKey: cacheKey, endpointId: cacheKey, accessToken: '', tokenType: 'Bearer', scopes };
472
- }
473
- const tokenType = String(exchangeBody['token_type'] ?? 'Bearer');
474
- const grantedScope = String(exchangeBody['scope'] ?? '').trim();
475
- const grantedScopes = grantedScope ? grantedScope.split(' ').filter(Boolean) : scopes;
476
- const expiresIn = Number(exchangeBody['expires_in'] ?? 0);
477
- this._tokenCache.set(cacheKey, {
478
- accessToken,
479
- tokenType,
480
- scopes: grantedScopes,
481
- expiresAt: Date.now() + expiresIn * 1000,
482
- });
483
- return { status: 'fetched', tokenCacheKey: cacheKey, endpointId: cacheKey, accessToken, tokenType, scopes: grantedScopes };
484
- }
485
- /**
486
- * Returns the cached SMART bearer for the given endpointId if still valid (>30s remaining).
487
- * Returns `undefined` if not cached or expired.
488
- */
489
- getCachedBearerToken(tokenCacheKey) {
490
- const cached = this._tokenCache.get(tokenCacheKey);
491
- if (cached && cached.expiresAt > Date.now() + 30_000) {
492
- return cached.accessToken;
493
- }
494
- return undefined;
495
- }
496
- /**
497
- * smart-backend.v1: obtain an OAuth2 backend token using client_credentials + private_key_jwt.
498
- */
499
- async authenticateBackendSmartStandard(options) {
500
- const { clientId, scopes, tokenCacheKey = `smart-backend:${clientId}`, endpointId, tokenUrl, tokenPath = '/token', audience, assertionTtlSeconds = 300, additionalTokenFields, } = options;
501
- const cacheKey = String(tokenCacheKey || endpointId || '').trim() || `smart-backend:${clientId}`;
502
- const cached = this._tokenCache.get(cacheKey);
503
- if (cached && cached.expiresAt > Date.now() + 30_000) {
504
- return {
505
- status: 'cached',
506
- profile: 'smart-backend.v1',
507
- tokenCacheKey: cacheKey,
508
- endpointId: cacheKey,
509
- accessToken: cached.accessToken,
510
- tokenType: cached.tokenType,
511
- scopes: cached.scopes,
512
- expiresAt: new Date(cached.expiresAt).toISOString(),
513
- };
514
- }
515
- const resolvedTokenUrl = this.resolveStandardTokenUrl(tokenUrl, tokenPath);
516
- const publicJwk = await this.resolveSmartAuthPublicJwk(options);
517
- const clientAssertion = await this.signSmartBackendClientAssertion({
518
- clientId,
519
- audience: audience ?? resolvedTokenUrl,
520
- publicJwk,
521
- ttlSeconds: assertionTtlSeconds,
522
- walletContext: options.walletContext,
523
- });
524
- const tokenRequest = {
525
- grant_type: 'client_credentials',
526
- client_id: clientId,
527
- scope: scopes.join(' '),
528
- client_assertion_type: 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',
529
- client_assertion: clientAssertion,
530
- ...(additionalTokenFields ?? {}),
531
- };
532
- const response = await this.postJson(tokenUrl ?? tokenPath, tokenRequest);
533
- const body = response.body ?? {};
534
- const accessToken = String(body.access_token ?? '').trim();
535
- if (response.status >= 400 || !accessToken) {
536
- return {
537
- status: 'failed',
538
- profile: 'smart-backend.v1',
539
- tokenCacheKey: cacheKey,
540
- endpointId: cacheKey,
541
- statusCode: response.status,
542
- response,
543
- };
544
- }
545
- const tokenType = String(body.token_type ?? 'Bearer');
546
- const grantedScope = String(body.scope ?? '').trim();
547
- const grantedScopes = grantedScope ? grantedScope.split(' ').filter(Boolean) : scopes;
548
- const expiresIn = Number(body.expires_in ?? 0);
549
- const expiresAt = Date.now() + expiresIn * 1000;
550
- this._tokenCache.set(cacheKey, {
551
- accessToken,
552
- tokenType,
553
- scopes: grantedScopes,
554
- expiresAt,
555
- });
556
- return {
557
- status: 'fetched',
558
- profile: 'smart-backend.v1',
559
- tokenCacheKey: cacheKey,
560
- endpointId: cacheKey,
561
- statusCode: response.status,
562
- accessToken,
563
- tokenType,
564
- scopes: grantedScopes,
565
- expiresAt: new Date(expiresAt).toISOString(),
566
- response,
567
- };
568
- }
569
- /**
570
- * Exchange token payload against gateway token endpoint and cache the result.
571
- */
572
- async requestSmartToken(input) {
573
- const tokenCacheKey = String(input.tokenCacheKey || input.endpointId || '').trim();
574
- if (!tokenCacheKey) {
575
- throw new Error('requestSmartToken requires tokenCacheKey.');
576
- }
577
- const normalizedScopes = Array.from(new Set((input.scopes || []).filter(Boolean))).sort();
578
- const cached = this._tokenCache.get(tokenCacheKey);
579
- if (cached && cached.expiresAt > Date.now() + 30_000) {
580
- return {
581
- status: 'cached',
582
- accessToken: cached.accessToken,
583
- tokenType: cached.tokenType,
584
- scopes: cached.scopes,
585
- };
586
- }
587
- const response = await this.postJson(input.path || '/token', input.exchangePayload || {});
588
- const body = response.body ?? {};
589
- const accessToken = String(body.access_token ?? '').trim();
590
- if (response.status >= 400 || !accessToken) {
591
- return {
592
- status: 'failed',
593
- statusCode: response.status,
594
- response,
595
- };
596
- }
597
- const tokenType = String(body.token_type ?? 'Bearer');
598
- const grantedScopes = Array.isArray(body.granted_scopes)
599
- ? body.granted_scopes
600
- : String(body.scope ?? '').trim().split(' ').filter(Boolean);
601
- const resolvedScopes = grantedScopes.length ? grantedScopes : normalizedScopes;
602
- const expiresIn = Number(body.expires_in ?? 0);
603
- this._tokenCache.set(tokenCacheKey, {
604
- accessToken,
605
- tokenType,
606
- scopes: resolvedScopes,
607
- expiresAt: Date.now() + expiresIn * 1000,
608
- });
609
- return {
610
- status: 'fetched',
611
- accessToken,
612
- tokenType,
613
- scopes: resolvedScopes,
614
- statusCode: response.status,
615
- response,
616
- };
617
- }
618
- /**
619
- * Friendly wrapper for SMART token request via GW identity/auth token-exchange route.
620
- * Uses one object, seconds-based polling, and constructor ctx fallback.
621
- */
622
- async requestSmartTokenSimple(input) {
623
- const routeCtx = this.requireRouteContext(input.tenantId && input.jurisdiction && input.sector
624
- ? { tenantId: input.tenantId, jurisdiction: input.jurisdiction, sector: input.sector }
625
- : undefined);
626
- const normalizedScopes = Array.from(new Set((input.scopes || []).filter(Boolean))).sort();
627
- const tokenCacheKey = String(input.tokenCacheKey || input.endpointId || `smart:${routeCtx.tenantId}:${normalizedScopes.join(',')}`).trim();
628
- if (!tokenCacheKey) {
629
- throw new Error('requestSmartTokenSimple requires tokenCacheKey (or non-empty scopes).');
630
- }
631
- const pollOptions = this.resolveSimplePollOptions(input.timeoutSeconds, input.intervalSeconds);
632
- const payload = {
633
- thid: `exchange-${randomUUID()}`,
634
- grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange',
635
- subject_token_type: 'urn:ietf:params:oauth:token-type:id_token',
636
- subject_token: input.idToken,
637
- scope: normalizedScopes.join(' '),
638
- organization: routeCtx.tenantId,
639
- ...(input.additionalClaims || {}),
640
- };
641
- const exchange = await this.submitAndPoll(this.identityTokenExchangePath(routeCtx), this.identityTokenExchangePollPath(routeCtx), payload, pollOptions);
642
- const exchangeBody = exchange.poll.body ?? {};
643
- const accessToken = String(exchangeBody.access_token || '').trim();
644
- if (exchange.poll.status >= 400 || !accessToken) {
645
- return {
646
- status: 'failed',
647
- statusCode: exchange.poll.status,
648
- response: exchange.poll.body,
649
- };
650
- }
651
- const tokenType = String(exchangeBody.token_type || 'Bearer');
652
- const grantedScopes = String(exchangeBody.scope || '').trim().split(' ').filter(Boolean);
653
- const resolvedScopes = grantedScopes.length ? grantedScopes : normalizedScopes;
654
- const expiresIn = Number(exchangeBody.expires_in ?? 0);
655
- this._tokenCache.set(tokenCacheKey, {
656
- accessToken,
657
- tokenType,
658
- scopes: resolvedScopes,
659
- expiresAt: Date.now() + expiresIn * 1000,
660
- });
661
- return {
662
- status: 'fetched',
663
- accessToken,
664
- tokenType,
665
- scopes: resolvedScopes,
666
- statusCode: exchange.poll.status,
667
- response: exchange.poll.body,
668
- };
669
- }
670
- // ---- Private auth helpers ----------------------------------------------
671
- _pkceS256Challenge(verifier) {
672
- return createHash('sha256').update(verifier).digest().toString('base64url');
673
- }
674
- _buildAuthDIDCommRequest(params) {
675
- const now = Math.floor(Date.now() / 1000);
676
- return {
677
- thid: params.thid,
678
- type: 'application/bundle-api+json',
679
- iat: now,
680
- exp: now + 300,
681
- client_id: params.clientId,
682
- body: params.body,
683
- meta: {
684
- jws: {
685
- protected: {
686
- alg: 'ES384',
687
- jwk: params.controllerPublicJwk,
688
- },
689
- },
690
- },
691
- ...(params.extra ?? {}),
692
- };
693
- }
694
- async resolveControllerPublicJwk(options) {
695
- if (options.controllerPublicJwk) {
696
- return options.controllerPublicJwk;
697
- }
698
- if (!this.wallet) {
699
- throw new Error('authenticateBackendPkceAndExchange requires controllerPublicJwk or a configured wallet provider.');
700
- }
701
- const walletContext = options.walletContext ?? {
702
- tenantId: options.ctx.tenantId,
703
- jurisdiction: options.ctx.jurisdiction,
704
- sector: options.ctx.sector,
705
- };
706
- const publicJwks = await this.wallet.getPublicJwks(walletContext);
707
- const controllerPublicJwk = publicJwks.find((jwk) => jwk.use === 'sig' || jwk.alg === 'ES384') ?? publicJwks[0];
708
- if (!controllerPublicJwk) {
709
- throw new Error('Wallet provider returned no public JWKs for the requested context.');
710
- }
711
- return controllerPublicJwk;
712
- }
713
- resolveStandardTokenUrl(tokenUrl, tokenPath) {
714
- if (tokenUrl && tokenUrl.trim()) {
715
- return tokenUrl.trim();
716
- }
717
- return `${this.baseUrl}${tokenPath.startsWith('/') ? tokenPath : `/${tokenPath}`}`;
718
- }
719
- async resolveSmartAuthPublicJwk(options) {
720
- if (options.publicJwk) {
721
- return options.publicJwk;
722
- }
723
- if (!this.wallet) {
724
- throw new Error('authenticateBackendSmartStandard requires publicJwk or a configured wallet provider.');
725
- }
726
- const walletContext = options.walletContext ?? {
727
- tenantId: options.clientId,
728
- jurisdiction: 'global',
729
- sector: 'backend',
730
- };
731
- const publicJwks = await this.wallet.getPublicJwks(walletContext);
732
- const signingJwk = publicJwks.find((jwk) => jwk.use === 'sig' || jwk.alg === 'ES384') ?? publicJwks[0];
733
- if (!signingJwk) {
734
- throw new Error('Wallet provider returned no public JWKs for smart-backend.v1.');
735
- }
736
- return signingJwk;
737
- }
738
- async signSmartBackendClientAssertion(params) {
739
- if (!this.wallet) {
740
- throw new Error('smart-backend.v1 signing requires a configured wallet provider.');
741
- }
742
- const now = Math.floor(Date.now() / 1000);
743
- const walletContext = params.walletContext ?? {
744
- tenantId: params.clientId,
745
- jurisdiction: 'global',
746
- sector: 'backend',
747
- };
748
- const kid = String(params.publicJwk.kid ?? '').trim();
749
- return this.wallet.signCompactJws(walletContext, {
750
- header: {
751
- typ: 'JWT',
752
- alg: this.preferredJwtAlg(params.publicJwk),
753
- ...(kid ? { kid } : {}),
754
- },
755
- claims: {
756
- iss: params.clientId,
757
- sub: params.clientId,
758
- aud: params.audience,
759
- iat: now,
760
- exp: now + Math.max(params.ttlSeconds, 1),
761
- jti: `jwt-${randomUUID()}`,
762
- },
763
- });
764
- }
765
- preferredJwtAlg(publicJwk) {
766
- const jwk = publicJwk;
767
- const alg = String(jwk.alg ?? '').trim();
768
- if (alg) {
769
- return alg;
770
- }
771
- const kty = String(jwk.kty ?? '').toUpperCase();
772
- const crv = String(jwk.crv ?? '').toUpperCase();
773
- if (kty === 'EC' && crv === 'P-256') {
774
- return 'ES256';
775
- }
776
- if (kty === 'EC' && crv === 'P-384') {
777
- return 'ES384';
778
- }
779
- if (kty === 'RSA') {
780
- return 'RS384';
781
- }
782
- return 'ES384';
783
- }
784
- // ---- Generic batch API --------------------------------------------------
785
- /**
786
- * POST a DIDComm bundle payload.
787
- * This is the preferred high-level method for DIDComm submission of
788
- * FHIR/API bundles (batch, transaction, message, etc.).
789
- *
790
- * Returns immediately with the HTTP response — pair with `pollUntilComplete` or use `submitAndPoll`.
791
- */
792
- async submitBundle(path, payload, options) {
793
- const mode = options?.mode ?? 'plain';
794
- if (mode === 'strict') {
795
- if (!options?.recipientEncryptionJwk || !options?.walletContext) {
796
- throw new Error('submitBundle strict mode requires recipientEncryptionJwk and walletContext.');
797
- }
798
- return this.submitBatchEncrypted(path, payload, options.recipientEncryptionJwk, options.walletContext);
799
- }
800
- return this.submitBatch(path, payload);
801
- }
802
- /**
803
- * @deprecated Use `submitBundle` instead.
804
- *
805
- * POST a DIDComm plaintext payload to a batch submit path.
806
- * Use this for all `_batch` routes (family registration, observations, tasks, etc.).
807
- * Content-Type: `application/didcomm-plaintext+json`.
808
- *
809
- * Returns immediately with the HTTP response — pair with `pollUntilComplete` or use `submitAndPoll`.
810
- */
811
- async submitBatch(path, payload) {
812
- const url = /^https?:\/\//.test(path)
813
- ? path
814
- : `${this.baseUrl}${path.startsWith('/') ? path : `/${path}`}`;
815
- const result = await submitDidcomm({
816
- mode: 'plain',
817
- url,
818
- payload: payload,
819
- defaultHeaders: this.defaultHeaders,
820
- bearerToken: this.bearerToken,
821
- fetcher: (requestUrl, init) => fetch(requestUrl, init),
822
- });
823
- return { status: result.status, location: result.location, body: result.body };
824
- }
825
- /**
826
- * Sign and encrypt a DIDComm payload (nested JWS-in-JWE) and POST to the given path.
827
- * Content-Type: `application/didcomm-encrypted+json`.
828
- *
829
- * Flow: `payload JSON → ES384 compact JWS → RSA-OAEP-256/A256GCM compact JWE → POST`
830
- *
831
- * Requires a wallet provider and the recipient's RSA encryption JWK
832
- * (e.g. from GW `.well-known/jwks.json` where `use === 'enc'`).
833
- */
834
- async submitBatchEncrypted(path, payload, recipientEncryptionJwk, walletContext) {
835
- if (!this.wallet) {
836
- throw new Error('submitBatchEncrypted requires a configured wallet provider.');
837
- }
838
- const publicJwks = await this.wallet.getPublicJwks(walletContext);
839
- const signingJwk = publicJwks.find((jwk) => jwk.use === 'sig' || jwk.alg === 'ES384') ?? publicJwks[0];
840
- const url = /^https?:\/\//.test(path)
841
- ? path
842
- : `${this.baseUrl}${path.startsWith('/') ? path : `/${path}`}`;
843
- const result = await submitDidcomm({
844
- mode: 'strict',
845
- url,
846
- payload,
847
- defaultHeaders: this.defaultHeaders,
848
- bearerToken: this.bearerToken,
849
- recipientEncryptionJwk,
850
- signCompactJws: async (claims) => this.wallet.signCompactJws(walletContext, {
851
- header: {
852
- typ: 'application/didcomm-signed+json',
853
- alg: 'ES384',
854
- ...(signingJwk?.kid ? { kid: signingJwk.kid } : {}),
855
- },
856
- claims,
857
- }),
858
- encryptCompactJwe: async (compactJws, recipientJwk) => this.wallet.buildCompactJwe(walletContext, {
859
- plaintext: compactJws,
860
- recipientJwk: recipientJwk,
861
- contentType: 'JWS',
862
- }),
863
- fetcher: (requestUrl, init) => fetch(requestUrl, init),
864
- });
865
- return { status: result.status, location: result.location, body: result.body };
866
- }
867
- /**
868
- * POST a plain JSON payload.
869
- * Use for non-DIDComm routes (e.g. token exchange body, API key management).
870
- * Content-Type: `application/json`.
871
- */
872
- async postJson(path, payload) {
873
- const response = await this.doPost(path, payload, 'application/json');
874
- const body = await this.parseResponseBody(response);
875
- return {
876
- status: response.status,
877
- location: response.headers.get('location') ?? undefined,
878
- body,
879
- };
880
- }
881
- /**
882
- * Legacy JSON submit for non-bundle payloads (openid/token/resource JSON bodies).
883
- * Keeps JSON flows explicit and semantically separated from DIDComm bundle flows.
884
- */
885
- async submitLegacyJson(path, payload) {
886
- return this.postJson(path, payload);
887
- }
888
- /**
889
- * POST a multipart/form-data payload.
890
- * Use for file upload endpoints. Prefer `uploadConversionFile` for DataConversion uploads.
891
- */
892
- async postFormData(path, formData) {
893
- const url = `${this.baseUrl}${path.startsWith('/') ? path : `/${path}`}`;
894
- const headers = {
895
- ...this.defaultHeaders,
896
- Accept: 'application/json, application/didcomm-plaintext+json, application/x-www-form-urlencoded, */*',
897
- };
898
- if (this.bearerToken) {
899
- headers.Authorization = `Bearer ${this.bearerToken}`;
900
- }
901
- const response = await fetch(url, {
902
- method: 'POST',
903
- headers,
904
- body: formData,
905
- });
906
- const body = await this.parseResponseBody(response);
907
- return {
908
- status: response.status,
909
- location: response.headers.get('location') ?? undefined,
910
- body,
911
- };
912
- }
913
- /**
914
- * Upload a file to a DataConversion endpoint.
915
- * Wraps `postFormData` with sensible defaults for file field naming and multipart encoding.
916
- *
917
- * @param params.path - Use `conversionUploadPath(ctx, softwareId, sourceFormat)`.
918
- * @param params.fileName - File name including extension (e.g. `data.xlsx`).
919
- * @param params.fileContent - File bytes (Blob, Buffer, Uint8Array, or ArrayBuffer).
920
- * @param params.fileFieldName - Form field name for the file. Defaults to `'file'`.
921
- * @param params.fields - Additional form string fields to include.
922
- */
923
- async uploadConversionFile(params) {
924
- const form = new FormData();
925
- const fileFieldName = params.fileFieldName ?? 'file';
926
- const content = params.fileContent instanceof Blob
927
- ? params.fileContent
928
- : new Blob([params.fileContent]);
929
- form.append(fileFieldName, content, params.fileName);
930
- for (const [key, value] of Object.entries(params.fields ?? {})) {
931
- form.append(key, value);
932
- }
933
- return this.postFormData(params.path, form);
934
- }
935
- /**
936
- * Single poll attempt against a `_batch-response` or `_*-response` path.
937
- * Returns HTTP 202 while the job is still processing, 200 (or other) when done.
938
- * Prefer `pollUntilComplete` for automatic retry loops.
939
- */
940
- async pollBatchResponse(path, request) {
941
- const response = await this.doPost(path, request, 'application/json');
942
- return {
943
- status: response.status,
944
- body: await this.parseResponseBody(response),
945
- retryAfterMs: parseRetryAfterMs(response.headers.get('retry-after')),
946
- };
947
- }
948
- /**
949
- * Submit a DIDComm batch payload and poll until the async job completes.
950
- * Convenience wrapper around `submitBatch` + `pollUntilComplete`.
951
- *
952
- * Requires `payload.thid` to be set (used as the poll correlation key).
953
- * Use `createDidcommPlainMessage` from `builders.ts` to build the payload with a `thid`.
954
- *
955
- * @example
956
- * const result = await client.submitAndPoll(
957
- * client.individualFamilyOrganizationBatchPath(ctx),
958
- * client.individualFamilyOrganizationPollPath(ctx),
959
- * payload,
960
- * { timeoutMs: 30_000, intervalMs: 2_000 },
961
- * );
962
- * // result.poll.status === 200 on success
963
- */
964
- async submitAndPoll(submitPath, pollPath, payload, options) {
965
- const submit = await this.submitBatch(submitPath, payload);
966
- const thid = String(payload.thid || '').trim();
967
- if (!thid) {
968
- throw new Error('submitAndPoll requires payload.thid.');
969
- }
970
- const poll = await this.pollUntilComplete(pollPath, { thid }, options);
971
- return { submit, poll };
972
- }
973
- /**
974
- * Create scheduled phone reminder Task entries through canonical Task `_batch` routes.
975
- * This high-level helper accepts business parameters and internally builds flat
976
- * FHIR-style claims under `resource.meta.claims`.
977
- *
978
- * `description` is the Task title.
979
- * `reminderSummary` is the contextual summary of what the reminder refers to
980
- * (appointment, medication schedule, or another event), mapped to `based-on-display`.
981
- */
982
- async createPhoneReminderTasks(ctx, input, options) {
983
- const routeCtx = this.requireRouteContext(ctx);
984
- const windows = Array.isArray(input.windows) ? input.windows : [];
985
- if (!windows.length) {
986
- throw new Error('createPhoneReminderTasks requires at least one reminder window.');
987
- }
988
- if (!input.subjectRef || !input.ownerRef || !input.focusRef) {
989
- throw new Error('createPhoneReminderTasks requires subjectRef, ownerRef and focusRef.');
990
- }
991
- const thid = `task-reminder-${randomUUID()}`;
992
- const maxAttempts = Number.isFinite(input.maxAttempts) ? Math.max(1, Math.floor(Number(input.maxAttempts))) : 3;
993
- const description = String(input.description || 'Reminder phone call').trim() || 'Reminder phone call';
994
- const reminderSummary = String(input.reminderSummary || input.appointmentSummary || '').trim();
995
- const dataType = String(input.dataType || 'Task').trim() || 'Task';
996
- const data = windows.map((window) => {
997
- const offsetMinutes = Math.max(0, Math.floor(Number(window.offsetMinutes)));
998
- const remindAt = String(window.remindAt || '').trim();
999
- if (!remindAt) {
1000
- throw new Error('createPhoneReminderTasks requires remindAt in every window.');
1001
- }
1002
- const taskIdSeed = [
1003
- routeCtx.tenantId,
1004
- routeCtx.jurisdiction,
1005
- routeCtx.sector,
1006
- input.subjectRef,
1007
- input.ownerRef,
1008
- input.focusRef,
1009
- remindAt,
1010
- String(offsetMinutes),
1011
- ].join('|');
1012
- const taskId = `task-${createHash('sha256').update(taskIdSeed).digest('hex').slice(0, 24)}`;
1013
- const claims = {
1014
- '@context': 'org.hl7.fhir.api',
1015
- id: taskId,
1016
- status: 'scheduled',
1017
- subject: input.subjectRef,
1018
- owner: input.ownerRef,
1019
- focus: input.focusRef,
1020
- 'execution-period-start': remindAt,
1021
- channel: 'phone',
1022
- 'trigger-type': 'phone-call',
1023
- 'timing-repeat-offset': String(offsetMinutes),
1024
- 'max-attempts': String(maxAttempts),
1025
- };
1026
- if (input.locale)
1027
- claims.language = String(input.locale);
1028
- if (input.subjectDisplay)
1029
- claims['subject-display'] = String(input.subjectDisplay);
1030
- if (reminderSummary)
1031
- claims['based-on-display'] = reminderSummary;
1032
- if (input.notificationPhone)
1033
- claims['subject-phone'] = String(input.notificationPhone);
1034
- if (input.controllerPhone)
1035
- claims['owner-phone'] = String(input.controllerPhone);
1036
- if (input.callSid)
1037
- claims['communication-request'] = String(input.callSid);
1038
- return {
1039
- type: dataType,
1040
- request: { method: 'POST' },
1041
- resource: {
1042
- resourceType: 'Task',
1043
- id: taskId,
1044
- description,
1045
- meta: { claims },
1046
- },
1047
- };
1048
- });
1049
- const payload = createDidcommPlainMessage({
1050
- iss: routeCtx.tenantId,
1051
- aud: routeCtx.tenantId,
1052
- thid,
1053
- body: { data },
1054
- });
1055
- return this.submitAndPoll(this.individualTaskBatchPath(routeCtx), this.individualTaskPollPath(routeCtx), payload, options ?? { timeoutMs: 20_000, intervalMs: 1_000 });
1056
- }
1057
- /** Endpoint path for medication overlap pre-check (planned GW contract). */
1058
- individualMedicationOverlapCheckPath(ctx) {
1059
- return this.v1Path(ctx, 'individual', 'org.hl7.fhir.api', 'MedicationStatement', '_overlap-check');
1060
- }
1061
- /**
1062
- * Pre-create overlap check for medication intake schedules.
1063
- * TODO: Requires GW endpoint implementation (`MedicationStatement/_overlap-check`).
1064
- */
1065
- async checkMedicationScheduleOverlap(ctx, input) {
1066
- const payload = createDidcommPlainMessage({
1067
- iss: ctx.tenantId,
1068
- aud: ctx.tenantId,
1069
- thid: `med-overlap-${randomUUID()}`,
1070
- body: input,
1071
- });
1072
- return this.submitBatch(this.individualMedicationOverlapCheckPath(ctx), payload);
1073
- }
1074
- /**
1075
- * High-level helper for medication reminder creation.
1076
- * This creates one Task per explicit intake time and delegates reminder execution to GW daemon.
1077
- * TODO: recurring interval expansion + overlap policy should be finalized in GW endpoint contract.
1078
- */
1079
- async createMedicationReminderTasks(ctx, input, options) {
1080
- const claims = (input.claims || {});
1081
- const claimStart = String(claims[MedicationStatementClaimsFhirApiExtended.TimingBoundsPeriodStart] ||
1082
- claims['MedicationStatement.timing-bounds-period-start'] ||
1083
- claims[MedicationStatementClaimsFhirApi.Effective] ||
1084
- claims['MedicationStatement.effective'] ||
1085
- claims['DosageDetails.start'] ||
1086
- claims['MedicationDetails.start'] ||
1087
- '').trim();
1088
- const claimTimeOfDay = claims[MedicationStatementClaimsFhirApiExtended.TimingTimeOfDay] ??
1089
- claims['MedicationStatement.timing-timeofday'] ??
1090
- claims['Timing.repeat.timeOfDay'] ??
1091
- claims['Timing.repeat.time-of-day'];
1092
- const claimTimes = Array.isArray(claimTimeOfDay)
1093
- ? claimTimeOfDay
1094
- : typeof claimTimeOfDay === 'string'
1095
- ? claimTimeOfDay.split(',').map((v) => v.trim()).filter(Boolean)
1096
- : [];
1097
- const times = (Array.isArray(input.intakeTimes) ? input.intakeTimes : [])
1098
- .concat(claimTimes.map((hhmm) => ({ hhmm: String(hhmm) })));
1099
- if (!times.length) {
1100
- throw new Error('createMedicationReminderTasks requires at least one intake time.');
1101
- }
1102
- const startDate = String(input.startDate || claimStart || '').trim();
1103
- if (!startDate) {
1104
- throw new Error('createMedicationReminderTasks requires startDate.');
1105
- }
1106
- const windows = times.map((t) => {
1107
- const hhmm = String(t.hhmm || '').trim();
1108
- const remindAt = `${startDate}T${hhmm}:00.000Z`;
1109
- return { offsetMinutes: 0, remindAt };
1110
- });
1111
- const medicationDescription = String(input.medicationDescription ||
1112
- claims[MedicationStatementClaimsFhirApi.Medication] ||
1113
- claims['MedicationStatement.medication'] ||
1114
- claims['MedicationStatement.medication-display'] ||
1115
- 'Medication').trim();
1116
- const doseValue = String(input.doseValue ||
1117
- claims[MedicationStatementClaimsFhirApiExtended.DoseQuantityValue] ||
1118
- claims['MedicationStatement.dose-quantity-value'] ||
1119
- claims['Dosage.quantity-value'] ||
1120
- '').trim();
1121
- const doseUnitOrFormCode = String(input.doseUnitOrFormCode ||
1122
- claims[MedicationStatementClaimsFhirApiExtended.DoseQuantityUnit] ||
1123
- claims['MedicationStatement.dose-quantity-unit'] ||
1124
- claims[MedicationStatementClaimsFhirApiExtended.DoseType] ||
1125
- claims['MedicationStatement.dose-type'] ||
1126
- claims['Dosage.quantity-unit'] ||
1127
- claims['Dosage.form'] ||
1128
- '').trim();
1129
- const summary = `${medicationDescription} ${doseValue}${doseUnitOrFormCode ? ` ${doseUnitOrFormCode}` : ''}`.trim();
1130
- return this.createPhoneReminderTasks(ctx, {
1131
- windows,
1132
- locale: input.locale,
1133
- subjectRef: input.subjectRef,
1134
- ownerRef: input.ownerRef,
1135
- focusRef: `MedicationStatement/${createHash('sha256').update(summary + startDate).digest('hex').slice(0, 24)}`,
1136
- subjectDisplay: medicationDescription,
1137
- reminderSummary: summary,
1138
- notificationPhone: input.notificationPhone,
1139
- controllerPhone: input.controllerPhone,
1140
- description: 'Medication reminder',
1141
- maxAttempts: input.maxAttempts,
1142
- }, options);
1143
- }
1144
- /**
1145
- * Search for an existing family Organization registration by phone + usualname.
1146
- * Submits to `individual/org.schema/Organization/_search`, polls for the result, and
1147
- * parses the bundle entry into a `FamilyOrganizationSummary`.
1148
- *
1149
- * Returns `null` when no matching registration exists.
1150
- */
1151
- async searchFamilyOrganization(ctx, filters, options) {
1152
- const routeCtx = this.requireRouteContext(ctx);
1153
- const thid = `search-${randomUUID()}`;
1154
- const claims = {
1155
- 'org.schema.Organization.owner.telephone': filters.controllerPhone,
1156
- 'org.schema.Organization.alternateName': filters.usualname,
1157
- 'org.schema.Service.category': routeCtx.sector,
1158
- };
1159
- if (filters.birthDate) {
1160
- claims['org.schema.Organization.foundingDate'] = filters.birthDate;
1161
- }
1162
- const payload = {
1163
- jti: randomUUID(),
1164
- thid,
1165
- iss: routeCtx.tenantId,
1166
- aud: routeCtx.tenantId,
1167
- type: 'application/api+json',
1168
- body: {
1169
- data: [{
1170
- type: 'Family-search-v1.0',
1171
- meta: { claims }, // legacy compatibility
1172
- resource: { meta: { claims } },
1173
- }],
1174
- },
1175
- };
1176
- const result = await this.submitAndPoll(this.individualFamilyOrganizationSearchPath(routeCtx), this.individualFamilyOrganizationSearchPollPath(routeCtx), payload, options ?? { timeoutMs: 20_000, intervalMs: 1_000 });
1177
- if (result.poll.status !== 200)
1178
- return null;
1179
- const entry = result.poll.body?.body?.data?.[0];
1180
- if (!entry)
1181
- return null;
1182
- const status = entry.meta?.claims?.['org.schema.FamilyRegistration.status'];
1183
- if (!status || status === 'not_found')
1184
- return null;
1185
- const subjectInfo = {
1186
- identifierType: entry.meta?.claims?.['org.schema.Organization.identifier.additionalType'],
1187
- identifierValue: entry.meta?.claims?.['org.schema.Organization.identifier.value'],
1188
- nickname: entry.meta?.claims?.['org.schema.Organization.alternateName'],
1189
- birthDate: entry.meta?.claims?.['org.schema.Organization.foundingDate'],
1190
- telephone: entry.meta?.claims?.['org.schema.Organization.owner.telephone'],
1191
- };
1192
- return {
1193
- status,
1194
- offerId: entry.meta?.claims?.['org.schema.Offer.identifier'],
1195
- organizationId: entry.resource?.id,
1196
- subjectInfo,
1197
- };
1198
- }
1199
- /**
1200
- * Activate tenant organization in GW from ICA-derived proof.
1201
- */
1202
- async activateOrganizationInGatewayFromIcaProof(ctx, input, options) {
1203
- if (!input?.vpToken) {
1204
- throw new Error('activateOrganizationInGatewayFromIcaProof requires vpToken.');
1205
- }
1206
- const claims = {
1207
- '@context': 'org.schema',
1208
- vp_token: input.vpToken,
1209
- ...(input.additionalClaims || {}),
1210
- };
1211
- const requestedMembers = Number.isFinite(Number(input.numberOfMembers))
1212
- ? Math.max(1, Math.floor(Number(input.numberOfMembers)))
1213
- : 2;
1214
- // Keep gateway-facing claim stable while exposing a generic SDK input.
1215
- claims['org.schema.Organization.numberOfEmployees'] = requestedMembers;
1216
- if (input.organizationVc)
1217
- claims['org.schema.OrganizationCredential.jwt'] = input.organizationVc;
1218
- if (input.legalRepresentativeVc) {
1219
- claims['org.schema.LegalRepresentativeCredential.jwt'] = input.legalRepresentativeVc;
1220
- }
1221
- if (input.regulatoryEvidence)
1222
- claims['org.schema.Organization.regulatoryEvidence'] = input.regulatoryEvidence;
1223
- const payload = createDidcommPlainMessage({
1224
- iss: 'did:web:controller.example.com',
1225
- aud: 'did:web:host.example.com',
1226
- body: {
1227
- // GW activation parser expects proof material at top-level DIDComm body.
1228
- vp_token: input.vpToken,
1229
- ...(input.organizationVc ? { organizationCredential: input.organizationVc } : {}),
1230
- ...(input.legalRepresentativeVc ? { representativeCredential: input.legalRepresentativeVc } : {}),
1231
- data: [
1232
- {
1233
- type: 'Organization-activation-request-v1.0',
1234
- meta: { claims }, // legacy compatibility
1235
- resource: { meta: { claims } },
1236
- },
1237
- ],
1238
- },
1239
- });
1240
- return this.submitAndPoll(this.hostRegistryOrganizationActivatePath(ctx), this.hostRegistryOrganizationActivatePollPath(ctx), payload, options);
1241
- }
1242
- /**
1243
- * Friendly wrapper for legal organization activation.
1244
- * Accepts one object and seconds-based polling options for integrator ergonomics.
1245
- */
1246
- async activateOrganizationInGatewaySimple(input) {
1247
- const serviceProviderDidWeb = String(input.serviceProviderDidWeb || '').trim();
1248
- const serviceProviderUrl = String(input.serviceProviderUrl || '').trim();
1249
- const controllerEmail = String(input.controllerEmail || '').trim();
1250
- const controllerTelephone = String(input.controllerTelephone || '').trim();
1251
- const controllerRole = String(input.controllerRole || '').trim();
1252
- const resolvedServiceDid = toDidWebFromUrlOrHost(serviceProviderDidWeb || serviceProviderUrl);
1253
- if (!resolvedServiceDid) {
1254
- throw new Error('activateOrganizationInGatewaySimple requires serviceProviderDidWeb or serviceProviderUrl.');
1255
- }
1256
- if (!controllerEmail && !controllerTelephone) {
1257
- throw new Error('activateOrganizationInGatewaySimple requires controllerEmail or controllerTelephone.');
1258
- }
1259
- if (!controllerRole) {
1260
- throw new Error('activateOrganizationInGatewaySimple requires controllerRole.');
1261
- }
1262
- const pollOptions = this.resolveSimplePollOptions(input.timeoutSeconds, input.intervalSeconds);
1263
- const hostCtx = this.requireHostRouteContext(input.jurisdiction && input.sector
1264
- ? { jurisdiction: input.jurisdiction, sector: input.sector }
1265
- : undefined);
1266
- const implicitClaims = {
1267
- 'org.schema.Service.category': hostCtx.sector,
1268
- 'org.schema.Service.identifier': resolvedServiceDid,
1269
- ...(serviceProviderUrl ? { 'org.schema.Service.url': serviceProviderUrl } : {}),
1270
- [ClaimsPersonSchemaorg.hasOccupation]: controllerRole,
1271
- ...(controllerEmail ? { [ClaimsPersonSchemaorg.email]: controllerEmail } : {}),
1272
- ...(controllerTelephone ? { [ClaimsPersonSchemaorg.telephone]: controllerTelephone } : {}),
1273
- };
1274
- const activation = await this.activateOrganizationInGatewayFromIcaProof(hostCtx, {
1275
- vpToken: input.vpToken,
1276
- numberOfMembers: input.numberOfMembers,
1277
- organizationVc: input.organizationVc,
1278
- legalRepresentativeVc: input.legalRepresentativeVc,
1279
- regulatoryEvidence: input.regulatoryEvidence,
1280
- additionalClaims: { ...implicitClaims, ...(input.additionalClaims || {}) },
1281
- }, pollOptions);
1282
- this.assertFirstDidcommEntrySuccess(activation, 'activateOrganizationInGatewaySimple');
1283
- return activation;
1284
- }
1285
- /**
1286
- * Friendly wrapper for legal organization Order confirmation.
1287
- * Accepts one object and builds payload/paths internally.
1288
- */
1289
- async confirmLegalOrganizationOrderSimple(input) {
1290
- if (!String(input.offerId || '').trim()) {
1291
- throw new Error('confirmLegalOrganizationOrderSimple requires offerId.');
1292
- }
1293
- const pollOptions = this.resolveSimplePollOptions(input.timeoutSeconds, input.intervalSeconds);
1294
- const hostCtx = this.requireHostRouteContext(input.jurisdiction && input.sector
1295
- ? { jurisdiction: input.jurisdiction, sector: input.sector }
1296
- : undefined);
1297
- const claims = {
1298
- '@context': 'org.schema',
1299
- 'Order.acceptedOffer.identifier': input.offerId,
1300
- ...(input.additionalClaims || {}),
1301
- };
1302
- const payload = createDidcommPlainMessage({
1303
- iss: 'did:web:controller.example.com',
1304
- aud: 'did:web:host.example.com',
1305
- thid: `order-${randomUUID()}`,
1306
- body: {
1307
- data: [{
1308
- type: input.dataType || 'Organization-order-request-v1.0',
1309
- meta: { claims }, // legacy compatibility
1310
- resource: { meta: { claims } },
1311
- }],
1312
- },
1313
- });
1314
- const order = await this.submitAndPoll(this.hostRegistryOrderBatchPath(hostCtx), this.hostRegistryOrderPollPath(hostCtx), payload, pollOptions);
1315
- this.assertFirstDidcommEntrySuccess(order, 'confirmLegalOrganizationOrderSimple');
1316
- return order;
1317
- }
1318
- /**
1319
- * Normalize GW async response into DIDComm message body.
1320
- *
1321
- * Transport note:
1322
- * - GW poll responses are HTTP JSON envelopes
1323
- * - business payload lives inside DIDComm `body`
1324
- *
1325
- * This helper abstracts envelope differences so consumers do not depend on
1326
- * raw `poll.body.body` paths.
1327
- */
1328
- getDidcommMessageBodyFromResponse(result) {
1329
- const pollBody = result?.poll?.body ?? result?.body ?? result;
1330
- const didcommBody = pollBody?.body;
1331
- if (didcommBody && typeof didcommBody === 'object')
1332
- return didcommBody;
1333
- if (pollBody && typeof pollBody === 'object' && Array.isArray(pollBody?.data)) {
1334
- return pollBody;
1335
- }
1336
- return undefined;
1337
- }
1338
- /**
1339
- * Return first DIDComm business entry from a submit/poll result.
1340
- */
1341
- getFirstDidcommDataEntryFromResponse(result) {
1342
- const body = this.getDidcommMessageBodyFromResponse(result);
1343
- const entry = body?.data?.[0];
1344
- return entry && typeof entry === 'object' ? entry : undefined;
1345
- }
1346
- /**
1347
- * Extract `org.schema.Offer.identifier` from a submit/poll result.
1348
- *
1349
- * This helper normalizes canonical and legacy claim locations.
1350
- */
1351
- getOfferIdFromResponse(result) {
1352
- const entry = this.getFirstDidcommDataEntryFromResponse(result);
1353
- const offerId = String(entry?.meta?.claims?.[ClaimsOfferSchemaorg.identifier]
1354
- || entry?.resource?.meta?.claims?.[ClaimsOfferSchemaorg.identifier]
1355
- || '').trim();
1356
- return offerId || undefined;
1357
- }
1358
- /**
1359
- * Extract a UI-ready Offer preview from activation/registration responses.
1360
- */
1361
- getOfferPreviewFromResponse(result) {
1362
- const entry = this.getFirstDidcommDataEntryFromResponse(result);
1363
- const claims = entry?.meta?.claims || entry?.resource?.meta?.claims || {};
1364
- const seatsRaw = claims[ClaimsOfferSchemaorg.eligibleQuantityValue];
1365
- const seats = typeof seatsRaw === 'number'
1366
- ? seatsRaw
1367
- : (typeof seatsRaw === 'string' && seatsRaw.trim() ? Number(seatsRaw) : undefined);
1368
- return {
1369
- offerId: this.getOfferIdFromResponse(result),
1370
- amount: claims[ClaimsOfferSchemaorg.price],
1371
- currency: claims[ClaimsOfferSchemaorg.priceCurrency],
1372
- seats: Number.isFinite(seats) ? seats : undefined,
1373
- planName: claims[ClaimsOfferSchemaorg.itemOfferedName],
1374
- sku: claims[ClaimsOfferSchemaorg.itemOfferedSku],
1375
- paymentMethod: claims[ClaimsOfferSchemaorg.acceptedPaymentMethod],
1376
- checkoutUrl: claims[ClaimsOfferSchemaorg.checkoutPageURLTemplate],
1377
- };
1378
- }
1379
- /**
1380
- * Alias of `getOfferPreviewFromResponse` with business naming.
1381
- */
1382
- getOfferInfoFromResponse(result) {
1383
- return this.getOfferPreviewFromResponse(result);
1384
- }
1385
- /**
1386
- * Extract activation code from response payload or claims.
1387
- * Supports common response shapes used in onboarding and license issuance flows.
1388
- */
1389
- getActivationCodeFromResponse(result) {
1390
- const root = result?.poll?.body || result?.body || {};
1391
- const byBody = String(root?.activationCode || root?.body?.activationCode || '').trim();
1392
- if (byBody)
1393
- return byBody;
1394
- const entry = this.getFirstDidcommDataEntryFromResponse(result);
1395
- const claims = entry?.meta?.claims || entry?.resource?.meta?.claims || {};
1396
- const byClaims = String(claims['org.schema.IndividualProduct.serialNumber']
1397
- || claims['org.schema.Offer.serialNumber']
1398
- || claims['activationCode']
1399
- || '').trim();
1400
- return byClaims || undefined;
1401
- }
1402
- /**
1403
- * Throws when first DIDComm entry contains a business-level error status.
1404
- */
1405
- assertFirstDidcommEntrySuccess(result, contextLabel) {
1406
- const entry = this.getFirstDidcommDataEntryFromResponse(result);
1407
- const responseStatusRaw = entry?.response?.status;
1408
- const responseStatus = Number(responseStatusRaw);
1409
- if (!Number.isFinite(responseStatus) || responseStatus < 400)
1410
- return;
1411
- const diagnostics = String(entry?.response?.outcome?.issue?.[0]?.diagnostics
1412
- || entry?.response?.outcome?.issue?.[0]?.details?.text
1413
- || '').trim();
1414
- throw new Error(`${contextLabel} failed (business status=${responseStatus})${diagnostics ? `: ${diagnostics}` : ''}`);
1415
- }
1416
- /**
1417
- * Activate employee/member device by activation code exchange + DCR registration.
1418
- *
1419
- * Step 1. Exchange activation code using user id_token to obtain an initial access token.
1420
- * Step 2. Register device keys through Device/_dcr authorized by that initial token.
1421
- */
1422
- async activateEmployeeDeviceWithActivationCode(ctx, input) {
1423
- const exchangePayload = {
1424
- thid: `exchange-${randomUUID()}`,
1425
- subject_token: input.activationCode,
1426
- };
1427
- const exchangeClient = new DataspaceNodeClient({
1428
- baseUrl: this.baseUrl,
1429
- bearerToken: input.idToken,
1430
- defaultHeaders: this.defaultHeaders,
1431
- wallet: this.wallet,
1432
- });
1433
- const exchange = await exchangeClient.submitAndPoll(this.identityTokenExchangePath(ctx), this.identityTokenExchangePollPath(ctx), exchangePayload, input.pollOptions);
1434
- const exchangeBody = exchange.poll.body?.body || exchange.poll.body || {};
1435
- const initialAccessToken = String(exchangeBody.initial_access_token || exchangeBody.access_token || '').trim();
1436
- if (!initialAccessToken) {
1437
- throw new Error('activateEmployeeDeviceWithActivationCode: missing initial_access_token in exchange response.');
1438
- }
1439
- const dcrPayload = {
1440
- thid: `dcr-${randomUUID()}`,
1441
- ...input.dcrPayload,
1442
- };
1443
- const dcrClient = new DataspaceNodeClient({
1444
- baseUrl: this.baseUrl,
1445
- bearerToken: initialAccessToken,
1446
- defaultHeaders: this.defaultHeaders,
1447
- wallet: this.wallet,
1448
- });
1449
- const dcr = await dcrClient.submitAndPoll(this.identityDeviceDcrPath(ctx), this.identityDeviceDcrPollPath(ctx), dcrPayload, input.pollOptions);
1450
- return {
1451
- initialAccessToken,
1452
- exchange,
1453
- dcr,
1454
- };
1455
- }
1456
- /**
1457
- * Friendly wrapper for employee/member device activation.
1458
- * Uses one object, seconds-based polling, and constructor ctx fallback.
1459
- */
1460
- async activateEmployeeDeviceWithActivationCodeSimple(input) {
1461
- const routeCtx = this.requireRouteContext(input.tenantId && input.jurisdiction && input.sector
1462
- ? { tenantId: input.tenantId, jurisdiction: input.jurisdiction, sector: input.sector }
1463
- : undefined);
1464
- const pollOptions = this.resolveSimplePollOptions(input.timeoutSeconds, input.intervalSeconds);
1465
- return this.activateEmployeeDeviceWithActivationCode(routeCtx, {
1466
- activationCode: input.activationCode,
1467
- idToken: input.idToken,
1468
- dcrPayload: input.dcrPayload,
1469
- pollOptions,
1470
- });
1471
- }
1472
- /**
1473
- * UC 5.3 wrapper: create organization employee in entity Employee batch route.
1474
- */
1475
- async createOrganizationEmployee(ctx, input, options) {
1476
- const routeCtx = this.requireRouteContext(ctx);
1477
- const payload = createDidcommPlainMessage({
1478
- iss: routeCtx.tenantId,
1479
- aud: routeCtx.tenantId,
1480
- thid: `employee-${randomUUID()}`,
1481
- body: {
1482
- data: [
1483
- {
1484
- type: input.dataType || 'Employee-create-request-v1.0',
1485
- meta: { claims: input.employeeClaims || {} }, // legacy compatibility
1486
- resource: { meta: { claims: input.employeeClaims || {} } },
1487
- },
1488
- ],
1489
- },
1490
- });
1491
- return this.submitAndPoll(this.employeeBatchPath(routeCtx), this.employeePollPath(routeCtx), payload, options);
1492
- }
1493
- /**
1494
- * UC 5.1 wrapper: bootstrap subject organization context via registration + optional order confirmation.
1495
- */
1496
- async bootstrapSubjectOrganizationIndex(ctx, input) {
1497
- const registrationPayload = {
1498
- thid: input.registrationPayload.thid || `family-org-${randomUUID()}`,
1499
- ...input.registrationPayload,
1500
- };
1501
- const registration = await this.submitAndPoll(this.individualFamilyOrganizationBatchPath(ctx), this.individualFamilyOrganizationPollPath(ctx), registrationPayload, input.pollOptions);
1502
- if (!input.confirmationPayload) {
1503
- return { registration };
1504
- }
1505
- const confirmationPayload = {
1506
- thid: input.confirmationPayload.thid || `family-order-${randomUUID()}`,
1507
- ...input.confirmationPayload,
1508
- };
1509
- const confirmation = await this.submitAndPoll(this.individualFamilyOrderBatchPath(ctx), this.individualFamilyOrderPollPath(ctx), confirmationPayload, input.pollOptions);
1510
- return { registration, confirmation };
1511
- }
1512
- /**
1513
- * Friendly wrapper (recommended step 1): register individual organization and return Offer.
1514
- */
1515
- async startIndividualOrganizationSimple(input) {
1516
- const routeCtx = this.requireRouteContext(input.tenantId && input.jurisdiction && input.sector
1517
- ? { tenantId: input.tenantId, jurisdiction: input.jurisdiction, sector: input.sector }
1518
- : undefined);
1519
- const alternateName = String(input.alternateName || '').trim();
1520
- if (!alternateName) {
1521
- throw new Error('bootstrapIndividualOrganizationSimple requires alternateName.');
1522
- }
1523
- const controllerEmail = String(input.controllerEmail || '').trim();
1524
- const controllerTelephone = String(input.controllerTelephone || '').trim();
1525
- if (!controllerEmail && !controllerTelephone) {
1526
- throw new Error('bootstrapIndividualOrganizationSimple requires controllerEmail or controllerTelephone.');
1527
- }
1528
- const controllerRole = String(input.controllerRole || 'org.hl7.v3.RoleCode|RESPRSN').trim();
1529
- const claims = {
1530
- '@context': 'org.schema',
1531
- 'org.schema.Organization.alternateName': alternateName,
1532
- 'org.schema.Service.category': routeCtx.sector,
1533
- [ClaimsPersonSchemaorg.hasOccupation]: controllerRole,
1534
- ...(controllerEmail ? { [ClaimsPersonSchemaorg.email]: controllerEmail } : {}),
1535
- ...(controllerTelephone ? { [ClaimsPersonSchemaorg.telephone]: controllerTelephone } : {}),
1536
- ...(input.additionalClaims || {}),
1537
- };
1538
- const pollOptions = this.resolveSimplePollOptions(input.timeoutSeconds, input.intervalSeconds);
1539
- const registrationPayload = createDidcommPlainMessage({
1540
- iss: routeCtx.tenantId,
1541
- aud: routeCtx.tenantId,
1542
- thid: `family-org-${randomUUID()}`,
1543
- body: {
1544
- data: [{
1545
- type: 'SubjectOrg-registration-form-v1.0',
1546
- meta: { claims },
1547
- resource: { meta: { claims } },
1548
- }],
1549
- },
1550
- });
1551
- const registration = await this.submitAndPoll(this.individualFamilyOrganizationBatchPath(routeCtx), this.individualFamilyOrganizationPollPath(routeCtx), registrationPayload, pollOptions);
1552
- this.assertFirstDidcommEntrySuccess(registration, 'startIndividualOrganizationSimple.registration');
1553
- const offerId = this.getOfferIdFromResponse(registration);
1554
- if (!offerId) {
1555
- throw new Error('startIndividualOrganizationSimple failed: missing offerId in registration response.');
1556
- }
1557
- return { registration, offerId, offerPreview: this.getOfferPreviewFromResponse(registration) };
1558
- }
1559
- /**
1560
- * Friendly wrapper (recommended step 2): confirm individual/family order from accepted offerId.
1561
- */
1562
- async confirmIndividualOrganizationOrderSimple(input) {
1563
- const routeCtx = this.requireRouteContext(input.tenantId && input.jurisdiction && input.sector
1564
- ? { tenantId: input.tenantId, jurisdiction: input.jurisdiction, sector: input.sector }
1565
- : undefined);
1566
- const offerId = String(input.offerId || '').trim();
1567
- if (!offerId) {
1568
- throw new Error('confirmIndividualOrganizationOrderSimple requires offerId.');
1569
- }
1570
- const pollOptions = this.resolveSimplePollOptions(input.timeoutSeconds, input.intervalSeconds);
1571
- const orderClaims = {
1572
- '@context': 'org.schema',
1573
- 'Order.acceptedOffer.identifier': offerId,
1574
- };
1575
- const confirmationPayload = createDidcommPlainMessage({
1576
- iss: routeCtx.tenantId,
1577
- aud: routeCtx.tenantId,
1578
- thid: `family-order-${randomUUID()}`,
1579
- body: {
1580
- data: [{
1581
- type: 'Family-order-request-v1.0',
1582
- meta: { claims: orderClaims },
1583
- resource: { meta: { claims: orderClaims } },
1584
- }],
1585
- },
1586
- });
1587
- const confirmation = await this.submitAndPoll(this.individualFamilyOrderBatchPath(routeCtx), this.individualFamilyOrderPollPath(routeCtx), confirmationPayload, pollOptions);
1588
- this.assertFirstDidcommEntrySuccess(confirmation, 'confirmIndividualOrganizationOrderSimple');
1589
- return confirmation;
1590
- }
1591
- /**
1592
- * Friendly wrapper (provisional): register + auto-confirm individual order.
1593
- * Prefer `startIndividualOrganizationSimple` + `confirmIndividualOrganizationOrderSimple`.
1594
- */
1595
- async bootstrapIndividualOrganizationSimple(input) {
1596
- const started = await this.startIndividualOrganizationSimple(input);
1597
- const confirmation = await this.confirmIndividualOrganizationOrderSimple({
1598
- tenantId: input.tenantId,
1599
- jurisdiction: input.jurisdiction,
1600
- sector: input.sector,
1601
- offerId: started.offerId,
1602
- timeoutSeconds: input.timeoutSeconds,
1603
- intervalSeconds: input.intervalSeconds,
1604
- });
1605
- return {
1606
- registration: started.registration,
1607
- offerId: started.offerId,
1608
- confirmation,
1609
- };
1610
- }
1611
- /**
1612
- * UC 5.5 wrapper: import IPS/FHIR composition and update subject index context.
1613
- */
1614
- async importIpsOrFhirAndUpdateIndex(ctx, input) {
1615
- const routeCtx = this.requireRouteContext(ctx);
1616
- const payload = {
1617
- thid: input.compositionPayload.thid || `composition-${randomUUID()}`,
1618
- ...input.compositionPayload,
1619
- };
1620
- const submitPath = (input.format || 'r4') === 'api'
1621
- ? this.individualCompositionR4BatchPath(routeCtx).replace('/org.hl7.fhir.r4/', '/org.hl7.fhir.api/')
1622
- : this.individualCompositionR4BatchPath(routeCtx);
1623
- const pollPath = (input.format || 'r4') === 'api'
1624
- ? this.individualCompositionR4PollPath(routeCtx).replace('/org.hl7.fhir.r4/', '/org.hl7.fhir.api/')
1625
- : this.individualCompositionR4PollPath(routeCtx);
1626
- return this.submitAndPoll(submitPath, pollPath, payload, input.pollOptions);
1627
- }
1628
- /**
1629
- * UC 5.6 consent helper from minimal frontend fields.
1630
- * Builds canonical Consent claims and submits/polls the Consent batch.
1631
- */
1632
- async grantProfessionalAccessSimple(ctx, input) {
1633
- const routeCtx = this.requireRouteContext(ctx);
1634
- const built = buildConsentClaimsSimpleWithCid({
1635
- subjectDid: input.subjectDid,
1636
- subjectPhone: input.subjectPhone,
1637
- subjectGivenName: input.subjectGivenName,
1638
- actor: input.actor || {},
1639
- actorRole: input.actorRole,
1640
- purpose: input.purpose,
1641
- actions: input.actions,
1642
- consentIdentifier: input.consentIdentifier,
1643
- consentDate: input.consentDate,
1644
- decision: input.decision,
1645
- attachmentContentType: input.attachmentContentType,
1646
- attachmentBase64: input.attachmentBase64,
1647
- }, {
1648
- consentIdentifierFactory: () => `urn:uuid:${randomUUID()}`,
1649
- });
1650
- const thid = `consent-${randomUUID()}`;
1651
- const consentPayload = {
1652
- thid,
1653
- body: {
1654
- data: [
1655
- {
1656
- type: input.dataType || 'Consent-grant-request-v1.0',
1657
- meta: { claims: built.consentClaims }, // legacy compatibility
1658
- resource: { meta: { claims: built.consentClaims } },
1659
- },
1660
- ],
1661
- },
1662
- };
1663
- const consent = await this.submitAndPoll(this.individualConsentR4BatchPath(routeCtx), this.individualConsentR4PollPath(routeCtx), consentPayload, input.pollOptions);
1664
- return {
1665
- thid,
1666
- consent,
1667
- actorIdentifier: built.actorIdentifier,
1668
- subjectIdentifier: built.subjectIdentifier,
1669
- consentClaims: built.consentClaims,
1670
- claimsCid: built.claimsCid,
1671
- };
1672
- }
1673
- /**
1674
- * UC 5.7 wrapper: generate digital twin composition from subject data.
1675
- */
1676
- async generateDigitalTwinFromSubjectData(ctx, input) {
1677
- const routeCtx = this.requireRouteContext(ctx);
1678
- const payload = {
1679
- thid: input.compositionPayload.thid || `digital-twin-${randomUUID()}`,
1680
- ...input.compositionPayload,
1681
- };
1682
- const submitPath = (input.format || 'r4') === 'api'
1683
- ? this.digitalTwinCompositionApiBatchPath(routeCtx)
1684
- : this.digitalTwinCompositionR4BatchPath(routeCtx);
1685
- const pollPath = (input.format || 'r4') === 'api'
1686
- ? this.digitalTwinCompositionApiPollPath(routeCtx)
1687
- : this.digitalTwinCompositionR4PollPath(routeCtx);
1688
- return this.submitAndPoll(submitPath, pollPath, payload, input.pollOptions);
1689
- }
1690
- /**
1691
- * Poll a `_*-response` path repeatedly until the status is no longer 202.
1692
- * Default: 60s timeout, 2s interval.
1693
- * Throws if timeout is exceeded.
1694
- *
1695
- * @param path - Poll path (e.g. `individualFamilyOrganizationPollPath(ctx)`).
1696
- * @param request - Must include `thid` matching the original submit payload.
1697
- * @param options - `timeoutMs` (default 60000) and `intervalMs` (default 2000).
1698
- */
1699
- async pollUntilComplete(path, request, options) {
1700
- const timeoutMs = options?.timeoutMs ?? 60_000;
1701
- const intervalMs = options?.intervalMs ?? 2_000;
1702
- const startedAt = Date.now();
1703
- let attempts = 0;
1704
- while (true) {
1705
- attempts += 1;
1706
- const result = await this.pollBatchResponse(path, request);
1707
- if (result.status !== 202) {
1708
- return {
1709
- status: result.status,
1710
- body: result.body,
1711
- attempts,
1712
- };
1713
- }
1714
- if (Date.now() - startedAt > timeoutMs) {
1715
- throw new Error(`Polling timeout after ${attempts} attempts (${timeoutMs}ms).`);
1716
- }
1717
- const waitMs = options?.intervalMs ?? result.retryAfterMs ?? intervalMs;
1718
- await new Promise((resolve) => setTimeout(resolve, waitMs));
1719
- }
1720
- }
1721
- // ---- Internal HTTP helpers ---------------------------------------------
1722
- async doPost(path, payload, contentType) {
1723
- const url = /^https?:\/\//.test(path)
1724
- ? path
1725
- : `${this.baseUrl}${path.startsWith('/') ? path : `/${path}`}`;
1726
- const headers = {
1727
- ...this.defaultHeaders,
1728
- 'Content-Type': contentType,
1729
- Accept: 'application/json, application/didcomm-plaintext+json, application/x-www-form-urlencoded, */*',
1730
- };
1731
- if (this.bearerToken) {
1732
- headers.Authorization = `Bearer ${this.bearerToken}`;
1733
- }
1734
- return fetch(url, {
1735
- method: 'POST',
1736
- headers,
1737
- body: JSON.stringify(payload),
1738
- });
1739
- }
1740
- async parseResponseBody(response) {
1741
- const contentType = response.headers.get('content-type') || '';
1742
- const raw = await response.text();
1743
- if (!raw)
1744
- return {};
1745
- if (contentType.includes('application/json') || contentType.includes('application/didcomm-plaintext+json')) {
1746
- try {
1747
- return JSON.parse(raw);
1748
- }
1749
- catch {
1750
- return {};
1751
- }
1752
- }
1753
- return raw;
1754
- }
1755
- }