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