dataspace-client-sdk-node 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/client.js CHANGED
@@ -8,21 +8,131 @@ function trimTrailingSlash(value) {
8
8
  function encode(value) {
9
9
  return encodeURIComponent(value);
10
10
  }
11
+ function toDidWebFromUrlOrHost(raw) {
12
+ const v = String(raw || '').trim();
13
+ if (!v)
14
+ return undefined;
15
+ if (v.startsWith('did:web:'))
16
+ return v;
17
+ const host = v
18
+ .replace(/^https?:\/\//i, '')
19
+ .replace(/\/.*$/, '')
20
+ .trim()
21
+ .toLowerCase();
22
+ if (!host)
23
+ return undefined;
24
+ return `did:web:${host}`;
25
+ }
26
+ function parseRetryAfterMs(header) {
27
+ if (!header)
28
+ return undefined;
29
+ const raw = header.trim();
30
+ if (!raw)
31
+ return undefined;
32
+ const seconds = Number(raw);
33
+ if (Number.isFinite(seconds) && seconds >= 0) {
34
+ return Math.floor(seconds * 1000);
35
+ }
36
+ const epochMs = Date.parse(raw);
37
+ if (Number.isFinite(epochMs)) {
38
+ const delta = epochMs - Date.now();
39
+ return delta > 0 ? delta : 0;
40
+ }
41
+ return undefined;
42
+ }
11
43
  export class DataspaceNodeClient {
12
44
  baseUrl;
13
45
  bearerToken;
14
46
  defaultHeaders;
15
47
  wallet;
48
+ defaultCtx;
49
+ defaultTimeoutMs;
50
+ defaultIntervalMs;
16
51
  _tokenCache = new Map();
17
52
  constructor(options) {
18
53
  this.baseUrl = trimTrailingSlash(options.baseUrl);
19
54
  this.bearerToken = options.bearerToken;
20
55
  this.defaultHeaders = options.defaultHeaders ?? {};
21
56
  this.wallet = options.wallet;
57
+ this.defaultCtx = options.ctx;
22
58
  }
23
59
  getWallet() {
24
60
  return this.wallet;
25
61
  }
62
+ /**
63
+ * Set default route context for subsequent calls.
64
+ */
65
+ setContext(ctx) {
66
+ this.defaultCtx = { ...ctx };
67
+ return this;
68
+ }
69
+ /**
70
+ * Preferred alias for organization/tenant integration context.
71
+ */
72
+ setContextOrg(ctx) {
73
+ return this.setContext(ctx);
74
+ }
75
+ setTenantId(tenantId) {
76
+ const current = this.defaultCtx ?? { tenantId: '', jurisdiction: '', sector: '' };
77
+ this.defaultCtx = { ...current, tenantId };
78
+ return this;
79
+ }
80
+ setJurisdiction(jurisdiction) {
81
+ const current = this.defaultCtx ?? { tenantId: '', jurisdiction: '', sector: '' };
82
+ this.defaultCtx = { ...current, jurisdiction };
83
+ return this;
84
+ }
85
+ setSector(sector) {
86
+ const current = this.defaultCtx ?? { tenantId: '', jurisdiction: '', sector: '' };
87
+ this.defaultCtx = { ...current, sector };
88
+ return this;
89
+ }
90
+ setDefaultTimeoutSeconds(seconds) {
91
+ if (Number.isFinite(Number(seconds))) {
92
+ this.defaultTimeoutMs = Math.max(1, Math.floor(Number(seconds) * 1000));
93
+ }
94
+ return this;
95
+ }
96
+ setDefaultIntervalSeconds(seconds) {
97
+ if (Number.isFinite(Number(seconds))) {
98
+ this.defaultIntervalMs = Math.max(1, Math.floor(Number(seconds) * 1000));
99
+ }
100
+ return this;
101
+ }
102
+ resolveSimplePollOptions(timeoutSeconds, intervalSeconds) {
103
+ const pollOptions = {};
104
+ if (Number.isFinite(Number(timeoutSeconds))) {
105
+ pollOptions.timeoutMs = Math.max(1, Math.floor(Number(timeoutSeconds) * 1000));
106
+ }
107
+ else if (this.defaultTimeoutMs) {
108
+ pollOptions.timeoutMs = this.defaultTimeoutMs;
109
+ }
110
+ if (Number.isFinite(Number(intervalSeconds))) {
111
+ pollOptions.intervalMs = Math.max(1, Math.floor(Number(intervalSeconds) * 1000));
112
+ }
113
+ else if (this.defaultIntervalMs) {
114
+ pollOptions.intervalMs = this.defaultIntervalMs;
115
+ }
116
+ return Object.keys(pollOptions).length ? pollOptions : undefined;
117
+ }
118
+ requireRouteContext(ctx) {
119
+ const resolved = ctx ?? this.defaultCtx;
120
+ const tenantId = String(resolved?.tenantId || '').trim();
121
+ const jurisdiction = String(resolved?.jurisdiction || '').trim();
122
+ const sector = String(resolved?.sector || '').trim();
123
+ if (!tenantId || !jurisdiction || !sector) {
124
+ throw new Error('Route context is required. Provide `ctx` in method call or constructor options.');
125
+ }
126
+ return { tenantId, jurisdiction, sector };
127
+ }
128
+ requireHostRouteContext(ctx) {
129
+ const jurisdiction = String(ctx?.jurisdiction || this.defaultCtx?.jurisdiction || '').trim();
130
+ const sector = String(ctx?.sector || this.defaultCtx?.sector || '').trim();
131
+ if (jurisdiction && sector) {
132
+ return { jurisdiction, sector };
133
+ }
134
+ throw new Error('Host route context is required. Provide `ctx` in method call or constructor options.ctx.');
135
+ }
26
136
  // ---- Path helpers -------------------------------------------------------
27
137
  /**
28
138
  * Generic GW v1 tenant route builder.
@@ -36,7 +146,8 @@ export class DataspaceNodeClient {
36
146
  * // → /acme/cds-ES/v1/health-care/individual/org.schema/Organization/_batch
37
147
  */
38
148
  v1Path(ctx, section, format, resourceType, action) {
39
- return `/${encode(ctx.tenantId)}/cds-${encode(ctx.jurisdiction)}/v1/${encode(ctx.sector)}/${encode(section)}/${encode(format)}/${encode(resourceType)}/${encode(action)}`;
149
+ const routeCtx = this.requireRouteContext(ctx);
150
+ return `/${encode(routeCtx.tenantId)}/cds-${encode(routeCtx.jurisdiction)}/v1/${encode(routeCtx.sector)}/${encode(section)}/${encode(format)}/${encode(resourceType)}/${encode(action)}`;
40
151
  }
41
152
  /**
42
153
  * Generic tenant-scoped identity route builder.
@@ -46,7 +157,8 @@ export class DataspaceNodeClient {
46
157
  * Dedicated path methods in this SDK use `host` (GW convention).
47
158
  */
48
159
  tenantIdentityPath(ctx, prefix, action) {
49
- return `/${encode(prefix)}/cds-${encode(ctx.jurisdiction)}/v1/${encode(ctx.sector)}/${encode(ctx.tenantId)}/identity/auth/${encode(action)}`;
160
+ const routeCtx = this.requireRouteContext(ctx);
161
+ return `/${encode(prefix)}/cds-${encode(routeCtx.jurisdiction)}/v1/${encode(routeCtx.sector)}/${encode(routeCtx.tenantId)}/identity/auth/${encode(action)}`;
50
162
  }
51
163
  /**
52
164
  * Generic host registry route builder (tenant-agnostic, `host/` prefix).
@@ -55,7 +167,8 @@ export class DataspaceNodeClient {
55
167
  * Pattern: `/host/cds-{jurisdiction}/v1/{sector}/registry/org.schema/{resourceType}/{action}`
56
168
  */
57
169
  hostRegistryPath(ctx, resourceType, action) {
58
- return `/host/cds-${encode(ctx.jurisdiction)}/v1/${encode(ctx.sector)}/registry/org.schema/${encode(resourceType)}/${encode(action)}`;
170
+ const hostCtx = this.requireHostRouteContext(ctx);
171
+ return `/host/cds-${encode(hostCtx.jurisdiction)}/v1/${encode(hostCtx.sector)}/registry/org.schema/${encode(resourceType)}/${encode(action)}`;
59
172
  }
60
173
  /** Submit path: host registry Organization batch (controller-level org registration). */
61
174
  hostRegistryOrganizationBatchPath(ctx) {
@@ -86,11 +199,13 @@ export class DataspaceNodeClient {
86
199
  * Use for `family-registration/_create-or-resume` DIDComm payloads.
87
200
  */
88
201
  individualFamilyOrganizationBatchPath(ctx) {
89
- return `/${encode(ctx.tenantId)}/cds-${encode(ctx.jurisdiction)}/v1/${encode(ctx.sector)}/individual/org.schema/Organization/_batch`;
202
+ const routeCtx = this.requireRouteContext(ctx);
203
+ return `/${encode(routeCtx.tenantId)}/cds-${encode(routeCtx.jurisdiction)}/v1/${encode(routeCtx.sector)}/individual/org.schema/Organization/_batch`;
90
204
  }
91
205
  /** Poll path: individual/family Organization. Pair with `individualFamilyOrganizationBatchPath`. */
92
206
  individualFamilyOrganizationPollPath(ctx) {
93
- return `/${encode(ctx.tenantId)}/cds-${encode(ctx.jurisdiction)}/v1/${encode(ctx.sector)}/individual/org.schema/Organization/_batch-response`;
207
+ const routeCtx = this.requireRouteContext(ctx);
208
+ return `/${encode(routeCtx.tenantId)}/cds-${encode(routeCtx.jurisdiction)}/v1/${encode(routeCtx.sector)}/individual/org.schema/Organization/_batch-response`;
94
209
  }
95
210
  /** Submit path: individual/family Organization search (`org.schema/Organization/_search`). */
96
211
  individualFamilyOrganizationSearchPath(ctx) {
@@ -102,11 +217,13 @@ export class DataspaceNodeClient {
102
217
  }
103
218
  /** Submit path: individual/family Order batch (`org.schema/Order/_batch`). */
104
219
  individualFamilyOrderBatchPath(ctx) {
105
- return `/${encode(ctx.tenantId)}/cds-${encode(ctx.jurisdiction)}/v1/${encode(ctx.sector)}/individual/org.schema/Order/_batch`;
220
+ const routeCtx = this.requireRouteContext(ctx);
221
+ return `/${encode(routeCtx.tenantId)}/cds-${encode(routeCtx.jurisdiction)}/v1/${encode(routeCtx.sector)}/individual/org.schema/Order/_batch`;
106
222
  }
107
223
  /** Poll path: individual/family Order. Pair with `individualFamilyOrderBatchPath`. */
108
224
  individualFamilyOrderPollPath(ctx) {
109
- return `/${encode(ctx.tenantId)}/cds-${encode(ctx.jurisdiction)}/v1/${encode(ctx.sector)}/individual/org.schema/Order/_batch-response`;
225
+ const routeCtx = this.requireRouteContext(ctx);
226
+ return `/${encode(routeCtx.tenantId)}/cds-${encode(routeCtx.jurisdiction)}/v1/${encode(routeCtx.sector)}/individual/org.schema/Order/_batch-response`;
110
227
  }
111
228
  /** Submit path: individual RelatedPerson (FHIR R4 API, `org.hl7.fhir.api/RelatedPerson/_batch`). */
112
229
  individualRelatedPersonBatchPath(ctx) {
@@ -477,6 +594,58 @@ export class DataspaceNodeClient {
477
594
  response,
478
595
  };
479
596
  }
597
+ /**
598
+ * Friendly wrapper for SMART token request via GW identity/auth token-exchange route.
599
+ * Uses one object, seconds-based polling, and constructor ctx fallback.
600
+ */
601
+ async requestSmartTokenSimple(input) {
602
+ const routeCtx = this.requireRouteContext(input.tenantId && input.jurisdiction && input.sector
603
+ ? { tenantId: input.tenantId, jurisdiction: input.jurisdiction, sector: input.sector }
604
+ : undefined);
605
+ const normalizedScopes = Array.from(new Set((input.scopes || []).filter(Boolean))).sort();
606
+ const endpointId = String(input.endpointId || `smart:${routeCtx.tenantId}:${normalizedScopes.join(',')}`).trim();
607
+ if (!endpointId) {
608
+ throw new Error('requestSmartTokenSimple requires endpointId (or non-empty scopes).');
609
+ }
610
+ const pollOptions = this.resolveSimplePollOptions(input.timeoutSeconds, input.intervalSeconds);
611
+ const payload = {
612
+ thid: `exchange-${randomUUID()}`,
613
+ grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange',
614
+ subject_token_type: 'urn:ietf:params:oauth:token-type:id_token',
615
+ subject_token: input.idToken,
616
+ scope: normalizedScopes.join(' '),
617
+ organization: routeCtx.tenantId,
618
+ ...(input.additionalClaims || {}),
619
+ };
620
+ const exchange = await this.submitAndPoll(this.identityTokenExchangePath(routeCtx), this.identityTokenExchangePollPath(routeCtx), payload, pollOptions);
621
+ const exchangeBody = exchange.poll.body ?? {};
622
+ const accessToken = String(exchangeBody.access_token || '').trim();
623
+ if (exchange.poll.status >= 400 || !accessToken) {
624
+ return {
625
+ status: 'failed',
626
+ statusCode: exchange.poll.status,
627
+ response: exchange.poll.body,
628
+ };
629
+ }
630
+ const tokenType = String(exchangeBody.token_type || 'Bearer');
631
+ const grantedScopes = String(exchangeBody.scope || '').trim().split(' ').filter(Boolean);
632
+ const resolvedScopes = grantedScopes.length ? grantedScopes : normalizedScopes;
633
+ const expiresIn = Number(exchangeBody.expires_in ?? 0);
634
+ this._tokenCache.set(endpointId, {
635
+ accessToken,
636
+ tokenType,
637
+ scopes: resolvedScopes,
638
+ expiresAt: Date.now() + expiresIn * 1000,
639
+ });
640
+ return {
641
+ status: 'fetched',
642
+ accessToken,
643
+ tokenType,
644
+ scopes: resolvedScopes,
645
+ statusCode: exchange.poll.status,
646
+ response: exchange.poll.body,
647
+ };
648
+ }
480
649
  // ---- Private auth helpers ----------------------------------------------
481
650
  _pkceS256Challenge(verifier) {
482
651
  return createHash('sha256').update(verifier).digest().toString('base64url');
@@ -729,6 +898,7 @@ export class DataspaceNodeClient {
729
898
  return {
730
899
  status: response.status,
731
900
  body: await this.parseResponseBody(response),
901
+ retryAfterMs: parseRetryAfterMs(response.headers.get('retry-after')),
732
902
  };
733
903
  }
734
904
  /**
@@ -766,6 +936,7 @@ export class DataspaceNodeClient {
766
936
  * (appointment, medication schedule, or another event), mapped to `based-on-display`.
767
937
  */
768
938
  async createPhoneReminderTasks(ctx, input, options) {
939
+ const routeCtx = this.requireRouteContext(ctx);
769
940
  const windows = Array.isArray(input.windows) ? input.windows : [];
770
941
  if (!windows.length) {
771
942
  throw new Error('createPhoneReminderTasks requires at least one reminder window.');
@@ -785,9 +956,9 @@ export class DataspaceNodeClient {
785
956
  throw new Error('createPhoneReminderTasks requires remindAt in every window.');
786
957
  }
787
958
  const taskIdSeed = [
788
- ctx.tenantId,
789
- ctx.jurisdiction,
790
- ctx.sector,
959
+ routeCtx.tenantId,
960
+ routeCtx.jurisdiction,
961
+ routeCtx.sector,
791
962
  input.subjectRef,
792
963
  input.ownerRef,
793
964
  input.focusRef,
@@ -832,12 +1003,12 @@ export class DataspaceNodeClient {
832
1003
  };
833
1004
  });
834
1005
  const payload = createDidcommPlainMessage({
835
- iss: ctx.tenantId,
836
- aud: ctx.tenantId,
1006
+ iss: routeCtx.tenantId,
1007
+ aud: routeCtx.tenantId,
837
1008
  thid,
838
1009
  body: { data },
839
1010
  });
840
- return this.submitAndPoll(this.individualTaskBatchPath(ctx), this.individualTaskPollPath(ctx), payload, options ?? { timeoutMs: 20_000, intervalMs: 1_000 });
1011
+ return this.submitAndPoll(this.individualTaskBatchPath(routeCtx), this.individualTaskPollPath(routeCtx), payload, options ?? { timeoutMs: 20_000, intervalMs: 1_000 });
841
1012
  }
842
1013
  /** Endpoint path for medication overlap pre-check (planned GW contract). */
843
1014
  individualMedicationOverlapCheckPath(ctx) {
@@ -934,11 +1105,12 @@ export class DataspaceNodeClient {
934
1105
  * Returns `null` when no matching registration exists.
935
1106
  */
936
1107
  async searchFamilyOrganization(ctx, filters, options) {
1108
+ const routeCtx = this.requireRouteContext(ctx);
937
1109
  const thid = `search-${randomUUID()}`;
938
1110
  const claims = {
939
1111
  'org.schema.Organization.owner.telephone': filters.controllerPhone,
940
1112
  'org.schema.Organization.alternateName': filters.usualname,
941
- 'org.schema.Service.category': ctx.sector,
1113
+ 'org.schema.Service.category': routeCtx.sector,
942
1114
  };
943
1115
  if (filters.birthDate) {
944
1116
  claims['org.schema.Organization.foundingDate'] = filters.birthDate;
@@ -946,8 +1118,8 @@ export class DataspaceNodeClient {
946
1118
  const payload = {
947
1119
  jti: randomUUID(),
948
1120
  thid,
949
- iss: ctx.tenantId,
950
- aud: ctx.tenantId,
1121
+ iss: routeCtx.tenantId,
1122
+ aud: routeCtx.tenantId,
951
1123
  type: 'application/api+json',
952
1124
  body: {
953
1125
  data: [{
@@ -957,7 +1129,7 @@ export class DataspaceNodeClient {
957
1129
  }],
958
1130
  },
959
1131
  };
960
- const result = await this.submitAndPoll(this.individualFamilyOrganizationSearchPath(ctx), this.individualFamilyOrganizationSearchPollPath(ctx), payload, options ?? { timeoutMs: 20_000, intervalMs: 1_000 });
1132
+ const result = await this.submitAndPoll(this.individualFamilyOrganizationSearchPath(routeCtx), this.individualFamilyOrganizationSearchPollPath(routeCtx), payload, options ?? { timeoutMs: 20_000, intervalMs: 1_000 });
961
1133
  if (result.poll.status !== 200)
962
1134
  return null;
963
1135
  const entry = result.poll.body?.body?.data?.[0];
@@ -992,6 +1164,11 @@ export class DataspaceNodeClient {
992
1164
  vp_token: input.vpToken,
993
1165
  ...(input.additionalClaims || {}),
994
1166
  };
1167
+ const requestedMembers = Number.isFinite(Number(input.numberOfMembers))
1168
+ ? Math.max(1, Math.floor(Number(input.numberOfMembers)))
1169
+ : 2;
1170
+ // Keep gateway-facing claim stable while exposing a generic SDK input.
1171
+ claims['org.schema.Organization.numberOfEmployees'] = requestedMembers;
995
1172
  if (input.organizationVc)
996
1173
  claims['org.schema.OrganizationCredential.jwt'] = input.organizationVc;
997
1174
  if (input.legalRepresentativeVc) {
@@ -1003,6 +1180,10 @@ export class DataspaceNodeClient {
1003
1180
  iss: 'did:web:controller.example.com',
1004
1181
  aud: 'did:web:host.example.com',
1005
1182
  body: {
1183
+ // GW activation parser expects proof material at top-level DIDComm body.
1184
+ vp_token: input.vpToken,
1185
+ ...(input.organizationVc ? { organizationCredential: input.organizationVc } : {}),
1186
+ ...(input.legalRepresentativeVc ? { representativeCredential: input.legalRepresentativeVc } : {}),
1006
1187
  data: [
1007
1188
  {
1008
1189
  type: 'Organization-activation-request-v1.0',
@@ -1014,6 +1195,157 @@ export class DataspaceNodeClient {
1014
1195
  });
1015
1196
  return this.submitAndPoll(this.hostRegistryOrganizationActivatePath(ctx), this.hostRegistryOrganizationActivatePollPath(ctx), payload, options);
1016
1197
  }
1198
+ /**
1199
+ * Friendly wrapper for legal organization activation.
1200
+ * Accepts one object and seconds-based polling options for integrator ergonomics.
1201
+ */
1202
+ async activateOrganizationInGatewaySimple(input) {
1203
+ const serviceProviderDidWeb = String(input.serviceProviderDidWeb || '').trim();
1204
+ const serviceProviderUrl = String(input.serviceProviderUrl || '').trim();
1205
+ const controllerEmail = String(input.controllerEmail || '').trim();
1206
+ const controllerTelephone = String(input.controllerTelephone || '').trim();
1207
+ const controllerRole = String(input.controllerRole || '').trim();
1208
+ const resolvedServiceDid = toDidWebFromUrlOrHost(serviceProviderDidWeb || serviceProviderUrl);
1209
+ if (!resolvedServiceDid) {
1210
+ throw new Error('activateOrganizationInGatewaySimple requires serviceProviderDidWeb or serviceProviderUrl.');
1211
+ }
1212
+ if (!controllerEmail && !controllerTelephone) {
1213
+ throw new Error('activateOrganizationInGatewaySimple requires controllerEmail or controllerTelephone.');
1214
+ }
1215
+ if (!controllerRole) {
1216
+ throw new Error('activateOrganizationInGatewaySimple requires controllerRole.');
1217
+ }
1218
+ const pollOptions = this.resolveSimplePollOptions(input.timeoutSeconds, input.intervalSeconds);
1219
+ const hostCtx = this.requireHostRouteContext(input.jurisdiction && input.sector
1220
+ ? { jurisdiction: input.jurisdiction, sector: input.sector }
1221
+ : undefined);
1222
+ const implicitClaims = {
1223
+ 'org.schema.Service.category': hostCtx.sector,
1224
+ 'org.schema.Service.identifier': resolvedServiceDid,
1225
+ ...(serviceProviderUrl ? { 'org.schema.Service.url': serviceProviderUrl } : {}),
1226
+ 'org.schema.Person.hasOccupation': controllerRole,
1227
+ ...(controllerEmail ? { 'org.schema.Person.email': controllerEmail } : {}),
1228
+ ...(controllerTelephone ? { 'org.schema.Person.telephone': controllerTelephone } : {}),
1229
+ };
1230
+ const activation = await this.activateOrganizationInGatewayFromIcaProof(hostCtx, {
1231
+ vpToken: input.vpToken,
1232
+ numberOfMembers: input.numberOfMembers,
1233
+ organizationVc: input.organizationVc,
1234
+ legalRepresentativeVc: input.legalRepresentativeVc,
1235
+ regulatoryEvidence: input.regulatoryEvidence,
1236
+ additionalClaims: { ...implicitClaims, ...(input.additionalClaims || {}) },
1237
+ }, pollOptions);
1238
+ this.assertFirstDidcommEntrySuccess(activation, 'activateOrganizationInGatewaySimple');
1239
+ return activation;
1240
+ }
1241
+ /**
1242
+ * Friendly wrapper for legal organization Order confirmation.
1243
+ * Accepts one object and builds payload/paths internally.
1244
+ */
1245
+ async confirmLegalOrganizationOrderSimple(input) {
1246
+ if (!String(input.offerId || '').trim()) {
1247
+ throw new Error('confirmLegalOrganizationOrderSimple requires offerId.');
1248
+ }
1249
+ const pollOptions = this.resolveSimplePollOptions(input.timeoutSeconds, input.intervalSeconds);
1250
+ const hostCtx = this.requireHostRouteContext(input.jurisdiction && input.sector
1251
+ ? { jurisdiction: input.jurisdiction, sector: input.sector }
1252
+ : undefined);
1253
+ const claims = {
1254
+ '@context': 'org.schema',
1255
+ 'Order.acceptedOffer.identifier': input.offerId,
1256
+ ...(input.additionalClaims || {}),
1257
+ };
1258
+ const payload = createDidcommPlainMessage({
1259
+ iss: 'did:web:controller.example.com',
1260
+ aud: 'did:web:host.example.com',
1261
+ thid: `order-${randomUUID()}`,
1262
+ body: {
1263
+ data: [{
1264
+ type: input.dataType || 'Organization-order-request-v1.0',
1265
+ meta: { claims }, // legacy compatibility
1266
+ resource: { meta: { claims } },
1267
+ }],
1268
+ },
1269
+ });
1270
+ const order = await this.submitAndPoll(this.hostRegistryOrderBatchPath(hostCtx), this.hostRegistryOrderPollPath(hostCtx), payload, pollOptions);
1271
+ this.assertFirstDidcommEntrySuccess(order, 'confirmLegalOrganizationOrderSimple');
1272
+ return order;
1273
+ }
1274
+ /**
1275
+ * Normalize GW async response into DIDComm message body.
1276
+ *
1277
+ * Transport note:
1278
+ * - GW poll responses are HTTP JSON envelopes
1279
+ * - business payload lives inside DIDComm `body`
1280
+ *
1281
+ * This helper abstracts envelope differences so consumers do not depend on
1282
+ * raw `poll.body.body` paths.
1283
+ */
1284
+ getDidcommMessageBodyFromResponse(result) {
1285
+ const pollBody = result?.poll?.body ?? result?.body ?? result;
1286
+ const didcommBody = pollBody?.body;
1287
+ if (didcommBody && typeof didcommBody === 'object')
1288
+ return didcommBody;
1289
+ if (pollBody && typeof pollBody === 'object' && Array.isArray(pollBody?.data)) {
1290
+ return pollBody;
1291
+ }
1292
+ return undefined;
1293
+ }
1294
+ /**
1295
+ * Return first DIDComm business entry from a submit/poll result.
1296
+ */
1297
+ getFirstDidcommDataEntryFromResponse(result) {
1298
+ const body = this.getDidcommMessageBodyFromResponse(result);
1299
+ const entry = body?.data?.[0];
1300
+ return entry && typeof entry === 'object' ? entry : undefined;
1301
+ }
1302
+ /**
1303
+ * Extract `org.schema.Offer.identifier` from a submit/poll result.
1304
+ *
1305
+ * This helper normalizes canonical and legacy claim locations.
1306
+ */
1307
+ getOfferIdFromResponse(result) {
1308
+ const entry = this.getFirstDidcommDataEntryFromResponse(result);
1309
+ const offerId = String(entry?.meta?.claims?.['org.schema.Offer.identifier']
1310
+ || entry?.resource?.meta?.claims?.['org.schema.Offer.identifier']
1311
+ || '').trim();
1312
+ return offerId || undefined;
1313
+ }
1314
+ /**
1315
+ * Extract a UI-ready Offer preview from activation/registration responses.
1316
+ */
1317
+ getOfferPreviewFromResponse(result) {
1318
+ const entry = this.getFirstDidcommDataEntryFromResponse(result);
1319
+ const claims = entry?.meta?.claims || entry?.resource?.meta?.claims || {};
1320
+ const seatsRaw = claims['org.schema.Offer.eligibleQuantity.value'];
1321
+ const seats = typeof seatsRaw === 'number'
1322
+ ? seatsRaw
1323
+ : (typeof seatsRaw === 'string' && seatsRaw.trim() ? Number(seatsRaw) : undefined);
1324
+ return {
1325
+ offerId: this.getOfferIdFromResponse(result),
1326
+ amount: claims['org.schema.Offer.price'],
1327
+ currency: claims['org.schema.Offer.priceCurrency'],
1328
+ seats: Number.isFinite(seats) ? seats : undefined,
1329
+ planName: claims['org.schema.Offer.itemOffered.name'],
1330
+ sku: claims['org.schema.Offer.itemOffered.sku'],
1331
+ paymentMethod: claims['org.schema.Offer.acceptedPaymentMethod'],
1332
+ checkoutUrl: claims['org.schema.Offer.checkoutPageURLTemplate'],
1333
+ };
1334
+ }
1335
+ /**
1336
+ * Throws when first DIDComm entry contains a business-level error status.
1337
+ */
1338
+ assertFirstDidcommEntrySuccess(result, contextLabel) {
1339
+ const entry = this.getFirstDidcommDataEntryFromResponse(result);
1340
+ const responseStatusRaw = entry?.response?.status;
1341
+ const responseStatus = Number(responseStatusRaw);
1342
+ if (!Number.isFinite(responseStatus) || responseStatus < 400)
1343
+ return;
1344
+ const diagnostics = String(entry?.response?.outcome?.issue?.[0]?.diagnostics
1345
+ || entry?.response?.outcome?.issue?.[0]?.details?.text
1346
+ || '').trim();
1347
+ throw new Error(`${contextLabel} failed (business status=${responseStatus})${diagnostics ? `: ${diagnostics}` : ''}`);
1348
+ }
1017
1349
  /**
1018
1350
  * Activate employee/member device by activation code exchange + DCR registration.
1019
1351
  *
@@ -1054,13 +1386,30 @@ export class DataspaceNodeClient {
1054
1386
  dcr,
1055
1387
  };
1056
1388
  }
1389
+ /**
1390
+ * Friendly wrapper for employee/member device activation.
1391
+ * Uses one object, seconds-based polling, and constructor ctx fallback.
1392
+ */
1393
+ async activateEmployeeDeviceWithActivationCodeSimple(input) {
1394
+ const routeCtx = this.requireRouteContext(input.tenantId && input.jurisdiction && input.sector
1395
+ ? { tenantId: input.tenantId, jurisdiction: input.jurisdiction, sector: input.sector }
1396
+ : undefined);
1397
+ const pollOptions = this.resolveSimplePollOptions(input.timeoutSeconds, input.intervalSeconds);
1398
+ return this.activateEmployeeDeviceWithActivationCode(routeCtx, {
1399
+ activationCode: input.activationCode,
1400
+ idToken: input.idToken,
1401
+ dcrPayload: input.dcrPayload,
1402
+ pollOptions,
1403
+ });
1404
+ }
1057
1405
  /**
1058
1406
  * UC 5.3 wrapper: create organization employee in entity Employee batch route.
1059
1407
  */
1060
1408
  async createOrganizationEmployee(ctx, input, options) {
1409
+ const routeCtx = this.requireRouteContext(ctx);
1061
1410
  const payload = createDidcommPlainMessage({
1062
- iss: ctx.tenantId,
1063
- aud: ctx.tenantId,
1411
+ iss: routeCtx.tenantId,
1412
+ aud: routeCtx.tenantId,
1064
1413
  thid: `employee-${randomUUID()}`,
1065
1414
  body: {
1066
1415
  data: [
@@ -1072,7 +1421,7 @@ export class DataspaceNodeClient {
1072
1421
  ],
1073
1422
  },
1074
1423
  });
1075
- return this.submitAndPoll(this.employeeBatchPath(ctx), this.employeePollPath(ctx), payload, options);
1424
+ return this.submitAndPoll(this.employeeBatchPath(routeCtx), this.employeePollPath(routeCtx), payload, options);
1076
1425
  }
1077
1426
  /**
1078
1427
  * UC 5.1 wrapper: bootstrap subject organization context via registration + optional order confirmation.
@@ -1093,20 +1442,120 @@ export class DataspaceNodeClient {
1093
1442
  const confirmation = await this.submitAndPoll(this.individualFamilyOrderBatchPath(ctx), this.individualFamilyOrderPollPath(ctx), confirmationPayload, input.pollOptions);
1094
1443
  return { registration, confirmation };
1095
1444
  }
1445
+ /**
1446
+ * Friendly wrapper (recommended step 1): register individual organization and return Offer.
1447
+ */
1448
+ async startIndividualOrganizationSimple(input) {
1449
+ const routeCtx = this.requireRouteContext(input.tenantId && input.jurisdiction && input.sector
1450
+ ? { tenantId: input.tenantId, jurisdiction: input.jurisdiction, sector: input.sector }
1451
+ : undefined);
1452
+ const alternateName = String(input.alternateName || '').trim();
1453
+ if (!alternateName) {
1454
+ throw new Error('bootstrapIndividualOrganizationSimple requires alternateName.');
1455
+ }
1456
+ const controllerEmail = String(input.controllerEmail || '').trim();
1457
+ const controllerTelephone = String(input.controllerTelephone || '').trim();
1458
+ if (!controllerEmail && !controllerTelephone) {
1459
+ throw new Error('bootstrapIndividualOrganizationSimple requires controllerEmail or controllerTelephone.');
1460
+ }
1461
+ const controllerRole = String(input.controllerRole || 'org.hl7.v3.RoleCode|RESPRSN').trim();
1462
+ const claims = {
1463
+ '@context': 'org.schema',
1464
+ 'org.schema.Organization.alternateName': alternateName,
1465
+ 'org.schema.Service.category': routeCtx.sector,
1466
+ 'org.schema.Person.hasOccupation': controllerRole,
1467
+ ...(controllerEmail ? { 'org.schema.Person.email': controllerEmail } : {}),
1468
+ ...(controllerTelephone ? { 'org.schema.Person.telephone': controllerTelephone } : {}),
1469
+ ...(input.additionalClaims || {}),
1470
+ };
1471
+ const pollOptions = this.resolveSimplePollOptions(input.timeoutSeconds, input.intervalSeconds);
1472
+ const registrationPayload = createDidcommPlainMessage({
1473
+ iss: routeCtx.tenantId,
1474
+ aud: routeCtx.tenantId,
1475
+ thid: `family-org-${randomUUID()}`,
1476
+ body: {
1477
+ data: [{
1478
+ type: 'SubjectOrg-registration-form-v1.0',
1479
+ meta: { claims },
1480
+ resource: { meta: { claims } },
1481
+ }],
1482
+ },
1483
+ });
1484
+ const registration = await this.submitAndPoll(this.individualFamilyOrganizationBatchPath(routeCtx), this.individualFamilyOrganizationPollPath(routeCtx), registrationPayload, pollOptions);
1485
+ this.assertFirstDidcommEntrySuccess(registration, 'startIndividualOrganizationSimple.registration');
1486
+ const offerId = this.getOfferIdFromResponse(registration);
1487
+ if (!offerId) {
1488
+ throw new Error('startIndividualOrganizationSimple failed: missing offerId in registration response.');
1489
+ }
1490
+ return { registration, offerId, offerPreview: this.getOfferPreviewFromResponse(registration) };
1491
+ }
1492
+ /**
1493
+ * Friendly wrapper (recommended step 2): confirm individual/family order from accepted offerId.
1494
+ */
1495
+ async confirmIndividualOrganizationOrderSimple(input) {
1496
+ const routeCtx = this.requireRouteContext(input.tenantId && input.jurisdiction && input.sector
1497
+ ? { tenantId: input.tenantId, jurisdiction: input.jurisdiction, sector: input.sector }
1498
+ : undefined);
1499
+ const offerId = String(input.offerId || '').trim();
1500
+ if (!offerId) {
1501
+ throw new Error('confirmIndividualOrganizationOrderSimple requires offerId.');
1502
+ }
1503
+ const pollOptions = this.resolveSimplePollOptions(input.timeoutSeconds, input.intervalSeconds);
1504
+ const orderClaims = {
1505
+ '@context': 'org.schema',
1506
+ 'Order.acceptedOffer.identifier': offerId,
1507
+ };
1508
+ const confirmationPayload = createDidcommPlainMessage({
1509
+ iss: routeCtx.tenantId,
1510
+ aud: routeCtx.tenantId,
1511
+ thid: `family-order-${randomUUID()}`,
1512
+ body: {
1513
+ data: [{
1514
+ type: 'Family-order-request-v1.0',
1515
+ meta: { claims: orderClaims },
1516
+ resource: { meta: { claims: orderClaims } },
1517
+ }],
1518
+ },
1519
+ });
1520
+ const confirmation = await this.submitAndPoll(this.individualFamilyOrderBatchPath(routeCtx), this.individualFamilyOrderPollPath(routeCtx), confirmationPayload, pollOptions);
1521
+ this.assertFirstDidcommEntrySuccess(confirmation, 'confirmIndividualOrganizationOrderSimple');
1522
+ return confirmation;
1523
+ }
1524
+ /**
1525
+ * Friendly wrapper (provisional): register + auto-confirm individual order.
1526
+ * Prefer `startIndividualOrganizationSimple` + `confirmIndividualOrganizationOrderSimple`.
1527
+ */
1528
+ async bootstrapIndividualOrganizationSimple(input) {
1529
+ const started = await this.startIndividualOrganizationSimple(input);
1530
+ const confirmation = await this.confirmIndividualOrganizationOrderSimple({
1531
+ tenantId: input.tenantId,
1532
+ jurisdiction: input.jurisdiction,
1533
+ sector: input.sector,
1534
+ offerId: started.offerId,
1535
+ timeoutSeconds: input.timeoutSeconds,
1536
+ intervalSeconds: input.intervalSeconds,
1537
+ });
1538
+ return {
1539
+ registration: started.registration,
1540
+ offerId: started.offerId,
1541
+ confirmation,
1542
+ };
1543
+ }
1096
1544
  /**
1097
1545
  * UC 5.5 wrapper: import IPS/FHIR composition and update subject index context.
1098
1546
  */
1099
1547
  async importIpsOrFhirAndUpdateIndex(ctx, input) {
1548
+ const routeCtx = this.requireRouteContext(ctx);
1100
1549
  const payload = {
1101
1550
  thid: input.compositionPayload.thid || `composition-${randomUUID()}`,
1102
1551
  ...input.compositionPayload,
1103
1552
  };
1104
1553
  const submitPath = (input.format || 'r4') === 'api'
1105
- ? this.individualCompositionR4BatchPath(ctx).replace('/org.hl7.fhir.r4/', '/org.hl7.fhir.api/')
1106
- : this.individualCompositionR4BatchPath(ctx);
1554
+ ? this.individualCompositionR4BatchPath(routeCtx).replace('/org.hl7.fhir.r4/', '/org.hl7.fhir.api/')
1555
+ : this.individualCompositionR4BatchPath(routeCtx);
1107
1556
  const pollPath = (input.format || 'r4') === 'api'
1108
- ? this.individualCompositionR4PollPath(ctx).replace('/org.hl7.fhir.r4/', '/org.hl7.fhir.api/')
1109
- : this.individualCompositionR4PollPath(ctx);
1557
+ ? this.individualCompositionR4PollPath(routeCtx).replace('/org.hl7.fhir.r4/', '/org.hl7.fhir.api/')
1558
+ : this.individualCompositionR4PollPath(routeCtx);
1110
1559
  return this.submitAndPoll(submitPath, pollPath, payload, input.pollOptions);
1111
1560
  }
1112
1561
  /**
@@ -1114,6 +1563,7 @@ export class DataspaceNodeClient {
1114
1563
  * Builds canonical Consent claims and submits/polls the Consent batch.
1115
1564
  */
1116
1565
  async grantProfessionalAccessSimple(ctx, input) {
1566
+ const routeCtx = this.requireRouteContext(ctx);
1117
1567
  const built = buildConsentClaimsSimpleWithCid({
1118
1568
  subjectDid: input.subjectDid,
1119
1569
  subjectPhone: input.subjectPhone,
@@ -1143,7 +1593,7 @@ export class DataspaceNodeClient {
1143
1593
  ],
1144
1594
  },
1145
1595
  };
1146
- const consent = await this.submitAndPoll(this.individualConsentR4BatchPath(ctx), this.individualConsentR4PollPath(ctx), consentPayload, input.pollOptions);
1596
+ const consent = await this.submitAndPoll(this.individualConsentR4BatchPath(routeCtx), this.individualConsentR4PollPath(routeCtx), consentPayload, input.pollOptions);
1147
1597
  return {
1148
1598
  thid,
1149
1599
  consent,
@@ -1157,16 +1607,17 @@ export class DataspaceNodeClient {
1157
1607
  * UC 5.7 wrapper: generate digital twin composition from subject data.
1158
1608
  */
1159
1609
  async generateDigitalTwinFromSubjectData(ctx, input) {
1610
+ const routeCtx = this.requireRouteContext(ctx);
1160
1611
  const payload = {
1161
1612
  thid: input.compositionPayload.thid || `digital-twin-${randomUUID()}`,
1162
1613
  ...input.compositionPayload,
1163
1614
  };
1164
1615
  const submitPath = (input.format || 'r4') === 'api'
1165
- ? this.digitalTwinCompositionApiBatchPath(ctx)
1166
- : this.digitalTwinCompositionR4BatchPath(ctx);
1616
+ ? this.digitalTwinCompositionApiBatchPath(routeCtx)
1617
+ : this.digitalTwinCompositionR4BatchPath(routeCtx);
1167
1618
  const pollPath = (input.format || 'r4') === 'api'
1168
- ? this.digitalTwinCompositionApiPollPath(ctx)
1169
- : this.digitalTwinCompositionR4PollPath(ctx);
1619
+ ? this.digitalTwinCompositionApiPollPath(routeCtx)
1620
+ : this.digitalTwinCompositionR4PollPath(routeCtx);
1170
1621
  return this.submitAndPoll(submitPath, pollPath, payload, input.pollOptions);
1171
1622
  }
1172
1623
  /**
@@ -1196,7 +1647,8 @@ export class DataspaceNodeClient {
1196
1647
  if (Date.now() - startedAt > timeoutMs) {
1197
1648
  throw new Error(`Polling timeout after ${attempts} attempts (${timeoutMs}ms).`);
1198
1649
  }
1199
- await new Promise((resolve) => setTimeout(resolve, intervalMs));
1650
+ const waitMs = options?.intervalMs ?? result.retryAfterMs ?? intervalMs;
1651
+ await new Promise((resolve) => setTimeout(resolve, waitMs));
1200
1652
  }
1201
1653
  }
1202
1654
  // ---- Internal HTTP helpers ---------------------------------------------