dataspace-client-sdk-node 0.1.2 → 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 CHANGED
@@ -18,6 +18,10 @@ All notable changes to this project will be documented in this file.
18
18
  - Offer/Order process clarified as part of the onboarding contract even when commercial amount is zero:
19
19
  - Offer extraction for UI review/acceptance
20
20
  - Explicit Order acceptance using offer identifier
21
+ - Activation trust-chain contract aligned with ICA/GW:
22
+ - `_activate` uses `vp_token` in message payload (not as HTTP Bearer),
23
+ - legal representative binding relies on `org.schema.Person.memberOf.taxID` and
24
+ `org.schema.Person.hasCredential.material` for key continuity between `_verify` and `vp_token` signature.
21
25
 
22
26
  ### Documentation
23
27
  - Updated:
@@ -25,4 +29,3 @@ All notable changes to this project will be documented in this file.
25
29
  - Clarified:
26
30
  - `/host` + `auth` alignment with current gateway model
27
31
  - namespace symmetry with ICA (`/ica`) and DataConv (`/publisher`)
28
-
package/README.md CHANGED
@@ -6,6 +6,7 @@ Node.js SDK to consume GW/UNID async DIDComm plain endpoints.
6
6
  **[→ Data Model Alignment (GW + Chat + SDK)](docs/DATA_MODEL_ALIGNMENT.md)**
7
7
  **[→ Frontend/Backend SDK Ownership Matrix](../docs/SDK_AUTH_OWNERSHIP.md)**
8
8
  **[→ Developer Use-Case Cookbook (UC5 + additional patterns)](docs/DEVELOPER_USE_CASES.md)**
9
+ **[→ Live Local GW UC5 E2E (no mocks)](docs/E2E_LOCAL_GW_UC5.md)**
9
10
  **[→ React Web Integration Guide](docs/REACT_WEB_INTEGRATION.md)**
10
11
  **[→ Backend Node Integration Guide](docs/BACKEND_NODE_INTEGRATION.md)**
11
12
  **[→ Portal Backend Integration Handover](docs/PORTAL_BACKEND_INTEGRATION_HANDOVER.md)**
@@ -295,7 +296,9 @@ Full runnable example: `examples/backend-pkce-auth.mjs`
295
296
  - `client.v1Path(ctx, section, format, resourceType, action)` for tenant routes.
296
297
  - `client.hostRegistryPath(ctx, resourceType, action)` for host registry routes.
297
298
  - Then call one of:
298
- - `submitBatch(path, didcommPayload)` for DIDComm plain submit routes,
299
+ - `submitBundle(path, didcommPayload)` for DIDComm bundle submit routes (preferred),
300
+ - `submitBatch(path, didcommPayload)` (legacy alias, kept for compatibility),
301
+ - `submitLegacyJson(path, payload)` for non-DIDComm JSON routes,
299
302
  - `pollBatchResponse(path, { thid })` / `pollUntilComplete(...)`,
300
303
  - `postJson(path, payload)` for non-batch JSON routes.
301
304
 
package/dist/client.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { AsyncPollRequest, BackendPkceAuthOptions, BackendPkceAuthResult, BackendSmartAuthOptions, BackendSmartAuthResult, ClientOptions, CreatePhoneReminderTasksInput, GrantProfessionalAccessSimpleInput, GrantProfessionalAccessSimpleResult, DigitalTwinGenerationInput, EmployeeDeviceActivationInput, EmployeeDeviceActivationSimpleInput, EmployeeDeviceActivationResult, FamilyOrganizationSummary, GatewayOrganizationActivationInput, GatewayOrganizationActivationSimpleInput, HostRouteContext, IpsOrFhirImportInput, MedicationOverlapCheckInput, MedicationRegistrationInput, OrganizationEmployeeCreationInput, PollOptions, PollResult, OfferPreview, RouteContext, SmartTokenExchangeInput, SmartTokenExchangeResult, SmartTokenRequestSimpleInput, LegalOrganizationOrderSimpleInput, SubjectOrganizationBootstrapInput, SubjectOrganizationBootstrapResult, IndividualOrganizationBootstrapSimpleInput, IndividualOrganizationBootstrapSimpleResult, IndividualOrganizationStartSimpleResult, IndividualOrganizationConfirmOrderSimpleInput, SubmitAndPollResult, SubmitResponse, V1Action, V1Section } from './types.js';
1
+ import type { AsyncPollRequest, BackendPkceAuthOptions, BackendPkceAuthResult, BackendSmartAuthOptions, BackendSmartAuthResult, ClientOptions, CreatePhoneReminderTasksInput, GrantProfessionalAccessSimpleInput, GrantProfessionalAccessSimpleResult, DigitalTwinGenerationInput, EmployeeDeviceActivationInput, EndpointSelector, EmployeeDeviceActivationSimpleInput, EmployeeDeviceActivationResult, FamilyOrganizationSummary, GatewayOrganizationActivationInput, GatewayOrganizationActivationSimpleInput, HostRouteContext, IpsOrFhirImportInput, MedicationOverlapCheckInput, MedicationRegistrationInput, OrganizationEmployeeCreationInput, PollOptions, PollResult, OfferPreview, OfferInfo, RouteContext, SmartTokenExchangeInput, SmartTokenExchangeResult, SmartTokenRequestSimpleInput, LegalOrganizationOrderSimpleInput, SubjectOrganizationBootstrapInput, SubjectOrganizationBootstrapResult, IndividualOrganizationBootstrapSimpleInput, IndividualOrganizationBootstrapSimpleResult, IndividualOrganizationStartSimpleResult, IndividualOrganizationConfirmOrderSimpleInput, SubmitAndPollResult, SubmitResponse, V1Action, V1Section } from './types.js';
2
2
  import type { WalletProvider } from './sdk/dataspace-wallet-sdk-node/provider.js';
3
3
  import type { PublicJwk, WalletContext } from './sdk/dataspace-wallet-sdk-node/types.js';
4
4
  export declare class DataspaceNodeClient {
@@ -25,6 +25,14 @@ export declare class DataspaceNodeClient {
25
25
  setSector(sector: string): this;
26
26
  setDefaultTimeoutSeconds(seconds: number): this;
27
27
  setDefaultIntervalSeconds(seconds: number): this;
28
+ /**
29
+ * Builds a deterministic endpoint id for token cache and auth/session reuse.
30
+ * If `providerDid` is provided, returns a full DID service id:
31
+ * did:web:...#section:format:resourceType:action
32
+ * Otherwise returns the canonical fragment without '#':
33
+ * section:format:resourceType:action
34
+ */
35
+ getEndpointId(selector: EndpointSelector, providerDid?: string): string;
28
36
  private resolveSimplePollOptions;
29
37
  private requireRouteContext;
30
38
  private requireHostRouteContext;
@@ -178,7 +186,7 @@ export declare class DataspaceNodeClient {
178
186
  * Returns the cached SMART bearer for the given endpointId if still valid (>30s remaining).
179
187
  * Returns `undefined` if not cached or expired.
180
188
  */
181
- getCachedBearerToken(endpointId: string): string | undefined;
189
+ getCachedBearerToken(tokenCacheKey: string): string | undefined;
182
190
  /**
183
191
  * smart-backend.v1: obtain an OAuth2 backend token using client_credentials + private_key_jwt.
184
192
  */
@@ -200,6 +208,22 @@ export declare class DataspaceNodeClient {
200
208
  private signSmartBackendClientAssertion;
201
209
  private preferredJwtAlg;
202
210
  /**
211
+ * POST a DIDComm bundle payload.
212
+ * This is the preferred high-level method for DIDComm submission of
213
+ * FHIR/API bundles (batch, transaction, message, etc.).
214
+ *
215
+ * Returns immediately with the HTTP response — pair with `pollUntilComplete` or use `submitAndPoll`.
216
+ */
217
+ submitBundle(path: string, payload: {
218
+ thid?: string;
219
+ } & Record<string, unknown>, options?: {
220
+ mode?: 'plain' | 'strict';
221
+ recipientEncryptionJwk?: PublicJwk;
222
+ walletContext?: WalletContext;
223
+ }): Promise<SubmitResponse>;
224
+ /**
225
+ * @deprecated Use `submitBundle` instead.
226
+ *
203
227
  * POST a DIDComm plaintext payload to a batch submit path.
204
228
  * Use this for all `_batch` routes (family registration, observations, tasks, etc.).
205
229
  * Content-Type: `application/didcomm-plaintext+json`.
@@ -225,6 +249,11 @@ export declare class DataspaceNodeClient {
225
249
  * Content-Type: `application/json`.
226
250
  */
227
251
  postJson(path: string, payload: unknown): Promise<SubmitResponse>;
252
+ /**
253
+ * Legacy JSON submit for non-bundle payloads (openid/token/resource JSON bodies).
254
+ * Keeps JSON flows explicit and semantically separated from DIDComm bundle flows.
255
+ */
256
+ submitLegacyJson(path: string, payload: unknown): Promise<SubmitResponse>;
228
257
  /**
229
258
  * POST a multipart/form-data payload.
230
259
  * Use for file upload endpoints. Prefer `uploadConversionFile` for DataConversion uploads.
@@ -350,6 +379,15 @@ export declare class DataspaceNodeClient {
350
379
  * Extract a UI-ready Offer preview from activation/registration responses.
351
380
  */
352
381
  getOfferPreviewFromResponse(result: SubmitAndPollResult | PollResult | unknown): OfferPreview;
382
+ /**
383
+ * Alias of `getOfferPreviewFromResponse` with business naming.
384
+ */
385
+ getOfferInfoFromResponse(result: SubmitAndPollResult | PollResult | unknown): OfferInfo;
386
+ /**
387
+ * Extract activation code from response payload or claims.
388
+ * Supports common response shapes used in onboarding and license issuance flows.
389
+ */
390
+ getActivationCodeFromResponse(result: SubmitAndPollResult | PollResult | unknown): string | undefined;
353
391
  /**
354
392
  * Throws when first DIDComm entry contains a business-level error status.
355
393
  */
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(/\/+$/, '');
@@ -99,6 +102,19 @@ export class DataspaceNodeClient {
99
102
  }
100
103
  return this;
101
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
+ }
102
118
  resolveSimplePollOptions(timeoutSeconds, intervalSeconds) {
103
119
  const pollOptions = {};
104
120
  if (Number.isFinite(Number(timeoutSeconds))) {
@@ -386,10 +402,11 @@ export class DataspaceNodeClient {
386
402
  * Results are cached in memory; re-runs automatically on expiry.
387
403
  */
388
404
  async authenticateBackendPkceAndExchange(options) {
389
- const { ctx, apiKey, scopes, endpointId = `pkce:${apiKey.slice(0, 8)}`, codeVerifier = randomUUID(), pollOptions, } = options;
390
- const cached = this._tokenCache.get(endpointId);
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);
391
408
  if (cached && cached.expiresAt > Date.now() + 30_000) {
392
- 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 };
393
410
  }
394
411
  const controllerPublicJwk = await this.resolveControllerPublicJwk(options);
395
412
  // Step 1: DCR – bind API key to service public key
@@ -402,7 +419,7 @@ export class DataspaceNodeClient {
402
419
  await this.submitBatch(this.identityDeviceDcrPath(ctx), dcrPayload);
403
420
  const dcrPoll = await this.pollUntilComplete(this.identityDeviceDcrPollPath(ctx), { thid: String(dcrPayload['thid']) }, pollOptions);
404
421
  if (dcrPoll.status !== 200) {
405
- return { status: 'failed', step: '_dcr', endpointId, accessToken: '', tokenType: 'Bearer', scopes };
422
+ return { status: 'failed', step: '_dcr', tokenCacheKey: cacheKey, endpointId: cacheKey, accessToken: '', tokenType: 'Bearer', scopes };
406
423
  }
407
424
  // Step 2: Code – PKCE S256 challenge
408
425
  const codeChallenge = this._pkceS256Challenge(codeVerifier);
@@ -418,7 +435,7 @@ export class DataspaceNodeClient {
418
435
  const codeBody = codePoll.body ?? {};
419
436
  const code = String(codeBody['code'] ?? '').trim();
420
437
  if (codePoll.status !== 200 || !code) {
421
- return { status: 'failed', step: '_code', endpointId, accessToken: '', tokenType: 'Bearer', scopes };
438
+ return { status: 'failed', step: '_code', tokenCacheKey: cacheKey, endpointId: cacheKey, accessToken: '', tokenType: 'Bearer', scopes };
422
439
  }
423
440
  // Step 3: Token – exchange code + verifier for id_token
424
441
  const tokenPayload = this._buildAuthDIDCommRequest({
@@ -433,7 +450,7 @@ export class DataspaceNodeClient {
433
450
  const tokenBody = tokenPoll.body ?? {};
434
451
  const idToken = String(tokenBody['id_token'] ?? '').trim();
435
452
  if (tokenPoll.status !== 200 || !idToken) {
436
- return { status: 'failed', step: '_token', endpointId, accessToken: '', tokenType: 'Bearer', scopes };
453
+ return { status: 'failed', step: '_token', tokenCacheKey: cacheKey, endpointId: cacheKey, accessToken: '', tokenType: 'Bearer', scopes };
437
454
  }
438
455
  // Step 4: Exchange – id_token → SMART bearer
439
456
  const exchangeThid = `exchange-${randomUUID()}`;
@@ -451,26 +468,26 @@ export class DataspaceNodeClient {
451
468
  const exchangeBody = exchangePoll.body ?? {};
452
469
  const accessToken = String(exchangeBody['access_token'] ?? '').trim();
453
470
  if (exchangePoll.status !== 200 || !accessToken) {
454
- return { status: 'failed', step: '_exchange', endpointId, accessToken: '', tokenType: 'Bearer', scopes };
471
+ return { status: 'failed', step: '_exchange', tokenCacheKey: cacheKey, endpointId: cacheKey, accessToken: '', tokenType: 'Bearer', scopes };
455
472
  }
456
473
  const tokenType = String(exchangeBody['token_type'] ?? 'Bearer');
457
474
  const grantedScope = String(exchangeBody['scope'] ?? '').trim();
458
475
  const grantedScopes = grantedScope ? grantedScope.split(' ').filter(Boolean) : scopes;
459
476
  const expiresIn = Number(exchangeBody['expires_in'] ?? 0);
460
- this._tokenCache.set(endpointId, {
477
+ this._tokenCache.set(cacheKey, {
461
478
  accessToken,
462
479
  tokenType,
463
480
  scopes: grantedScopes,
464
481
  expiresAt: Date.now() + expiresIn * 1000,
465
482
  });
466
- return { status: 'fetched', endpointId, accessToken, tokenType, scopes: grantedScopes };
483
+ return { status: 'fetched', tokenCacheKey: cacheKey, endpointId: cacheKey, accessToken, tokenType, scopes: grantedScopes };
467
484
  }
468
485
  /**
469
486
  * Returns the cached SMART bearer for the given endpointId if still valid (>30s remaining).
470
487
  * Returns `undefined` if not cached or expired.
471
488
  */
472
- getCachedBearerToken(endpointId) {
473
- const cached = this._tokenCache.get(endpointId);
489
+ getCachedBearerToken(tokenCacheKey) {
490
+ const cached = this._tokenCache.get(tokenCacheKey);
474
491
  if (cached && cached.expiresAt > Date.now() + 30_000) {
475
492
  return cached.accessToken;
476
493
  }
@@ -480,13 +497,15 @@ export class DataspaceNodeClient {
480
497
  * smart-backend.v1: obtain an OAuth2 backend token using client_credentials + private_key_jwt.
481
498
  */
482
499
  async authenticateBackendSmartStandard(options) {
483
- const { clientId, scopes, endpointId = `smart-backend:${clientId}`, tokenUrl, tokenPath = '/token', audience, assertionTtlSeconds = 300, additionalTokenFields, } = options;
484
- const cached = this._tokenCache.get(endpointId);
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);
485
503
  if (cached && cached.expiresAt > Date.now() + 30_000) {
486
504
  return {
487
505
  status: 'cached',
488
506
  profile: 'smart-backend.v1',
489
- endpointId,
507
+ tokenCacheKey: cacheKey,
508
+ endpointId: cacheKey,
490
509
  accessToken: cached.accessToken,
491
510
  tokenType: cached.tokenType,
492
511
  scopes: cached.scopes,
@@ -517,7 +536,8 @@ export class DataspaceNodeClient {
517
536
  return {
518
537
  status: 'failed',
519
538
  profile: 'smart-backend.v1',
520
- endpointId,
539
+ tokenCacheKey: cacheKey,
540
+ endpointId: cacheKey,
521
541
  statusCode: response.status,
522
542
  response,
523
543
  };
@@ -527,7 +547,7 @@ export class DataspaceNodeClient {
527
547
  const grantedScopes = grantedScope ? grantedScope.split(' ').filter(Boolean) : scopes;
528
548
  const expiresIn = Number(body.expires_in ?? 0);
529
549
  const expiresAt = Date.now() + expiresIn * 1000;
530
- this._tokenCache.set(endpointId, {
550
+ this._tokenCache.set(cacheKey, {
531
551
  accessToken,
532
552
  tokenType,
533
553
  scopes: grantedScopes,
@@ -536,7 +556,8 @@ export class DataspaceNodeClient {
536
556
  return {
537
557
  status: 'fetched',
538
558
  profile: 'smart-backend.v1',
539
- endpointId,
559
+ tokenCacheKey: cacheKey,
560
+ endpointId: cacheKey,
540
561
  statusCode: response.status,
541
562
  accessToken,
542
563
  tokenType,
@@ -549,12 +570,12 @@ export class DataspaceNodeClient {
549
570
  * Exchange token payload against gateway token endpoint and cache the result.
550
571
  */
551
572
  async requestSmartToken(input) {
552
- const endpointId = String(input.endpointId || '').trim();
553
- if (!endpointId) {
554
- throw new Error('requestSmartToken requires endpointId.');
573
+ const tokenCacheKey = String(input.tokenCacheKey || input.endpointId || '').trim();
574
+ if (!tokenCacheKey) {
575
+ throw new Error('requestSmartToken requires tokenCacheKey.');
555
576
  }
556
577
  const normalizedScopes = Array.from(new Set((input.scopes || []).filter(Boolean))).sort();
557
- const cached = this._tokenCache.get(endpointId);
578
+ const cached = this._tokenCache.get(tokenCacheKey);
558
579
  if (cached && cached.expiresAt > Date.now() + 30_000) {
559
580
  return {
560
581
  status: 'cached',
@@ -579,7 +600,7 @@ export class DataspaceNodeClient {
579
600
  : String(body.scope ?? '').trim().split(' ').filter(Boolean);
580
601
  const resolvedScopes = grantedScopes.length ? grantedScopes : normalizedScopes;
581
602
  const expiresIn = Number(body.expires_in ?? 0);
582
- this._tokenCache.set(endpointId, {
603
+ this._tokenCache.set(tokenCacheKey, {
583
604
  accessToken,
584
605
  tokenType,
585
606
  scopes: resolvedScopes,
@@ -603,9 +624,9 @@ export class DataspaceNodeClient {
603
624
  ? { tenantId: input.tenantId, jurisdiction: input.jurisdiction, sector: input.sector }
604
625
  : undefined);
605
626
  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).');
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).');
609
630
  }
610
631
  const pollOptions = this.resolveSimplePollOptions(input.timeoutSeconds, input.intervalSeconds);
611
632
  const payload = {
@@ -631,7 +652,7 @@ export class DataspaceNodeClient {
631
652
  const grantedScopes = String(exchangeBody.scope || '').trim().split(' ').filter(Boolean);
632
653
  const resolvedScopes = grantedScopes.length ? grantedScopes : normalizedScopes;
633
654
  const expiresIn = Number(exchangeBody.expires_in ?? 0);
634
- this._tokenCache.set(endpointId, {
655
+ this._tokenCache.set(tokenCacheKey, {
635
656
  accessToken,
636
657
  tokenType,
637
658
  scopes: resolvedScopes,
@@ -762,6 +783,25 @@ export class DataspaceNodeClient {
762
783
  }
763
784
  // ---- Generic batch API --------------------------------------------------
764
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
+ *
765
805
  * POST a DIDComm plaintext payload to a batch submit path.
766
806
  * Use this for all `_batch` routes (family registration, observations, tasks, etc.).
767
807
  * Content-Type: `application/didcomm-plaintext+json`.
@@ -769,13 +809,18 @@ export class DataspaceNodeClient {
769
809
  * Returns immediately with the HTTP response — pair with `pollUntilComplete` or use `submitAndPoll`.
770
810
  */
771
811
  async submitBatch(path, payload) {
772
- const response = await this.doPost(path, payload, 'application/didcomm-plaintext+json');
773
- const body = await this.parseResponseBody(response);
774
- return {
775
- status: response.status,
776
- location: response.headers.get('location') ?? undefined,
777
- body,
778
- };
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 };
779
824
  }
780
825
  /**
781
826
  * Sign and encrypt a DIDComm payload (nested JWS-in-JWE) and POST to the given path.
@@ -792,40 +837,32 @@ export class DataspaceNodeClient {
792
837
  }
793
838
  const publicJwks = await this.wallet.getPublicJwks(walletContext);
794
839
  const signingJwk = publicJwks.find((jwk) => jwk.use === 'sig' || jwk.alg === 'ES384') ?? publicJwks[0];
795
- // Step 1: sign payload claims as compact JWS
796
- const compactJws = await this.wallet.signCompactJws(walletContext, {
797
- header: {
798
- typ: 'application/didcomm-signed+json',
799
- alg: 'ES384',
800
- ...(signingJwk?.kid ? { kid: signingJwk.kid } : {}),
801
- },
802
- claims: payload,
803
- });
804
- // Step 2: encrypt JWS string as compact JWE (RSA-OAEP-256 + A256GCM, cty: JWS)
805
- const compactJwe = await this.wallet.buildCompactJwe(walletContext, {
806
- plaintext: compactJws,
807
- recipientJwk: recipientEncryptionJwk,
808
- contentType: 'JWS',
809
- });
810
- // Step 3: POST the JWE token directly (not JSON-serialised)
811
840
  const url = /^https?:\/\//.test(path)
812
841
  ? path
813
842
  : `${this.baseUrl}${path.startsWith('/') ? path : `/${path}`}`;
814
- const headers = {
815
- ...this.defaultHeaders,
816
- 'Content-Type': 'application/didcomm-encrypted+json',
817
- Accept: 'application/json, application/didcomm-plaintext+json, */*',
818
- };
819
- if (this.bearerToken) {
820
- headers.Authorization = `Bearer ${this.bearerToken}`;
821
- }
822
- const response = await fetch(url, { method: 'POST', headers, body: compactJwe });
823
- const body = await this.parseResponseBody(response);
824
- return {
825
- status: response.status,
826
- location: response.headers.get('location') ?? undefined,
827
- body,
828
- };
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 };
829
866
  }
830
867
  /**
831
868
  * POST a plain JSON payload.
@@ -841,6 +878,13 @@ export class DataspaceNodeClient {
841
878
  body,
842
879
  };
843
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
+ }
844
888
  /**
845
889
  * POST a multipart/form-data payload.
846
890
  * Use for file upload endpoints. Prefer `uploadConversionFile` for DataConversion uploads.
@@ -1223,9 +1267,9 @@ export class DataspaceNodeClient {
1223
1267
  'org.schema.Service.category': hostCtx.sector,
1224
1268
  'org.schema.Service.identifier': resolvedServiceDid,
1225
1269
  ...(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 } : {}),
1270
+ [ClaimsPersonSchemaorg.hasOccupation]: controllerRole,
1271
+ ...(controllerEmail ? { [ClaimsPersonSchemaorg.email]: controllerEmail } : {}),
1272
+ ...(controllerTelephone ? { [ClaimsPersonSchemaorg.telephone]: controllerTelephone } : {}),
1229
1273
  };
1230
1274
  const activation = await this.activateOrganizationInGatewayFromIcaProof(hostCtx, {
1231
1275
  vpToken: input.vpToken,
@@ -1306,8 +1350,8 @@ export class DataspaceNodeClient {
1306
1350
  */
1307
1351
  getOfferIdFromResponse(result) {
1308
1352
  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']
1353
+ const offerId = String(entry?.meta?.claims?.[ClaimsOfferSchemaorg.identifier]
1354
+ || entry?.resource?.meta?.claims?.[ClaimsOfferSchemaorg.identifier]
1311
1355
  || '').trim();
1312
1356
  return offerId || undefined;
1313
1357
  }
@@ -1317,21 +1361,44 @@ export class DataspaceNodeClient {
1317
1361
  getOfferPreviewFromResponse(result) {
1318
1362
  const entry = this.getFirstDidcommDataEntryFromResponse(result);
1319
1363
  const claims = entry?.meta?.claims || entry?.resource?.meta?.claims || {};
1320
- const seatsRaw = claims['org.schema.Offer.eligibleQuantity.value'];
1364
+ const seatsRaw = claims[ClaimsOfferSchemaorg.eligibleQuantityValue];
1321
1365
  const seats = typeof seatsRaw === 'number'
1322
1366
  ? seatsRaw
1323
1367
  : (typeof seatsRaw === 'string' && seatsRaw.trim() ? Number(seatsRaw) : undefined);
1324
1368
  return {
1325
1369
  offerId: this.getOfferIdFromResponse(result),
1326
- amount: claims['org.schema.Offer.price'],
1327
- currency: claims['org.schema.Offer.priceCurrency'],
1370
+ amount: claims[ClaimsOfferSchemaorg.price],
1371
+ currency: claims[ClaimsOfferSchemaorg.priceCurrency],
1328
1372
  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'],
1373
+ planName: claims[ClaimsOfferSchemaorg.itemOfferedName],
1374
+ sku: claims[ClaimsOfferSchemaorg.itemOfferedSku],
1375
+ paymentMethod: claims[ClaimsOfferSchemaorg.acceptedPaymentMethod],
1376
+ checkoutUrl: claims[ClaimsOfferSchemaorg.checkoutPageURLTemplate],
1333
1377
  };
1334
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
+ }
1335
1402
  /**
1336
1403
  * Throws when first DIDComm entry contains a business-level error status.
1337
1404
  */
@@ -1463,9 +1530,9 @@ export class DataspaceNodeClient {
1463
1530
  '@context': 'org.schema',
1464
1531
  'org.schema.Organization.alternateName': alternateName,
1465
1532
  '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 } : {}),
1533
+ [ClaimsPersonSchemaorg.hasOccupation]: controllerRole,
1534
+ ...(controllerEmail ? { [ClaimsPersonSchemaorg.email]: controllerEmail } : {}),
1535
+ ...(controllerTelephone ? { [ClaimsPersonSchemaorg.telephone]: controllerTelephone } : {}),
1469
1536
  ...(input.additionalClaims || {}),
1470
1537
  };
1471
1538
  const pollOptions = this.resolveSimplePollOptions(input.timeoutSeconds, input.intervalSeconds);
@@ -1672,10 +1739,17 @@ export class DataspaceNodeClient {
1672
1739
  }
1673
1740
  async parseResponseBody(response) {
1674
1741
  const contentType = response.headers.get('content-type') || '';
1742
+ const raw = await response.text();
1743
+ if (!raw)
1744
+ return {};
1675
1745
  if (contentType.includes('application/json') || contentType.includes('application/didcomm-plaintext+json')) {
1676
- return response.json().catch(() => ({}));
1746
+ try {
1747
+ return JSON.parse(raw);
1748
+ }
1749
+ catch {
1750
+ return {};
1751
+ }
1677
1752
  }
1678
- const text = await response.text();
1679
- return text;
1753
+ return raw;
1680
1754
  }
1681
1755
  }
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from './types.js';
2
2
  export * from './builders.js';
3
+ export * from './vp-token.js';
3
4
  export * from './client.js';
4
5
  export * from './sdk/dataspace-wallet-sdk-node/index.js';
package/dist/index.js CHANGED
@@ -4,5 +4,6 @@
4
4
  // See subjectVaultPhoneResolution.test.ts for context and migration plan.
5
5
  export * from './types.js';
6
6
  export * from './builders.js';
7
+ export * from './vp-token.js';
7
8
  export * from './client.js';
8
9
  export * from './sdk/dataspace-wallet-sdk-node/index.js';
package/dist/types.d.ts CHANGED
@@ -168,6 +168,13 @@ export type OfferPreview = {
168
168
  paymentMethod?: string;
169
169
  checkoutUrl?: string;
170
170
  };
171
+ export type OfferInfo = OfferPreview;
172
+ export type EndpointSelector = {
173
+ section: string;
174
+ format: string;
175
+ resourceType: string;
176
+ action: string;
177
+ };
171
178
  /**
172
179
  * Input for organization activation in GW using ICA-derived proof material.
173
180
  *
@@ -471,6 +478,8 @@ export type BackendPkceAuthOptions = {
471
478
  /** Requested scopes for the SMART bearer token. */
472
479
  scopes: string[];
473
480
  /** Cache key for the resulting bearer token. Defaults to `pkce:<apiKey prefix>`. */
481
+ tokenCacheKey?: string;
482
+ /** @deprecated Use `tokenCacheKey`. */
474
483
  endpointId?: string;
475
484
  /** PKCE code verifier. Auto-generated with randomUUID if not provided. */
476
485
  codeVerifier?: string;
@@ -480,6 +489,8 @@ export type BackendPkceAuthOptions = {
480
489
  export type BackendPkceAuthResult = {
481
490
  /** `fetched`: new token obtained. `cached`: valid token already in cache. `failed`: flow error. */
482
491
  status: 'fetched' | 'cached' | 'failed';
492
+ tokenCacheKey: string;
493
+ /** @deprecated Use `tokenCacheKey`. */
483
494
  endpointId: string;
484
495
  accessToken: string;
485
496
  tokenType: string;
@@ -490,6 +501,8 @@ export type BackendPkceAuthResult = {
490
501
  export type BackendSmartAuthOptions = {
491
502
  clientId: string;
492
503
  scopes: string[];
504
+ tokenCacheKey?: string;
505
+ /** @deprecated Use `tokenCacheKey`. */
493
506
  endpointId?: string;
494
507
  tokenUrl?: string;
495
508
  tokenPath?: string;
@@ -502,6 +515,8 @@ export type BackendSmartAuthOptions = {
502
515
  export type BackendSmartAuthResult = {
503
516
  status: 'fetched' | 'cached' | 'failed';
504
517
  profile: 'smart-backend.v1';
518
+ tokenCacheKey: string;
519
+ /** @deprecated Use `tokenCacheKey`. */
505
520
  endpointId: string;
506
521
  accessToken?: string;
507
522
  tokenType?: string;
@@ -511,7 +526,9 @@ export type BackendSmartAuthResult = {
511
526
  response?: unknown;
512
527
  };
513
528
  export type SmartTokenExchangeInput = {
514
- endpointId: string;
529
+ tokenCacheKey: string;
530
+ /** @deprecated Use `tokenCacheKey`. */
531
+ endpointId?: string;
515
532
  scopes: string[];
516
533
  exchangePayload: Record<string, unknown>;
517
534
  path?: string;
@@ -522,6 +539,8 @@ export type SmartTokenRequestSimpleInput = {
522
539
  sector?: string;
523
540
  idToken: string;
524
541
  scopes: string[];
542
+ tokenCacheKey?: string;
543
+ /** @deprecated Use `tokenCacheKey`. */
525
544
  endpointId?: string;
526
545
  timeoutSeconds?: number;
527
546
  intervalSeconds?: number;