dataspace-client-sdk-node 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +31 -0
- package/README.md +44 -1
- package/dist/client.d.ts +153 -33
- package/dist/client.js +619 -93
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/types.d.ts +112 -1
- package/dist/vp-token.d.ts +37 -0
- package/dist/vp-token.js +56 -0
- package/docs/API.md +19 -4
- package/docs/BACKEND_NODE_INTEGRATION.md +249 -0
- package/docs/CONTROLLER_FLOW_STEP_BY_STEP.md +283 -0
- package/docs/DATA_MODEL_ALIGNMENT.md +37 -13
- package/docs/DEVELOPER_USE_CASES.md +10 -2
- package/docs/E2E_LOCAL_GW_UC5.md +49 -0
- package/docs/ENDPOINT_ID_CATALOG.md +90 -0
- package/docs/LEGAL_ORGANIZATION_FLOW_STEP_BY_STEP.md +84 -0
- package/docs/PERSONAL_FLOW_STEP_BY_STEP.md +70 -0
- package/docs/PORTAL_BACKEND_INTEGRATION_HANDOVER.md +343 -0
- package/docs/PRACTITIONER_FLOW_STEP_BY_STEP.md +182 -0
- package/docs/REACT_WEB_INTEGRATION.md +72 -0
- package/examples/e2e-bootstrap-tenant.mjs +3 -2
- package/examples/e2e-individual-flow.mjs +13 -8
- package/examples/host-activate-and-employee.mjs +3 -2
- package/examples/smoke-legal-org-local.mjs +40 -0
- package/package.json +4 -3
- package/src/client.ts +784 -132
- package/src/index.ts +1 -0
- package/src/types.ts +123 -1
- package/src/vp-token.ts +91 -0
- package/tests/client.test.mjs +491 -0
- package/tests/fixtures/ica-vp-minimal.json +67 -0
- package/tests/helpers/vp-token-fixture.mjs +23 -0
- package/tests/live-gw-uc5.e2e.test.mjs +108 -0
- package/SDK_PARITY_MAP.md +0 -120
- package/TODO_PROMPT_NEXT_STEPS.md +0 -185
- package/artifacts/update-smart-wallet.js +0 -1016
package/dist/client.js
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 } from 'gdc-common-utils-ts/utils/didcomm-submit';
|
|
6
|
+
import { ClaimsOfferSchemaorg, ClaimsPersonSchemaorg } from 'gdc-common-utils-ts/constants/schemaorg';
|
|
4
7
|
import { MedicationStatementClaimsFhirApi, MedicationStatementClaimsFhirApiExtended, } from 'gdc-common-utils-ts/models/interoperable-claims/medication-statement-claims';
|
|
5
8
|
function trimTrailingSlash(value) {
|
|
6
9
|
return value.replace(/\/+$/, '');
|
|
@@ -8,21 +11,144 @@ function trimTrailingSlash(value) {
|
|
|
8
11
|
function encode(value) {
|
|
9
12
|
return encodeURIComponent(value);
|
|
10
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
|
+
}
|
|
11
46
|
export class DataspaceNodeClient {
|
|
12
47
|
baseUrl;
|
|
13
48
|
bearerToken;
|
|
14
49
|
defaultHeaders;
|
|
15
50
|
wallet;
|
|
51
|
+
defaultCtx;
|
|
52
|
+
defaultTimeoutMs;
|
|
53
|
+
defaultIntervalMs;
|
|
16
54
|
_tokenCache = new Map();
|
|
17
55
|
constructor(options) {
|
|
18
56
|
this.baseUrl = trimTrailingSlash(options.baseUrl);
|
|
19
57
|
this.bearerToken = options.bearerToken;
|
|
20
58
|
this.defaultHeaders = options.defaultHeaders ?? {};
|
|
21
59
|
this.wallet = options.wallet;
|
|
60
|
+
this.defaultCtx = options.ctx;
|
|
22
61
|
}
|
|
23
62
|
getWallet() {
|
|
24
63
|
return this.wallet;
|
|
25
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
|
+
}
|
|
26
152
|
// ---- Path helpers -------------------------------------------------------
|
|
27
153
|
/**
|
|
28
154
|
* Generic GW v1 tenant route builder.
|
|
@@ -36,7 +162,8 @@ export class DataspaceNodeClient {
|
|
|
36
162
|
* // → /acme/cds-ES/v1/health-care/individual/org.schema/Organization/_batch
|
|
37
163
|
*/
|
|
38
164
|
v1Path(ctx, section, format, resourceType, action) {
|
|
39
|
-
|
|
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)}`;
|
|
40
167
|
}
|
|
41
168
|
/**
|
|
42
169
|
* Generic tenant-scoped identity route builder.
|
|
@@ -46,7 +173,8 @@ export class DataspaceNodeClient {
|
|
|
46
173
|
* Dedicated path methods in this SDK use `host` (GW convention).
|
|
47
174
|
*/
|
|
48
175
|
tenantIdentityPath(ctx, prefix, action) {
|
|
49
|
-
|
|
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)}`;
|
|
50
178
|
}
|
|
51
179
|
/**
|
|
52
180
|
* Generic host registry route builder (tenant-agnostic, `host/` prefix).
|
|
@@ -55,7 +183,8 @@ export class DataspaceNodeClient {
|
|
|
55
183
|
* Pattern: `/host/cds-{jurisdiction}/v1/{sector}/registry/org.schema/{resourceType}/{action}`
|
|
56
184
|
*/
|
|
57
185
|
hostRegistryPath(ctx, resourceType, action) {
|
|
58
|
-
|
|
186
|
+
const hostCtx = this.requireHostRouteContext(ctx);
|
|
187
|
+
return `/host/cds-${encode(hostCtx.jurisdiction)}/v1/${encode(hostCtx.sector)}/registry/org.schema/${encode(resourceType)}/${encode(action)}`;
|
|
59
188
|
}
|
|
60
189
|
/** Submit path: host registry Organization batch (controller-level org registration). */
|
|
61
190
|
hostRegistryOrganizationBatchPath(ctx) {
|
|
@@ -86,11 +215,13 @@ export class DataspaceNodeClient {
|
|
|
86
215
|
* Use for `family-registration/_create-or-resume` DIDComm payloads.
|
|
87
216
|
*/
|
|
88
217
|
individualFamilyOrganizationBatchPath(ctx) {
|
|
89
|
-
|
|
218
|
+
const routeCtx = this.requireRouteContext(ctx);
|
|
219
|
+
return `/${encode(routeCtx.tenantId)}/cds-${encode(routeCtx.jurisdiction)}/v1/${encode(routeCtx.sector)}/individual/org.schema/Organization/_batch`;
|
|
90
220
|
}
|
|
91
221
|
/** Poll path: individual/family Organization. Pair with `individualFamilyOrganizationBatchPath`. */
|
|
92
222
|
individualFamilyOrganizationPollPath(ctx) {
|
|
93
|
-
|
|
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`;
|
|
94
225
|
}
|
|
95
226
|
/** Submit path: individual/family Organization search (`org.schema/Organization/_search`). */
|
|
96
227
|
individualFamilyOrganizationSearchPath(ctx) {
|
|
@@ -102,11 +233,13 @@ export class DataspaceNodeClient {
|
|
|
102
233
|
}
|
|
103
234
|
/** Submit path: individual/family Order batch (`org.schema/Order/_batch`). */
|
|
104
235
|
individualFamilyOrderBatchPath(ctx) {
|
|
105
|
-
|
|
236
|
+
const routeCtx = this.requireRouteContext(ctx);
|
|
237
|
+
return `/${encode(routeCtx.tenantId)}/cds-${encode(routeCtx.jurisdiction)}/v1/${encode(routeCtx.sector)}/individual/org.schema/Order/_batch`;
|
|
106
238
|
}
|
|
107
239
|
/** Poll path: individual/family Order. Pair with `individualFamilyOrderBatchPath`. */
|
|
108
240
|
individualFamilyOrderPollPath(ctx) {
|
|
109
|
-
|
|
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`;
|
|
110
243
|
}
|
|
111
244
|
/** Submit path: individual RelatedPerson (FHIR R4 API, `org.hl7.fhir.api/RelatedPerson/_batch`). */
|
|
112
245
|
individualRelatedPersonBatchPath(ctx) {
|
|
@@ -269,10 +402,11 @@ export class DataspaceNodeClient {
|
|
|
269
402
|
* Results are cached in memory; re-runs automatically on expiry.
|
|
270
403
|
*/
|
|
271
404
|
async authenticateBackendPkceAndExchange(options) {
|
|
272
|
-
const { ctx, apiKey, scopes,
|
|
273
|
-
const
|
|
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);
|
|
274
408
|
if (cached && cached.expiresAt > Date.now() + 30_000) {
|
|
275
|
-
return { status: 'cached', endpointId, accessToken: cached.accessToken, tokenType: cached.tokenType, scopes: cached.scopes };
|
|
409
|
+
return { status: 'cached', tokenCacheKey: cacheKey, endpointId: cacheKey, accessToken: cached.accessToken, tokenType: cached.tokenType, scopes: cached.scopes };
|
|
276
410
|
}
|
|
277
411
|
const controllerPublicJwk = await this.resolveControllerPublicJwk(options);
|
|
278
412
|
// Step 1: DCR – bind API key to service public key
|
|
@@ -285,7 +419,7 @@ export class DataspaceNodeClient {
|
|
|
285
419
|
await this.submitBatch(this.identityDeviceDcrPath(ctx), dcrPayload);
|
|
286
420
|
const dcrPoll = await this.pollUntilComplete(this.identityDeviceDcrPollPath(ctx), { thid: String(dcrPayload['thid']) }, pollOptions);
|
|
287
421
|
if (dcrPoll.status !== 200) {
|
|
288
|
-
return { status: 'failed', step: '_dcr', endpointId, accessToken: '', tokenType: 'Bearer', scopes };
|
|
422
|
+
return { status: 'failed', step: '_dcr', tokenCacheKey: cacheKey, endpointId: cacheKey, accessToken: '', tokenType: 'Bearer', scopes };
|
|
289
423
|
}
|
|
290
424
|
// Step 2: Code – PKCE S256 challenge
|
|
291
425
|
const codeChallenge = this._pkceS256Challenge(codeVerifier);
|
|
@@ -301,7 +435,7 @@ export class DataspaceNodeClient {
|
|
|
301
435
|
const codeBody = codePoll.body ?? {};
|
|
302
436
|
const code = String(codeBody['code'] ?? '').trim();
|
|
303
437
|
if (codePoll.status !== 200 || !code) {
|
|
304
|
-
return { status: 'failed', step: '_code', endpointId, accessToken: '', tokenType: 'Bearer', scopes };
|
|
438
|
+
return { status: 'failed', step: '_code', tokenCacheKey: cacheKey, endpointId: cacheKey, accessToken: '', tokenType: 'Bearer', scopes };
|
|
305
439
|
}
|
|
306
440
|
// Step 3: Token – exchange code + verifier for id_token
|
|
307
441
|
const tokenPayload = this._buildAuthDIDCommRequest({
|
|
@@ -316,7 +450,7 @@ export class DataspaceNodeClient {
|
|
|
316
450
|
const tokenBody = tokenPoll.body ?? {};
|
|
317
451
|
const idToken = String(tokenBody['id_token'] ?? '').trim();
|
|
318
452
|
if (tokenPoll.status !== 200 || !idToken) {
|
|
319
|
-
return { status: 'failed', step: '_token', endpointId, accessToken: '', tokenType: 'Bearer', scopes };
|
|
453
|
+
return { status: 'failed', step: '_token', tokenCacheKey: cacheKey, endpointId: cacheKey, accessToken: '', tokenType: 'Bearer', scopes };
|
|
320
454
|
}
|
|
321
455
|
// Step 4: Exchange – id_token → SMART bearer
|
|
322
456
|
const exchangeThid = `exchange-${randomUUID()}`;
|
|
@@ -334,26 +468,26 @@ export class DataspaceNodeClient {
|
|
|
334
468
|
const exchangeBody = exchangePoll.body ?? {};
|
|
335
469
|
const accessToken = String(exchangeBody['access_token'] ?? '').trim();
|
|
336
470
|
if (exchangePoll.status !== 200 || !accessToken) {
|
|
337
|
-
return { status: 'failed', step: '_exchange', endpointId, accessToken: '', tokenType: 'Bearer', scopes };
|
|
471
|
+
return { status: 'failed', step: '_exchange', tokenCacheKey: cacheKey, endpointId: cacheKey, accessToken: '', tokenType: 'Bearer', scopes };
|
|
338
472
|
}
|
|
339
473
|
const tokenType = String(exchangeBody['token_type'] ?? 'Bearer');
|
|
340
474
|
const grantedScope = String(exchangeBody['scope'] ?? '').trim();
|
|
341
475
|
const grantedScopes = grantedScope ? grantedScope.split(' ').filter(Boolean) : scopes;
|
|
342
476
|
const expiresIn = Number(exchangeBody['expires_in'] ?? 0);
|
|
343
|
-
this._tokenCache.set(
|
|
477
|
+
this._tokenCache.set(cacheKey, {
|
|
344
478
|
accessToken,
|
|
345
479
|
tokenType,
|
|
346
480
|
scopes: grantedScopes,
|
|
347
481
|
expiresAt: Date.now() + expiresIn * 1000,
|
|
348
482
|
});
|
|
349
|
-
return { status: 'fetched', endpointId, accessToken, tokenType, scopes: grantedScopes };
|
|
483
|
+
return { status: 'fetched', tokenCacheKey: cacheKey, endpointId: cacheKey, accessToken, tokenType, scopes: grantedScopes };
|
|
350
484
|
}
|
|
351
485
|
/**
|
|
352
486
|
* Returns the cached SMART bearer for the given endpointId if still valid (>30s remaining).
|
|
353
487
|
* Returns `undefined` if not cached or expired.
|
|
354
488
|
*/
|
|
355
|
-
getCachedBearerToken(
|
|
356
|
-
const cached = this._tokenCache.get(
|
|
489
|
+
getCachedBearerToken(tokenCacheKey) {
|
|
490
|
+
const cached = this._tokenCache.get(tokenCacheKey);
|
|
357
491
|
if (cached && cached.expiresAt > Date.now() + 30_000) {
|
|
358
492
|
return cached.accessToken;
|
|
359
493
|
}
|
|
@@ -363,13 +497,15 @@ export class DataspaceNodeClient {
|
|
|
363
497
|
* smart-backend.v1: obtain an OAuth2 backend token using client_credentials + private_key_jwt.
|
|
364
498
|
*/
|
|
365
499
|
async authenticateBackendSmartStandard(options) {
|
|
366
|
-
const { clientId, scopes,
|
|
367
|
-
const
|
|
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);
|
|
368
503
|
if (cached && cached.expiresAt > Date.now() + 30_000) {
|
|
369
504
|
return {
|
|
370
505
|
status: 'cached',
|
|
371
506
|
profile: 'smart-backend.v1',
|
|
372
|
-
|
|
507
|
+
tokenCacheKey: cacheKey,
|
|
508
|
+
endpointId: cacheKey,
|
|
373
509
|
accessToken: cached.accessToken,
|
|
374
510
|
tokenType: cached.tokenType,
|
|
375
511
|
scopes: cached.scopes,
|
|
@@ -400,7 +536,8 @@ export class DataspaceNodeClient {
|
|
|
400
536
|
return {
|
|
401
537
|
status: 'failed',
|
|
402
538
|
profile: 'smart-backend.v1',
|
|
403
|
-
|
|
539
|
+
tokenCacheKey: cacheKey,
|
|
540
|
+
endpointId: cacheKey,
|
|
404
541
|
statusCode: response.status,
|
|
405
542
|
response,
|
|
406
543
|
};
|
|
@@ -410,7 +547,7 @@ export class DataspaceNodeClient {
|
|
|
410
547
|
const grantedScopes = grantedScope ? grantedScope.split(' ').filter(Boolean) : scopes;
|
|
411
548
|
const expiresIn = Number(body.expires_in ?? 0);
|
|
412
549
|
const expiresAt = Date.now() + expiresIn * 1000;
|
|
413
|
-
this._tokenCache.set(
|
|
550
|
+
this._tokenCache.set(cacheKey, {
|
|
414
551
|
accessToken,
|
|
415
552
|
tokenType,
|
|
416
553
|
scopes: grantedScopes,
|
|
@@ -419,7 +556,8 @@ export class DataspaceNodeClient {
|
|
|
419
556
|
return {
|
|
420
557
|
status: 'fetched',
|
|
421
558
|
profile: 'smart-backend.v1',
|
|
422
|
-
|
|
559
|
+
tokenCacheKey: cacheKey,
|
|
560
|
+
endpointId: cacheKey,
|
|
423
561
|
statusCode: response.status,
|
|
424
562
|
accessToken,
|
|
425
563
|
tokenType,
|
|
@@ -432,12 +570,12 @@ export class DataspaceNodeClient {
|
|
|
432
570
|
* Exchange token payload against gateway token endpoint and cache the result.
|
|
433
571
|
*/
|
|
434
572
|
async requestSmartToken(input) {
|
|
435
|
-
const
|
|
436
|
-
if (!
|
|
437
|
-
throw new Error('requestSmartToken requires
|
|
573
|
+
const tokenCacheKey = String(input.tokenCacheKey || input.endpointId || '').trim();
|
|
574
|
+
if (!tokenCacheKey) {
|
|
575
|
+
throw new Error('requestSmartToken requires tokenCacheKey.');
|
|
438
576
|
}
|
|
439
577
|
const normalizedScopes = Array.from(new Set((input.scopes || []).filter(Boolean))).sort();
|
|
440
|
-
const cached = this._tokenCache.get(
|
|
578
|
+
const cached = this._tokenCache.get(tokenCacheKey);
|
|
441
579
|
if (cached && cached.expiresAt > Date.now() + 30_000) {
|
|
442
580
|
return {
|
|
443
581
|
status: 'cached',
|
|
@@ -462,7 +600,7 @@ export class DataspaceNodeClient {
|
|
|
462
600
|
: String(body.scope ?? '').trim().split(' ').filter(Boolean);
|
|
463
601
|
const resolvedScopes = grantedScopes.length ? grantedScopes : normalizedScopes;
|
|
464
602
|
const expiresIn = Number(body.expires_in ?? 0);
|
|
465
|
-
this._tokenCache.set(
|
|
603
|
+
this._tokenCache.set(tokenCacheKey, {
|
|
466
604
|
accessToken,
|
|
467
605
|
tokenType,
|
|
468
606
|
scopes: resolvedScopes,
|
|
@@ -477,6 +615,58 @@ export class DataspaceNodeClient {
|
|
|
477
615
|
response,
|
|
478
616
|
};
|
|
479
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
|
+
}
|
|
480
670
|
// ---- Private auth helpers ----------------------------------------------
|
|
481
671
|
_pkceS256Challenge(verifier) {
|
|
482
672
|
return createHash('sha256').update(verifier).digest().toString('base64url');
|
|
@@ -593,6 +783,25 @@ export class DataspaceNodeClient {
|
|
|
593
783
|
}
|
|
594
784
|
// ---- Generic batch API --------------------------------------------------
|
|
595
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
|
+
*
|
|
596
805
|
* POST a DIDComm plaintext payload to a batch submit path.
|
|
597
806
|
* Use this for all `_batch` routes (family registration, observations, tasks, etc.).
|
|
598
807
|
* Content-Type: `application/didcomm-plaintext+json`.
|
|
@@ -600,13 +809,18 @@ export class DataspaceNodeClient {
|
|
|
600
809
|
* Returns immediately with the HTTP response — pair with `pollUntilComplete` or use `submitAndPoll`.
|
|
601
810
|
*/
|
|
602
811
|
async submitBatch(path, payload) {
|
|
603
|
-
const
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
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 };
|
|
610
824
|
}
|
|
611
825
|
/**
|
|
612
826
|
* Sign and encrypt a DIDComm payload (nested JWS-in-JWE) and POST to the given path.
|
|
@@ -623,40 +837,32 @@ export class DataspaceNodeClient {
|
|
|
623
837
|
}
|
|
624
838
|
const publicJwks = await this.wallet.getPublicJwks(walletContext);
|
|
625
839
|
const signingJwk = publicJwks.find((jwk) => jwk.use === 'sig' || jwk.alg === 'ES384') ?? publicJwks[0];
|
|
626
|
-
// Step 1: sign payload claims as compact JWS
|
|
627
|
-
const compactJws = await this.wallet.signCompactJws(walletContext, {
|
|
628
|
-
header: {
|
|
629
|
-
typ: 'application/didcomm-signed+json',
|
|
630
|
-
alg: 'ES384',
|
|
631
|
-
...(signingJwk?.kid ? { kid: signingJwk.kid } : {}),
|
|
632
|
-
},
|
|
633
|
-
claims: payload,
|
|
634
|
-
});
|
|
635
|
-
// Step 2: encrypt JWS string as compact JWE (RSA-OAEP-256 + A256GCM, cty: JWS)
|
|
636
|
-
const compactJwe = await this.wallet.buildCompactJwe(walletContext, {
|
|
637
|
-
plaintext: compactJws,
|
|
638
|
-
recipientJwk: recipientEncryptionJwk,
|
|
639
|
-
contentType: 'JWS',
|
|
640
|
-
});
|
|
641
|
-
// Step 3: POST the JWE token directly (not JSON-serialised)
|
|
642
840
|
const url = /^https?:\/\//.test(path)
|
|
643
841
|
? path
|
|
644
842
|
: `${this.baseUrl}${path.startsWith('/') ? path : `/${path}`}`;
|
|
645
|
-
const
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
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 };
|
|
660
866
|
}
|
|
661
867
|
/**
|
|
662
868
|
* POST a plain JSON payload.
|
|
@@ -672,6 +878,13 @@ export class DataspaceNodeClient {
|
|
|
672
878
|
body,
|
|
673
879
|
};
|
|
674
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
|
+
}
|
|
675
888
|
/**
|
|
676
889
|
* POST a multipart/form-data payload.
|
|
677
890
|
* Use for file upload endpoints. Prefer `uploadConversionFile` for DataConversion uploads.
|
|
@@ -729,6 +942,7 @@ export class DataspaceNodeClient {
|
|
|
729
942
|
return {
|
|
730
943
|
status: response.status,
|
|
731
944
|
body: await this.parseResponseBody(response),
|
|
945
|
+
retryAfterMs: parseRetryAfterMs(response.headers.get('retry-after')),
|
|
732
946
|
};
|
|
733
947
|
}
|
|
734
948
|
/**
|
|
@@ -766,6 +980,7 @@ export class DataspaceNodeClient {
|
|
|
766
980
|
* (appointment, medication schedule, or another event), mapped to `based-on-display`.
|
|
767
981
|
*/
|
|
768
982
|
async createPhoneReminderTasks(ctx, input, options) {
|
|
983
|
+
const routeCtx = this.requireRouteContext(ctx);
|
|
769
984
|
const windows = Array.isArray(input.windows) ? input.windows : [];
|
|
770
985
|
if (!windows.length) {
|
|
771
986
|
throw new Error('createPhoneReminderTasks requires at least one reminder window.');
|
|
@@ -785,9 +1000,9 @@ export class DataspaceNodeClient {
|
|
|
785
1000
|
throw new Error('createPhoneReminderTasks requires remindAt in every window.');
|
|
786
1001
|
}
|
|
787
1002
|
const taskIdSeed = [
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
1003
|
+
routeCtx.tenantId,
|
|
1004
|
+
routeCtx.jurisdiction,
|
|
1005
|
+
routeCtx.sector,
|
|
791
1006
|
input.subjectRef,
|
|
792
1007
|
input.ownerRef,
|
|
793
1008
|
input.focusRef,
|
|
@@ -832,12 +1047,12 @@ export class DataspaceNodeClient {
|
|
|
832
1047
|
};
|
|
833
1048
|
});
|
|
834
1049
|
const payload = createDidcommPlainMessage({
|
|
835
|
-
iss:
|
|
836
|
-
aud:
|
|
1050
|
+
iss: routeCtx.tenantId,
|
|
1051
|
+
aud: routeCtx.tenantId,
|
|
837
1052
|
thid,
|
|
838
1053
|
body: { data },
|
|
839
1054
|
});
|
|
840
|
-
return this.submitAndPoll(this.individualTaskBatchPath(
|
|
1055
|
+
return this.submitAndPoll(this.individualTaskBatchPath(routeCtx), this.individualTaskPollPath(routeCtx), payload, options ?? { timeoutMs: 20_000, intervalMs: 1_000 });
|
|
841
1056
|
}
|
|
842
1057
|
/** Endpoint path for medication overlap pre-check (planned GW contract). */
|
|
843
1058
|
individualMedicationOverlapCheckPath(ctx) {
|
|
@@ -934,11 +1149,12 @@ export class DataspaceNodeClient {
|
|
|
934
1149
|
* Returns `null` when no matching registration exists.
|
|
935
1150
|
*/
|
|
936
1151
|
async searchFamilyOrganization(ctx, filters, options) {
|
|
1152
|
+
const routeCtx = this.requireRouteContext(ctx);
|
|
937
1153
|
const thid = `search-${randomUUID()}`;
|
|
938
1154
|
const claims = {
|
|
939
1155
|
'org.schema.Organization.owner.telephone': filters.controllerPhone,
|
|
940
1156
|
'org.schema.Organization.alternateName': filters.usualname,
|
|
941
|
-
'org.schema.Service.category':
|
|
1157
|
+
'org.schema.Service.category': routeCtx.sector,
|
|
942
1158
|
};
|
|
943
1159
|
if (filters.birthDate) {
|
|
944
1160
|
claims['org.schema.Organization.foundingDate'] = filters.birthDate;
|
|
@@ -946,8 +1162,8 @@ export class DataspaceNodeClient {
|
|
|
946
1162
|
const payload = {
|
|
947
1163
|
jti: randomUUID(),
|
|
948
1164
|
thid,
|
|
949
|
-
iss:
|
|
950
|
-
aud:
|
|
1165
|
+
iss: routeCtx.tenantId,
|
|
1166
|
+
aud: routeCtx.tenantId,
|
|
951
1167
|
type: 'application/api+json',
|
|
952
1168
|
body: {
|
|
953
1169
|
data: [{
|
|
@@ -957,7 +1173,7 @@ export class DataspaceNodeClient {
|
|
|
957
1173
|
}],
|
|
958
1174
|
},
|
|
959
1175
|
};
|
|
960
|
-
const result = await this.submitAndPoll(this.individualFamilyOrganizationSearchPath(
|
|
1176
|
+
const result = await this.submitAndPoll(this.individualFamilyOrganizationSearchPath(routeCtx), this.individualFamilyOrganizationSearchPollPath(routeCtx), payload, options ?? { timeoutMs: 20_000, intervalMs: 1_000 });
|
|
961
1177
|
if (result.poll.status !== 200)
|
|
962
1178
|
return null;
|
|
963
1179
|
const entry = result.poll.body?.body?.data?.[0];
|
|
@@ -992,6 +1208,11 @@ export class DataspaceNodeClient {
|
|
|
992
1208
|
vp_token: input.vpToken,
|
|
993
1209
|
...(input.additionalClaims || {}),
|
|
994
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;
|
|
995
1216
|
if (input.organizationVc)
|
|
996
1217
|
claims['org.schema.OrganizationCredential.jwt'] = input.organizationVc;
|
|
997
1218
|
if (input.legalRepresentativeVc) {
|
|
@@ -1003,6 +1224,10 @@ export class DataspaceNodeClient {
|
|
|
1003
1224
|
iss: 'did:web:controller.example.com',
|
|
1004
1225
|
aud: 'did:web:host.example.com',
|
|
1005
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 } : {}),
|
|
1006
1231
|
data: [
|
|
1007
1232
|
{
|
|
1008
1233
|
type: 'Organization-activation-request-v1.0',
|
|
@@ -1014,6 +1239,180 @@ export class DataspaceNodeClient {
|
|
|
1014
1239
|
});
|
|
1015
1240
|
return this.submitAndPoll(this.hostRegistryOrganizationActivatePath(ctx), this.hostRegistryOrganizationActivatePollPath(ctx), payload, options);
|
|
1016
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
|
+
}
|
|
1017
1416
|
/**
|
|
1018
1417
|
* Activate employee/member device by activation code exchange + DCR registration.
|
|
1019
1418
|
*
|
|
@@ -1054,13 +1453,30 @@ export class DataspaceNodeClient {
|
|
|
1054
1453
|
dcr,
|
|
1055
1454
|
};
|
|
1056
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
|
+
}
|
|
1057
1472
|
/**
|
|
1058
1473
|
* UC 5.3 wrapper: create organization employee in entity Employee batch route.
|
|
1059
1474
|
*/
|
|
1060
1475
|
async createOrganizationEmployee(ctx, input, options) {
|
|
1476
|
+
const routeCtx = this.requireRouteContext(ctx);
|
|
1061
1477
|
const payload = createDidcommPlainMessage({
|
|
1062
|
-
iss:
|
|
1063
|
-
aud:
|
|
1478
|
+
iss: routeCtx.tenantId,
|
|
1479
|
+
aud: routeCtx.tenantId,
|
|
1064
1480
|
thid: `employee-${randomUUID()}`,
|
|
1065
1481
|
body: {
|
|
1066
1482
|
data: [
|
|
@@ -1072,7 +1488,7 @@ export class DataspaceNodeClient {
|
|
|
1072
1488
|
],
|
|
1073
1489
|
},
|
|
1074
1490
|
});
|
|
1075
|
-
return this.submitAndPoll(this.employeeBatchPath(
|
|
1491
|
+
return this.submitAndPoll(this.employeeBatchPath(routeCtx), this.employeePollPath(routeCtx), payload, options);
|
|
1076
1492
|
}
|
|
1077
1493
|
/**
|
|
1078
1494
|
* UC 5.1 wrapper: bootstrap subject organization context via registration + optional order confirmation.
|
|
@@ -1093,20 +1509,120 @@ export class DataspaceNodeClient {
|
|
|
1093
1509
|
const confirmation = await this.submitAndPoll(this.individualFamilyOrderBatchPath(ctx), this.individualFamilyOrderPollPath(ctx), confirmationPayload, input.pollOptions);
|
|
1094
1510
|
return { registration, confirmation };
|
|
1095
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
|
+
}
|
|
1096
1611
|
/**
|
|
1097
1612
|
* UC 5.5 wrapper: import IPS/FHIR composition and update subject index context.
|
|
1098
1613
|
*/
|
|
1099
1614
|
async importIpsOrFhirAndUpdateIndex(ctx, input) {
|
|
1615
|
+
const routeCtx = this.requireRouteContext(ctx);
|
|
1100
1616
|
const payload = {
|
|
1101
1617
|
thid: input.compositionPayload.thid || `composition-${randomUUID()}`,
|
|
1102
1618
|
...input.compositionPayload,
|
|
1103
1619
|
};
|
|
1104
1620
|
const submitPath = (input.format || 'r4') === 'api'
|
|
1105
|
-
? this.individualCompositionR4BatchPath(
|
|
1106
|
-
: this.individualCompositionR4BatchPath(
|
|
1621
|
+
? this.individualCompositionR4BatchPath(routeCtx).replace('/org.hl7.fhir.r4/', '/org.hl7.fhir.api/')
|
|
1622
|
+
: this.individualCompositionR4BatchPath(routeCtx);
|
|
1107
1623
|
const pollPath = (input.format || 'r4') === 'api'
|
|
1108
|
-
? this.individualCompositionR4PollPath(
|
|
1109
|
-
: this.individualCompositionR4PollPath(
|
|
1624
|
+
? this.individualCompositionR4PollPath(routeCtx).replace('/org.hl7.fhir.r4/', '/org.hl7.fhir.api/')
|
|
1625
|
+
: this.individualCompositionR4PollPath(routeCtx);
|
|
1110
1626
|
return this.submitAndPoll(submitPath, pollPath, payload, input.pollOptions);
|
|
1111
1627
|
}
|
|
1112
1628
|
/**
|
|
@@ -1114,6 +1630,7 @@ export class DataspaceNodeClient {
|
|
|
1114
1630
|
* Builds canonical Consent claims and submits/polls the Consent batch.
|
|
1115
1631
|
*/
|
|
1116
1632
|
async grantProfessionalAccessSimple(ctx, input) {
|
|
1633
|
+
const routeCtx = this.requireRouteContext(ctx);
|
|
1117
1634
|
const built = buildConsentClaimsSimpleWithCid({
|
|
1118
1635
|
subjectDid: input.subjectDid,
|
|
1119
1636
|
subjectPhone: input.subjectPhone,
|
|
@@ -1143,7 +1660,7 @@ export class DataspaceNodeClient {
|
|
|
1143
1660
|
],
|
|
1144
1661
|
},
|
|
1145
1662
|
};
|
|
1146
|
-
const consent = await this.submitAndPoll(this.individualConsentR4BatchPath(
|
|
1663
|
+
const consent = await this.submitAndPoll(this.individualConsentR4BatchPath(routeCtx), this.individualConsentR4PollPath(routeCtx), consentPayload, input.pollOptions);
|
|
1147
1664
|
return {
|
|
1148
1665
|
thid,
|
|
1149
1666
|
consent,
|
|
@@ -1157,16 +1674,17 @@ export class DataspaceNodeClient {
|
|
|
1157
1674
|
* UC 5.7 wrapper: generate digital twin composition from subject data.
|
|
1158
1675
|
*/
|
|
1159
1676
|
async generateDigitalTwinFromSubjectData(ctx, input) {
|
|
1677
|
+
const routeCtx = this.requireRouteContext(ctx);
|
|
1160
1678
|
const payload = {
|
|
1161
1679
|
thid: input.compositionPayload.thid || `digital-twin-${randomUUID()}`,
|
|
1162
1680
|
...input.compositionPayload,
|
|
1163
1681
|
};
|
|
1164
1682
|
const submitPath = (input.format || 'r4') === 'api'
|
|
1165
|
-
? this.digitalTwinCompositionApiBatchPath(
|
|
1166
|
-
: this.digitalTwinCompositionR4BatchPath(
|
|
1683
|
+
? this.digitalTwinCompositionApiBatchPath(routeCtx)
|
|
1684
|
+
: this.digitalTwinCompositionR4BatchPath(routeCtx);
|
|
1167
1685
|
const pollPath = (input.format || 'r4') === 'api'
|
|
1168
|
-
? this.digitalTwinCompositionApiPollPath(
|
|
1169
|
-
: this.digitalTwinCompositionR4PollPath(
|
|
1686
|
+
? this.digitalTwinCompositionApiPollPath(routeCtx)
|
|
1687
|
+
: this.digitalTwinCompositionR4PollPath(routeCtx);
|
|
1170
1688
|
return this.submitAndPoll(submitPath, pollPath, payload, input.pollOptions);
|
|
1171
1689
|
}
|
|
1172
1690
|
/**
|
|
@@ -1196,7 +1714,8 @@ export class DataspaceNodeClient {
|
|
|
1196
1714
|
if (Date.now() - startedAt > timeoutMs) {
|
|
1197
1715
|
throw new Error(`Polling timeout after ${attempts} attempts (${timeoutMs}ms).`);
|
|
1198
1716
|
}
|
|
1199
|
-
|
|
1717
|
+
const waitMs = options?.intervalMs ?? result.retryAfterMs ?? intervalMs;
|
|
1718
|
+
await new Promise((resolve) => setTimeout(resolve, waitMs));
|
|
1200
1719
|
}
|
|
1201
1720
|
}
|
|
1202
1721
|
// ---- Internal HTTP helpers ---------------------------------------------
|
|
@@ -1220,10 +1739,17 @@ export class DataspaceNodeClient {
|
|
|
1220
1739
|
}
|
|
1221
1740
|
async parseResponseBody(response) {
|
|
1222
1741
|
const contentType = response.headers.get('content-type') || '';
|
|
1742
|
+
const raw = await response.text();
|
|
1743
|
+
if (!raw)
|
|
1744
|
+
return {};
|
|
1223
1745
|
if (contentType.includes('application/json') || contentType.includes('application/didcomm-plaintext+json')) {
|
|
1224
|
-
|
|
1746
|
+
try {
|
|
1747
|
+
return JSON.parse(raw);
|
|
1748
|
+
}
|
|
1749
|
+
catch {
|
|
1750
|
+
return {};
|
|
1751
|
+
}
|
|
1225
1752
|
}
|
|
1226
|
-
|
|
1227
|
-
return text;
|
|
1753
|
+
return raw;
|
|
1228
1754
|
}
|
|
1229
1755
|
}
|