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/src/client.ts 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, type DidcommFetchInit } from 'gdc-common-utils-ts/utils/didcomm-submit';
6
+ import { ClaimsOfferSchemaorg, ClaimsPersonSchemaorg } from 'gdc-common-utils-ts/constants/schemaorg';
4
7
  import {
5
8
  MedicationStatementClaimsFhirApi,
6
9
  MedicationStatementClaimsFhirApiExtended,
@@ -17,6 +20,7 @@ import type {
17
20
  GrantProfessionalAccessSimpleResult,
18
21
  DigitalTwinGenerationInput,
19
22
  EmployeeDeviceActivationInput,
23
+ EndpointSelector,
20
24
  EmployeeDeviceActivationSimpleInput,
21
25
  EmployeeDeviceActivationResult,
22
26
  FamilyOrganizationSummary,
@@ -31,6 +35,7 @@ import type {
31
35
  PollOptions,
32
36
  PollResult,
33
37
  OfferPreview,
38
+ OfferInfo,
34
39
  RouteContext,
35
40
  SmartTokenExchangeInput,
36
41
  SmartTokenExchangeResult,
@@ -163,6 +168,19 @@ export class DataspaceNodeClient {
163
168
  return this;
164
169
  }
165
170
 
171
+ /**
172
+ * Builds a deterministic endpoint id for token cache and auth/session reuse.
173
+ * If `providerDid` is provided, returns a full DID service id:
174
+ * did:web:...#section:format:resourceType:action
175
+ * Otherwise returns the canonical fragment without '#':
176
+ * section:format:resourceType:action
177
+ */
178
+ public getEndpointId(selector: EndpointSelector, providerDid?: string): string {
179
+ const fragment = generateServiceId(selector); // #section:format:resourceType:action
180
+ if (providerDid) return `${providerDid}${fragment}`;
181
+ return fragment.replace(/^#/, '');
182
+ }
183
+
166
184
  private resolveSimplePollOptions(timeoutSeconds?: number, intervalSeconds?: number): PollOptions | undefined {
167
185
  const pollOptions: PollOptions = {};
168
186
  if (Number.isFinite(Number(timeoutSeconds))) {
@@ -518,14 +536,16 @@ export class DataspaceNodeClient {
518
536
  ctx,
519
537
  apiKey,
520
538
  scopes,
521
- endpointId = `pkce:${apiKey.slice(0, 8)}`,
539
+ tokenCacheKey = `pkce:${apiKey.slice(0, 8)}`,
540
+ endpointId,
522
541
  codeVerifier = randomUUID(),
523
542
  pollOptions,
524
543
  } = options;
525
544
 
526
- const cached = this._tokenCache.get(endpointId);
545
+ const cacheKey = String(tokenCacheKey || endpointId || '').trim() || `pkce:${apiKey.slice(0, 8)}`;
546
+ const cached = this._tokenCache.get(cacheKey);
527
547
  if (cached && cached.expiresAt > Date.now() + 30_000) {
528
- return { status: 'cached', endpointId, accessToken: cached.accessToken, tokenType: cached.tokenType, scopes: cached.scopes };
548
+ return { status: 'cached', tokenCacheKey: cacheKey, endpointId: cacheKey, accessToken: cached.accessToken, tokenType: cached.tokenType, scopes: cached.scopes };
529
549
  }
530
550
 
531
551
  const controllerPublicJwk = await this.resolveControllerPublicJwk(options);
@@ -544,7 +564,7 @@ export class DataspaceNodeClient {
544
564
  pollOptions,
545
565
  );
546
566
  if (dcrPoll.status !== 200) {
547
- return { status: 'failed', step: '_dcr', endpointId, accessToken: '', tokenType: 'Bearer', scopes };
567
+ return { status: 'failed', step: '_dcr', tokenCacheKey: cacheKey, endpointId: cacheKey, accessToken: '', tokenType: 'Bearer', scopes };
548
568
  }
549
569
 
550
570
  // Step 2: Code – PKCE S256 challenge
@@ -565,7 +585,7 @@ export class DataspaceNodeClient {
565
585
  const codeBody = (codePoll.body as Record<string, unknown>) ?? {};
566
586
  const code = String(codeBody['code'] ?? '').trim();
567
587
  if (codePoll.status !== 200 || !code) {
568
- return { status: 'failed', step: '_code', endpointId, accessToken: '', tokenType: 'Bearer', scopes };
588
+ return { status: 'failed', step: '_code', tokenCacheKey: cacheKey, endpointId: cacheKey, accessToken: '', tokenType: 'Bearer', scopes };
569
589
  }
570
590
 
571
591
  // Step 3: Token – exchange code + verifier for id_token
@@ -585,7 +605,7 @@ export class DataspaceNodeClient {
585
605
  const tokenBody = (tokenPoll.body as Record<string, unknown>) ?? {};
586
606
  const idToken = String(tokenBody['id_token'] ?? '').trim();
587
607
  if (tokenPoll.status !== 200 || !idToken) {
588
- return { status: 'failed', step: '_token', endpointId, accessToken: '', tokenType: 'Bearer', scopes };
608
+ return { status: 'failed', step: '_token', tokenCacheKey: cacheKey, endpointId: cacheKey, accessToken: '', tokenType: 'Bearer', scopes };
589
609
  }
590
610
 
591
611
  // Step 4: Exchange – id_token → SMART bearer
@@ -608,7 +628,7 @@ export class DataspaceNodeClient {
608
628
  const exchangeBody = (exchangePoll.body as Record<string, unknown>) ?? {};
609
629
  const accessToken = String(exchangeBody['access_token'] ?? '').trim();
610
630
  if (exchangePoll.status !== 200 || !accessToken) {
611
- return { status: 'failed', step: '_exchange', endpointId, accessToken: '', tokenType: 'Bearer', scopes };
631
+ return { status: 'failed', step: '_exchange', tokenCacheKey: cacheKey, endpointId: cacheKey, accessToken: '', tokenType: 'Bearer', scopes };
612
632
  }
613
633
 
614
634
  const tokenType = String(exchangeBody['token_type'] ?? 'Bearer');
@@ -616,22 +636,22 @@ export class DataspaceNodeClient {
616
636
  const grantedScopes = grantedScope ? grantedScope.split(' ').filter(Boolean) : scopes;
617
637
  const expiresIn = Number(exchangeBody['expires_in'] ?? 0);
618
638
 
619
- this._tokenCache.set(endpointId, {
639
+ this._tokenCache.set(cacheKey, {
620
640
  accessToken,
621
641
  tokenType,
622
642
  scopes: grantedScopes,
623
643
  expiresAt: Date.now() + expiresIn * 1000,
624
644
  });
625
645
 
626
- return { status: 'fetched', endpointId, accessToken, tokenType, scopes: grantedScopes };
646
+ return { status: 'fetched', tokenCacheKey: cacheKey, endpointId: cacheKey, accessToken, tokenType, scopes: grantedScopes };
627
647
  }
628
648
 
629
649
  /**
630
650
  * Returns the cached SMART bearer for the given endpointId if still valid (>30s remaining).
631
651
  * Returns `undefined` if not cached or expired.
632
652
  */
633
- public getCachedBearerToken(endpointId: string): string | undefined {
634
- const cached = this._tokenCache.get(endpointId);
653
+ public getCachedBearerToken(tokenCacheKey: string): string | undefined {
654
+ const cached = this._tokenCache.get(tokenCacheKey);
635
655
  if (cached && cached.expiresAt > Date.now() + 30_000) {
636
656
  return cached.accessToken;
637
657
  }
@@ -647,7 +667,8 @@ export class DataspaceNodeClient {
647
667
  const {
648
668
  clientId,
649
669
  scopes,
650
- endpointId = `smart-backend:${clientId}`,
670
+ tokenCacheKey = `smart-backend:${clientId}`,
671
+ endpointId,
651
672
  tokenUrl,
652
673
  tokenPath = '/token',
653
674
  audience,
@@ -655,12 +676,14 @@ export class DataspaceNodeClient {
655
676
  additionalTokenFields,
656
677
  } = options;
657
678
 
658
- const cached = this._tokenCache.get(endpointId);
679
+ const cacheKey = String(tokenCacheKey || endpointId || '').trim() || `smart-backend:${clientId}`;
680
+ const cached = this._tokenCache.get(cacheKey);
659
681
  if (cached && cached.expiresAt > Date.now() + 30_000) {
660
682
  return {
661
683
  status: 'cached',
662
684
  profile: 'smart-backend.v1',
663
- endpointId,
685
+ tokenCacheKey: cacheKey,
686
+ endpointId: cacheKey,
664
687
  accessToken: cached.accessToken,
665
688
  tokenType: cached.tokenType,
666
689
  scopes: cached.scopes,
@@ -694,7 +717,8 @@ export class DataspaceNodeClient {
694
717
  return {
695
718
  status: 'failed',
696
719
  profile: 'smart-backend.v1',
697
- endpointId,
720
+ tokenCacheKey: cacheKey,
721
+ endpointId: cacheKey,
698
722
  statusCode: response.status,
699
723
  response,
700
724
  };
@@ -706,7 +730,7 @@ export class DataspaceNodeClient {
706
730
  const expiresIn = Number(body.expires_in ?? 0);
707
731
  const expiresAt = Date.now() + expiresIn * 1000;
708
732
 
709
- this._tokenCache.set(endpointId, {
733
+ this._tokenCache.set(cacheKey, {
710
734
  accessToken,
711
735
  tokenType,
712
736
  scopes: grantedScopes,
@@ -716,7 +740,8 @@ export class DataspaceNodeClient {
716
740
  return {
717
741
  status: 'fetched',
718
742
  profile: 'smart-backend.v1',
719
- endpointId,
743
+ tokenCacheKey: cacheKey,
744
+ endpointId: cacheKey,
720
745
  statusCode: response.status,
721
746
  accessToken,
722
747
  tokenType,
@@ -730,13 +755,13 @@ export class DataspaceNodeClient {
730
755
  * Exchange token payload against gateway token endpoint and cache the result.
731
756
  */
732
757
  public async requestSmartToken(input: SmartTokenExchangeInput): Promise<SmartTokenExchangeResult> {
733
- const endpointId = String(input.endpointId || '').trim();
734
- if (!endpointId) {
735
- throw new Error('requestSmartToken requires endpointId.');
758
+ const tokenCacheKey = String(input.tokenCacheKey || input.endpointId || '').trim();
759
+ if (!tokenCacheKey) {
760
+ throw new Error('requestSmartToken requires tokenCacheKey.');
736
761
  }
737
762
 
738
763
  const normalizedScopes = Array.from(new Set((input.scopes || []).filter(Boolean))).sort();
739
- const cached = this._tokenCache.get(endpointId);
764
+ const cached = this._tokenCache.get(tokenCacheKey);
740
765
  if (cached && cached.expiresAt > Date.now() + 30_000) {
741
766
  return {
742
767
  status: 'cached',
@@ -764,7 +789,7 @@ export class DataspaceNodeClient {
764
789
  : String(body.scope ?? '').trim().split(' ').filter(Boolean);
765
790
  const resolvedScopes = grantedScopes.length ? grantedScopes : normalizedScopes;
766
791
  const expiresIn = Number(body.expires_in ?? 0);
767
- this._tokenCache.set(endpointId, {
792
+ this._tokenCache.set(tokenCacheKey, {
768
793
  accessToken,
769
794
  tokenType,
770
795
  scopes: resolvedScopes,
@@ -794,9 +819,9 @@ export class DataspaceNodeClient {
794
819
  : undefined,
795
820
  );
796
821
  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).');
822
+ const tokenCacheKey = String(input.tokenCacheKey || input.endpointId || `smart:${routeCtx.tenantId}:${normalizedScopes.join(',')}`).trim();
823
+ if (!tokenCacheKey) {
824
+ throw new Error('requestSmartTokenSimple requires tokenCacheKey (or non-empty scopes).');
800
825
  }
801
826
  const pollOptions = this.resolveSimplePollOptions(input.timeoutSeconds, input.intervalSeconds);
802
827
 
@@ -831,7 +856,7 @@ export class DataspaceNodeClient {
831
856
  const grantedScopes = String(exchangeBody.scope || '').trim().split(' ').filter(Boolean);
832
857
  const resolvedScopes = grantedScopes.length ? grantedScopes : normalizedScopes;
833
858
  const expiresIn = Number(exchangeBody.expires_in ?? 0);
834
- this._tokenCache.set(endpointId, {
859
+ this._tokenCache.set(tokenCacheKey, {
835
860
  accessToken,
836
861
  tokenType,
837
862
  scopes: resolvedScopes,
@@ -999,6 +1024,34 @@ export class DataspaceNodeClient {
999
1024
  // ---- Generic batch API --------------------------------------------------
1000
1025
 
1001
1026
  /**
1027
+ * POST a DIDComm bundle payload.
1028
+ * This is the preferred high-level method for DIDComm submission of
1029
+ * FHIR/API bundles (batch, transaction, message, etc.).
1030
+ *
1031
+ * Returns immediately with the HTTP response — pair with `pollUntilComplete` or use `submitAndPoll`.
1032
+ */
1033
+ public async submitBundle(
1034
+ path: string,
1035
+ payload: { thid?: string } & Record<string, unknown>,
1036
+ options?: {
1037
+ mode?: 'plain' | 'strict';
1038
+ recipientEncryptionJwk?: PublicJwk;
1039
+ walletContext?: WalletContext;
1040
+ },
1041
+ ): Promise<SubmitResponse> {
1042
+ const mode = options?.mode ?? 'plain';
1043
+ if (mode === 'strict') {
1044
+ if (!options?.recipientEncryptionJwk || !options?.walletContext) {
1045
+ throw new Error('submitBundle strict mode requires recipientEncryptionJwk and walletContext.');
1046
+ }
1047
+ return this.submitBatchEncrypted(path, payload, options.recipientEncryptionJwk, options.walletContext);
1048
+ }
1049
+ return this.submitBatch(path, payload);
1050
+ }
1051
+
1052
+ /**
1053
+ * @deprecated Use `submitBundle` instead.
1054
+ *
1002
1055
  * POST a DIDComm plaintext payload to a batch submit path.
1003
1056
  * Use this for all `_batch` routes (family registration, observations, tasks, etc.).
1004
1057
  * Content-Type: `application/didcomm-plaintext+json`.
@@ -1006,14 +1059,18 @@ export class DataspaceNodeClient {
1006
1059
  * Returns immediately with the HTTP response — pair with `pollUntilComplete` or use `submitAndPoll`.
1007
1060
  */
1008
1061
  public async submitBatch(path: string, payload: unknown): Promise<SubmitResponse> {
1009
- const response = await this.doPost(path, payload, 'application/didcomm-plaintext+json');
1010
- const body = await this.parseResponseBody(response);
1011
-
1012
- return {
1013
- status: response.status,
1014
- location: response.headers.get('location') ?? undefined,
1015
- body,
1016
- };
1062
+ const url = /^https?:\/\//.test(path)
1063
+ ? path
1064
+ : `${this.baseUrl}${path.startsWith('/') ? path : `/${path}`}`;
1065
+ const result = await submitDidcomm({
1066
+ mode: 'plain',
1067
+ url,
1068
+ payload: payload as Record<string, unknown>,
1069
+ defaultHeaders: this.defaultHeaders,
1070
+ bearerToken: this.bearerToken,
1071
+ fetcher: (requestUrl: string, init: DidcommFetchInit) => fetch(requestUrl, init),
1072
+ });
1073
+ return { status: result.status, location: result.location, body: result.body };
1017
1074
  }
1018
1075
 
1019
1076
  /**
@@ -1038,42 +1095,34 @@ export class DataspaceNodeClient {
1038
1095
  const publicJwks = await this.wallet.getPublicJwks(walletContext);
1039
1096
  const signingJwk = publicJwks.find((jwk) => jwk.use === 'sig' || jwk.alg === 'ES384') ?? publicJwks[0];
1040
1097
 
1041
- // Step 1: sign payload claims as compact JWS
1042
- const compactJws = await this.wallet.signCompactJws(walletContext, {
1043
- header: {
1044
- typ: 'application/didcomm-signed+json',
1045
- alg: 'ES384',
1046
- ...(signingJwk?.kid ? { kid: signingJwk.kid } : {}),
1047
- },
1048
- claims: payload as Record<string, unknown>,
1049
- });
1050
-
1051
- // Step 2: encrypt JWS string as compact JWE (RSA-OAEP-256 + A256GCM, cty: JWS)
1052
- const compactJwe = await this.wallet.buildCompactJwe(walletContext, {
1053
- plaintext: compactJws,
1054
- recipientJwk: recipientEncryptionJwk,
1055
- contentType: 'JWS',
1056
- });
1057
-
1058
- // Step 3: POST the JWE token directly (not JSON-serialised)
1059
1098
  const url = /^https?:\/\//.test(path)
1060
1099
  ? path
1061
1100
  : `${this.baseUrl}${path.startsWith('/') ? path : `/${path}`}`;
1062
- const headers: Record<string, string> = {
1063
- ...this.defaultHeaders,
1064
- 'Content-Type': 'application/didcomm-encrypted+json',
1065
- Accept: 'application/json, application/didcomm-plaintext+json, */*',
1066
- };
1067
- if (this.bearerToken) {
1068
- headers.Authorization = `Bearer ${this.bearerToken}`;
1069
- }
1070
- const response = await fetch(url, { method: 'POST', headers, body: compactJwe });
1071
- const body = await this.parseResponseBody(response);
1072
- return {
1073
- status: response.status,
1074
- location: response.headers.get('location') ?? undefined,
1075
- body,
1076
- };
1101
+ const result = await submitDidcomm({
1102
+ mode: 'strict',
1103
+ url,
1104
+ payload,
1105
+ defaultHeaders: this.defaultHeaders,
1106
+ bearerToken: this.bearerToken,
1107
+ recipientEncryptionJwk,
1108
+ signCompactJws: async (claims: Record<string, unknown>) =>
1109
+ this.wallet!.signCompactJws(walletContext, {
1110
+ header: {
1111
+ typ: 'application/didcomm-signed+json',
1112
+ alg: 'ES384',
1113
+ ...(signingJwk?.kid ? { kid: signingJwk.kid } : {}),
1114
+ },
1115
+ claims,
1116
+ }),
1117
+ encryptCompactJwe: async (compactJws: string, recipientJwk: unknown) =>
1118
+ this.wallet!.buildCompactJwe(walletContext, {
1119
+ plaintext: compactJws,
1120
+ recipientJwk: recipientJwk as PublicJwk,
1121
+ contentType: 'JWS',
1122
+ }),
1123
+ fetcher: (requestUrl: string, init: DidcommFetchInit) => fetch(requestUrl, init),
1124
+ });
1125
+ return { status: result.status, location: result.location, body: result.body };
1077
1126
  }
1078
1127
 
1079
1128
  /**
@@ -1091,6 +1140,14 @@ export class DataspaceNodeClient {
1091
1140
  };
1092
1141
  }
1093
1142
 
1143
+ /**
1144
+ * Legacy JSON submit for non-bundle payloads (openid/token/resource JSON bodies).
1145
+ * Keeps JSON flows explicit and semantically separated from DIDComm bundle flows.
1146
+ */
1147
+ public async submitLegacyJson(path: string, payload: unknown): Promise<SubmitResponse> {
1148
+ return this.postJson(path, payload);
1149
+ }
1150
+
1094
1151
  /**
1095
1152
  * POST a multipart/form-data payload.
1096
1153
  * Use for file upload endpoints. Prefer `uploadConversionFile` for DataConversion uploads.
@@ -1568,9 +1625,9 @@ export class DataspaceNodeClient {
1568
1625
  'org.schema.Service.category': hostCtx.sector,
1569
1626
  'org.schema.Service.identifier': resolvedServiceDid,
1570
1627
  ...(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 } : {}),
1628
+ [ClaimsPersonSchemaorg.hasOccupation]: controllerRole,
1629
+ ...(controllerEmail ? { [ClaimsPersonSchemaorg.email]: controllerEmail } : {}),
1630
+ ...(controllerTelephone ? { [ClaimsPersonSchemaorg.telephone]: controllerTelephone } : {}),
1574
1631
  };
1575
1632
 
1576
1633
  const activation = await this.activateOrganizationInGatewayFromIcaProof(
@@ -1675,8 +1732,8 @@ export class DataspaceNodeClient {
1675
1732
  public getOfferIdFromResponse(result: SubmitAndPollResult | PollResult | unknown): string | undefined {
1676
1733
  const entry = this.getFirstDidcommDataEntryFromResponse(result);
1677
1734
  const offerId = String(
1678
- (entry as any)?.meta?.claims?.['org.schema.Offer.identifier']
1679
- || (entry as any)?.resource?.meta?.claims?.['org.schema.Offer.identifier']
1735
+ (entry as any)?.meta?.claims?.[ClaimsOfferSchemaorg.identifier]
1736
+ || (entry as any)?.resource?.meta?.claims?.[ClaimsOfferSchemaorg.identifier]
1680
1737
  || '',
1681
1738
  ).trim();
1682
1739
  return offerId || undefined;
@@ -1688,23 +1745,51 @@ export class DataspaceNodeClient {
1688
1745
  public getOfferPreviewFromResponse(result: SubmitAndPollResult | PollResult | unknown): OfferPreview {
1689
1746
  const entry = this.getFirstDidcommDataEntryFromResponse(result) as any;
1690
1747
  const claims = entry?.meta?.claims || entry?.resource?.meta?.claims || {};
1691
- const seatsRaw = claims['org.schema.Offer.eligibleQuantity.value'];
1748
+ const seatsRaw = claims[ClaimsOfferSchemaorg.eligibleQuantityValue];
1692
1749
  const seats =
1693
1750
  typeof seatsRaw === 'number'
1694
1751
  ? seatsRaw
1695
1752
  : (typeof seatsRaw === 'string' && seatsRaw.trim() ? Number(seatsRaw) : undefined);
1696
1753
  return {
1697
1754
  offerId: this.getOfferIdFromResponse(result),
1698
- amount: claims['org.schema.Offer.price'],
1699
- currency: claims['org.schema.Offer.priceCurrency'],
1755
+ amount: claims[ClaimsOfferSchemaorg.price],
1756
+ currency: claims[ClaimsOfferSchemaorg.priceCurrency],
1700
1757
  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'],
1758
+ planName: claims[ClaimsOfferSchemaorg.itemOfferedName],
1759
+ sku: claims[ClaimsOfferSchemaorg.itemOfferedSku],
1760
+ paymentMethod: claims[ClaimsOfferSchemaorg.acceptedPaymentMethod],
1761
+ checkoutUrl: claims[ClaimsOfferSchemaorg.checkoutPageURLTemplate],
1705
1762
  };
1706
1763
  }
1707
1764
 
1765
+ /**
1766
+ * Alias of `getOfferPreviewFromResponse` with business naming.
1767
+ */
1768
+ public getOfferInfoFromResponse(result: SubmitAndPollResult | PollResult | unknown): OfferInfo {
1769
+ return this.getOfferPreviewFromResponse(result);
1770
+ }
1771
+
1772
+ /**
1773
+ * Extract activation code from response payload or claims.
1774
+ * Supports common response shapes used in onboarding and license issuance flows.
1775
+ */
1776
+ public getActivationCodeFromResponse(result: SubmitAndPollResult | PollResult | unknown): string | undefined {
1777
+ const root = (result as any)?.poll?.body || (result as any)?.body || {};
1778
+ const byBody =
1779
+ String(root?.activationCode || root?.body?.activationCode || '').trim();
1780
+ if (byBody) return byBody;
1781
+
1782
+ const entry = this.getFirstDidcommDataEntryFromResponse(result) as any;
1783
+ const claims = entry?.meta?.claims || entry?.resource?.meta?.claims || {};
1784
+ const byClaims = String(
1785
+ claims['org.schema.IndividualProduct.serialNumber']
1786
+ || claims['org.schema.Offer.serialNumber']
1787
+ || claims['activationCode']
1788
+ || '',
1789
+ ).trim();
1790
+ return byClaims || undefined;
1791
+ }
1792
+
1708
1793
  /**
1709
1794
  * Throws when first DIDComm entry contains a business-level error status.
1710
1795
  */
@@ -1912,9 +1997,9 @@ export class DataspaceNodeClient {
1912
1997
  '@context': 'org.schema',
1913
1998
  'org.schema.Organization.alternateName': alternateName,
1914
1999
  '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 } : {}),
2000
+ [ClaimsPersonSchemaorg.hasOccupation]: controllerRole,
2001
+ ...(controllerEmail ? { [ClaimsPersonSchemaorg.email]: controllerEmail } : {}),
2002
+ ...(controllerTelephone ? { [ClaimsPersonSchemaorg.telephone]: controllerTelephone } : {}),
1918
2003
  ...(input.additionalClaims || {}),
1919
2004
  };
1920
2005
  const pollOptions = this.resolveSimplePollOptions(input.timeoutSeconds, input.intervalSeconds);
@@ -2179,10 +2264,15 @@ export class DataspaceNodeClient {
2179
2264
 
2180
2265
  private async parseResponseBody(response: Response): Promise<unknown> {
2181
2266
  const contentType = response.headers.get('content-type') || '';
2267
+ const raw = await response.text();
2268
+ if (!raw) return {};
2182
2269
  if (contentType.includes('application/json') || contentType.includes('application/didcomm-plaintext+json')) {
2183
- return response.json().catch(() => ({}));
2270
+ try {
2271
+ return JSON.parse(raw);
2272
+ } catch {
2273
+ return {};
2274
+ }
2184
2275
  }
2185
- const text = await response.text();
2186
- return text;
2276
+ return raw;
2187
2277
  }
2188
2278
  }
package/src/index.ts 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/src/types.ts CHANGED
@@ -206,6 +206,15 @@ export type OfferPreview = {
206
206
  checkoutUrl?: string;
207
207
  };
208
208
 
209
+ export type OfferInfo = OfferPreview;
210
+
211
+ export type EndpointSelector = {
212
+ section: string;
213
+ format: string;
214
+ resourceType: string;
215
+ action: string;
216
+ };
217
+
209
218
  /**
210
219
  * Input for organization activation in GW using ICA-derived proof material.
211
220
  *
@@ -526,6 +535,8 @@ export type BackendPkceAuthOptions = {
526
535
  /** Requested scopes for the SMART bearer token. */
527
536
  scopes: string[];
528
537
  /** Cache key for the resulting bearer token. Defaults to `pkce:<apiKey prefix>`. */
538
+ tokenCacheKey?: string;
539
+ /** @deprecated Use `tokenCacheKey`. */
529
540
  endpointId?: string;
530
541
  /** PKCE code verifier. Auto-generated with randomUUID if not provided. */
531
542
  codeVerifier?: string;
@@ -536,6 +547,8 @@ export type BackendPkceAuthOptions = {
536
547
  export type BackendPkceAuthResult = {
537
548
  /** `fetched`: new token obtained. `cached`: valid token already in cache. `failed`: flow error. */
538
549
  status: 'fetched' | 'cached' | 'failed';
550
+ tokenCacheKey: string;
551
+ /** @deprecated Use `tokenCacheKey`. */
539
552
  endpointId: string;
540
553
  accessToken: string;
541
554
  tokenType: string;
@@ -547,6 +560,8 @@ export type BackendPkceAuthResult = {
547
560
  export type BackendSmartAuthOptions = {
548
561
  clientId: string;
549
562
  scopes: string[];
563
+ tokenCacheKey?: string;
564
+ /** @deprecated Use `tokenCacheKey`. */
550
565
  endpointId?: string;
551
566
  tokenUrl?: string;
552
567
  tokenPath?: string;
@@ -560,6 +575,8 @@ export type BackendSmartAuthOptions = {
560
575
  export type BackendSmartAuthResult = {
561
576
  status: 'fetched' | 'cached' | 'failed';
562
577
  profile: 'smart-backend.v1';
578
+ tokenCacheKey: string;
579
+ /** @deprecated Use `tokenCacheKey`. */
563
580
  endpointId: string;
564
581
  accessToken?: string;
565
582
  tokenType?: string;
@@ -570,7 +587,9 @@ export type BackendSmartAuthResult = {
570
587
  };
571
588
 
572
589
  export type SmartTokenExchangeInput = {
573
- endpointId: string;
590
+ tokenCacheKey: string;
591
+ /** @deprecated Use `tokenCacheKey`. */
592
+ endpointId?: string;
574
593
  scopes: string[];
575
594
  exchangePayload: Record<string, unknown>;
576
595
  path?: string;
@@ -582,6 +601,8 @@ export type SmartTokenRequestSimpleInput = {
582
601
  sector?: string;
583
602
  idToken: string;
584
603
  scopes: string[];
604
+ tokenCacheKey?: string;
605
+ /** @deprecated Use `tokenCacheKey`. */
585
606
  endpointId?: string;
586
607
  timeoutSeconds?: number;
587
608
  intervalSeconds?: number;
@@ -0,0 +1,91 @@
1
+ export type VpTokenHeader = {
2
+ alg: string;
3
+ typ?: string;
4
+ kid?: string;
5
+ [key: string]: unknown;
6
+ };
7
+
8
+ export type VpTokenPayload = {
9
+ iss: string;
10
+ sub?: string;
11
+ aud?: string;
12
+ jti?: string;
13
+ iat?: number;
14
+ exp?: number;
15
+ nonce?: string;
16
+ vp: {
17
+ '@context'?: unknown;
18
+ type?: unknown;
19
+ holder?: string;
20
+ verifiableCredential: string[];
21
+ [key: string]: unknown;
22
+ };
23
+ [key: string]: unknown;
24
+ };
25
+
26
+ function toB64UrlJson(input: unknown): string {
27
+ return Buffer.from(JSON.stringify(input), 'utf-8').toString('base64url');
28
+ }
29
+
30
+ function fallbackId(): string {
31
+ const rand = Math.random().toString(36).slice(2, 10);
32
+ return `id-${Date.now()}-${rand}`;
33
+ }
34
+
35
+ export function generateUuidLike(): string {
36
+ const fn = (globalThis as any)?.crypto?.randomUUID;
37
+ if (typeof fn === 'function') return fn.call((globalThis as any).crypto);
38
+ return fallbackId();
39
+ }
40
+
41
+ export function buildEpochWindow(ttlSeconds = 300): { iat: number; exp: number } {
42
+ const iat = Math.floor(Date.now() / 1000);
43
+ return { iat, exp: iat + Math.max(1, Math.floor(ttlSeconds)) };
44
+ }
45
+
46
+ export function createVP(input?: Partial<VpTokenPayload>): VpTokenPayload {
47
+ const ttl = input?.exp && input?.iat ? undefined : buildEpochWindow(300);
48
+ const jti = input?.jti || generateUuidLike();
49
+ const nonce = input?.nonce || generateUuidLike();
50
+ return {
51
+ iss: String(input?.iss || ''),
52
+ sub: input?.sub,
53
+ aud: input?.aud,
54
+ jti,
55
+ iat: input?.iat ?? ttl?.iat,
56
+ exp: input?.exp ?? ttl?.exp,
57
+ nonce,
58
+ vp: {
59
+ '@context': ['https://www.w3.org/2018/credentials/v1'],
60
+ type: ['VerifiablePresentation'],
61
+ holder: input?.vp?.holder || input?.iss || '',
62
+ verifiableCredential: [],
63
+ ...(input?.vp || {}),
64
+ },
65
+ };
66
+ }
67
+
68
+ export function addVC(vpPayload: VpTokenPayload, vcJwt: string): VpTokenPayload {
69
+ const v = String(vcJwt || '').trim();
70
+ if (v) vpPayload.vp.verifiableCredential.push(v);
71
+ return vpPayload;
72
+ }
73
+
74
+ export function prepareForSignature(header: VpTokenHeader, payload: VpTokenPayload): {
75
+ encodedHeader: string;
76
+ encodedPayload: string;
77
+ signingInput: string;
78
+ } {
79
+ const encodedHeader = toB64UrlJson(header);
80
+ const encodedPayload = toB64UrlJson(payload);
81
+ return { encodedHeader, encodedPayload, signingInput: `${encodedHeader}.${encodedPayload}` };
82
+ }
83
+
84
+ export function prepareBytesForSignature(header: VpTokenHeader, payload: VpTokenPayload): Uint8Array {
85
+ const { signingInput } = prepareForSignature(header, payload);
86
+ return new TextEncoder().encode(signingInput);
87
+ }
88
+
89
+ export function buildVpTokenCompact(encodedHeader: string, encodedPayload: string, signatureBase64Url: string): string {
90
+ return `${encodedHeader}.${encodedPayload}.${String(signatureBase64Url || '').trim()}`;
91
+ }