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/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,
|
|
390
|
-
const
|
|
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(
|
|
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(
|
|
473
|
-
const cached = this._tokenCache.get(
|
|
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,
|
|
484
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
553
|
-
if (!
|
|
554
|
-
throw new Error('requestSmartToken requires
|
|
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(
|
|
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(
|
|
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
|
|
607
|
-
if (!
|
|
608
|
-
throw new Error('requestSmartTokenSimple requires
|
|
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(
|
|
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
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
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
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
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
|
-
|
|
1227
|
-
...(controllerEmail ? {
|
|
1228
|
-
...(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?.[
|
|
1310
|
-
|| entry?.resource?.meta?.claims?.[
|
|
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[
|
|
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[
|
|
1327
|
-
currency: claims[
|
|
1370
|
+
amount: claims[ClaimsOfferSchemaorg.price],
|
|
1371
|
+
currency: claims[ClaimsOfferSchemaorg.priceCurrency],
|
|
1328
1372
|
seats: Number.isFinite(seats) ? seats : undefined,
|
|
1329
|
-
planName: claims[
|
|
1330
|
-
sku: claims[
|
|
1331
|
-
paymentMethod: claims[
|
|
1332
|
-
checkoutUrl: claims[
|
|
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
|
-
|
|
1467
|
-
...(controllerEmail ? {
|
|
1468
|
-
...(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
|
-
|
|
1746
|
+
try {
|
|
1747
|
+
return JSON.parse(raw);
|
|
1748
|
+
}
|
|
1749
|
+
catch {
|
|
1750
|
+
return {};
|
|
1751
|
+
}
|
|
1677
1752
|
}
|
|
1678
|
-
|
|
1679
|
-
return text;
|
|
1753
|
+
return raw;
|
|
1680
1754
|
}
|
|
1681
1755
|
}
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
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
|
-
|
|
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;
|
package/dist/vp-token.js
ADDED
|
@@ -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
|
-
### `
|
|
481
|
+
### `submitBundle`
|
|
482
482
|
|
|
483
|
-
POST a DIDComm
|
|
483
|
+
POST a DIDComm bundle payload. Preferred API for FHIR/API bundle operations.
|
|
484
484
|
|
|
485
485
|
```ts
|
|
486
|
-
|
|
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.
|
|
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' },
|