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/CHANGELOG.md +4 -1
- package/README.md +21 -348
- package/dist/client.d.ts +40 -2
- package/dist/client.js +156 -82
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/types.d.ts +20 -1
- package/dist/vp-token.d.ts +37 -0
- package/dist/vp-token.js +56 -0
- package/docs/API.md +19 -4
- package/docs/BACKEND_NODE_INTEGRATION.md +138 -8
- package/docs/CONTROLLER_FLOW_STEP_BY_STEP.md +283 -0
- package/docs/DEVELOPER_USE_CASES.md +10 -2
- package/docs/E2E_LOCAL_GW_UC5.md +49 -0
- package/docs/ENDPOINT_ID_CATALOG.md +90 -0
- package/docs/LEGAL_ORGANIZATION_FLOW_STEP_BY_STEP.md +84 -0
- package/docs/PERSONAL_FLOW_STEP_BY_STEP.md +70 -0
- package/docs/PORTAL_BACKEND_INTEGRATION_HANDOVER.md +11 -3
- package/docs/PRACTITIONER_FLOW_STEP_BY_STEP.md +182 -0
- package/examples/e2e-bootstrap-tenant.mjs +3 -2
- package/examples/e2e-individual-flow.mjs +13 -8
- package/examples/host-activate-and-employee.mjs +3 -2
- package/package.json +4 -3
- package/src/client.ts +175 -85
- package/src/index.ts +1 -0
- package/src/types.ts +22 -1
- package/src/vp-token.ts +91 -0
- package/tests/fixtures/ica-vp-minimal.json +67 -0
- package/tests/helpers/vp-token-fixture.mjs +23 -0
- package/tests/live-gw-uc5.e2e.test.mjs +108 -0
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
|
-
|
|
539
|
+
tokenCacheKey = `pkce:${apiKey.slice(0, 8)}`,
|
|
540
|
+
endpointId,
|
|
522
541
|
codeVerifier = randomUUID(),
|
|
523
542
|
pollOptions,
|
|
524
543
|
} = options;
|
|
525
544
|
|
|
526
|
-
const
|
|
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(
|
|
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(
|
|
634
|
-
const cached = this._tokenCache.get(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
734
|
-
if (!
|
|
735
|
-
throw new Error('requestSmartToken requires
|
|
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(
|
|
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(
|
|
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
|
|
798
|
-
if (!
|
|
799
|
-
throw new Error('requestSmartTokenSimple requires
|
|
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(
|
|
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
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
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
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
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
|
-
|
|
1572
|
-
...(controllerEmail ? {
|
|
1573
|
-
...(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?.[
|
|
1679
|
-
|| (entry as any)?.resource?.meta?.claims?.[
|
|
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[
|
|
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[
|
|
1699
|
-
currency: claims[
|
|
1755
|
+
amount: claims[ClaimsOfferSchemaorg.price],
|
|
1756
|
+
currency: claims[ClaimsOfferSchemaorg.priceCurrency],
|
|
1700
1757
|
seats: Number.isFinite(seats as number) ? seats : undefined,
|
|
1701
|
-
planName: claims[
|
|
1702
|
-
sku: claims[
|
|
1703
|
-
paymentMethod: claims[
|
|
1704
|
-
checkoutUrl: claims[
|
|
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
|
-
|
|
1916
|
-
...(controllerEmail ? {
|
|
1917
|
-
...(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
|
-
|
|
2270
|
+
try {
|
|
2271
|
+
return JSON.parse(raw);
|
|
2272
|
+
} catch {
|
|
2273
|
+
return {};
|
|
2274
|
+
}
|
|
2184
2275
|
}
|
|
2185
|
-
|
|
2186
|
-
return text;
|
|
2276
|
+
return raw;
|
|
2187
2277
|
}
|
|
2188
2278
|
}
|
package/src/index.ts
CHANGED
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
|
-
|
|
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;
|
package/src/vp-token.ts
ADDED
|
@@ -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
|
+
}
|