dataspace-client-sdk-node 0.1.2 → 0.2.1

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
@@ -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;
@@ -0,0 +1,37 @@
1
+ export type VpTokenHeader = {
2
+ alg: string;
3
+ typ?: string;
4
+ kid?: string;
5
+ [key: string]: unknown;
6
+ };
7
+ export type VpTokenPayload = {
8
+ iss: string;
9
+ sub?: string;
10
+ aud?: string;
11
+ jti?: string;
12
+ iat?: number;
13
+ exp?: number;
14
+ nonce?: string;
15
+ vp: {
16
+ '@context'?: unknown;
17
+ type?: unknown;
18
+ holder?: string;
19
+ verifiableCredential: string[];
20
+ [key: string]: unknown;
21
+ };
22
+ [key: string]: unknown;
23
+ };
24
+ export declare function generateUuidLike(): string;
25
+ export declare function buildEpochWindow(ttlSeconds?: number): {
26
+ iat: number;
27
+ exp: number;
28
+ };
29
+ export declare function createVP(input?: Partial<VpTokenPayload>): VpTokenPayload;
30
+ export declare function addVC(vpPayload: VpTokenPayload, vcJwt: string): VpTokenPayload;
31
+ export declare function prepareForSignature(header: VpTokenHeader, payload: VpTokenPayload): {
32
+ encodedHeader: string;
33
+ encodedPayload: string;
34
+ signingInput: string;
35
+ };
36
+ export declare function prepareBytesForSignature(header: VpTokenHeader, payload: VpTokenPayload): Uint8Array;
37
+ export declare function buildVpTokenCompact(encodedHeader: string, encodedPayload: string, signatureBase64Url: string): string;
@@ -0,0 +1,56 @@
1
+ function toB64UrlJson(input) {
2
+ return Buffer.from(JSON.stringify(input), 'utf-8').toString('base64url');
3
+ }
4
+ function fallbackId() {
5
+ const rand = Math.random().toString(36).slice(2, 10);
6
+ return `id-${Date.now()}-${rand}`;
7
+ }
8
+ export function generateUuidLike() {
9
+ const fn = globalThis?.crypto?.randomUUID;
10
+ if (typeof fn === 'function')
11
+ return fn.call(globalThis.crypto);
12
+ return fallbackId();
13
+ }
14
+ export function buildEpochWindow(ttlSeconds = 300) {
15
+ const iat = Math.floor(Date.now() / 1000);
16
+ return { iat, exp: iat + Math.max(1, Math.floor(ttlSeconds)) };
17
+ }
18
+ export function createVP(input) {
19
+ const ttl = input?.exp && input?.iat ? undefined : buildEpochWindow(300);
20
+ const jti = input?.jti || generateUuidLike();
21
+ const nonce = input?.nonce || generateUuidLike();
22
+ return {
23
+ iss: String(input?.iss || ''),
24
+ sub: input?.sub,
25
+ aud: input?.aud,
26
+ jti,
27
+ iat: input?.iat ?? ttl?.iat,
28
+ exp: input?.exp ?? ttl?.exp,
29
+ nonce,
30
+ vp: {
31
+ '@context': ['https://www.w3.org/2018/credentials/v1'],
32
+ type: ['VerifiablePresentation'],
33
+ holder: input?.vp?.holder || input?.iss || '',
34
+ verifiableCredential: [],
35
+ ...(input?.vp || {}),
36
+ },
37
+ };
38
+ }
39
+ export function addVC(vpPayload, vcJwt) {
40
+ const v = String(vcJwt || '').trim();
41
+ if (v)
42
+ vpPayload.vp.verifiableCredential.push(v);
43
+ return vpPayload;
44
+ }
45
+ export function prepareForSignature(header, payload) {
46
+ const encodedHeader = toB64UrlJson(header);
47
+ const encodedPayload = toB64UrlJson(payload);
48
+ return { encodedHeader, encodedPayload, signingInput: `${encodedHeader}.${encodedPayload}` };
49
+ }
50
+ export function prepareBytesForSignature(header, payload) {
51
+ const { signingInput } = prepareForSignature(header, payload);
52
+ return new TextEncoder().encode(signingInput);
53
+ }
54
+ export function buildVpTokenCompact(encodedHeader, encodedPayload, signatureBase64Url) {
55
+ return `${encodedHeader}.${encodedPayload}.${String(signatureBase64Url || '').trim()}`;
56
+ }
package/docs/API.md CHANGED
@@ -478,22 +478,33 @@ const token = client.getCachedBearerToken('pkce:my-api-key');
478
478
 
479
479
  ## Transport methods
480
480
 
481
- ### `submitBatch`
481
+ ### `submitBundle`
482
482
 
483
- POST a DIDComm plaintext payload (`application/didcomm-plaintext+json`).
483
+ POST a DIDComm bundle payload. Preferred API for FHIR/API bundle operations.
484
484
 
485
485
  ```ts
486
- submitBatch(path: string, payload: unknown): Promise<SubmitResponse>
486
+ submitBundle(
487
+ path: string,
488
+ payload: { thid?: string } & Record<string, unknown>,
489
+ options?: {
490
+ mode?: 'plain' | 'strict';
491
+ recipientEncryptionJwk?: PublicJwk;
492
+ walletContext?: WalletContext;
493
+ },
494
+ ): Promise<SubmitResponse>
487
495
  ```
488
496
 
489
497
  ```ts
490
- const { status, location } = await client.submitBatch(
498
+ const { status, location } = await client.submitBundle(
491
499
  client.individualTaskBatchPath(ctx),
492
500
  payload,
501
+ { mode: 'plain' },
493
502
  );
494
503
  // location header contains the poll URL
495
504
  ```
496
505
 
506
+ `submitBatch(...)` remains available as a legacy alias for compatibility.
507
+
497
508
  ---
498
509
 
499
510
  ### `submitBatchEncrypted`
@@ -531,6 +542,10 @@ POST a plain JSON body (`application/json`). Use for token endpoints and API key
531
542
  postJson(path: string, payload: unknown): Promise<SubmitResponse>
532
543
  ```
533
544
 
545
+ ### `submitLegacyJson`
546
+
547
+ Alias of `postJson(...)` for explicit non-bundle JSON submits (openid/token/resource payloads).
548
+
534
549
  ```ts
535
550
  const response = await client.postJson('/host/admin/api-keys', {
536
551
  agent: { email: 'service@example.com' },