gdc-sdk-node-ts 2.4.40 → 2.4.42
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.
|
@@ -2,6 +2,11 @@ import type { JWK } from 'gdc-common-utils-ts/models/jwk';
|
|
|
2
2
|
import type { ActorKind } from 'gdc-common-utils-ts/models/actor-session';
|
|
3
3
|
import type { LegalOrganizationVerificationTransactionInput } from 'gdc-common-utils-ts/utils/legal-organization-verification-transaction';
|
|
4
4
|
import { type AppInfo, type ConfidentialStorageProfile, type PollOptions, type SubmitAndPollResult } from 'gdc-sdk-core-ts';
|
|
5
|
+
import { OrganizationControllerSdk } from './orchestration/organization-controller-sdk.js';
|
|
6
|
+
import { ProfessionalSdk } from './orchestration/professional-sdk.js';
|
|
7
|
+
import { DigitalTwinSdk } from './orchestration/digital-twin-sdk.js';
|
|
8
|
+
import type { SmartTokenExchangeResult } from './smart-token.js';
|
|
9
|
+
import type { DigitalTwinSearchInput, DigitalTwinSearchResult } from './digital-twin.js';
|
|
5
10
|
import type { RouteContext } from './individual-onboarding.js';
|
|
6
11
|
import type { HostRouteContext } from './host-onboarding.js';
|
|
7
12
|
import type { SecureDidcommTransportAdapter } from 'gdc-sdk-core-ts';
|
|
@@ -205,6 +210,69 @@ export type ResolvedServerProfileSession = Readonly<{
|
|
|
205
210
|
}>): Promise<unknown>;
|
|
206
211
|
}>;
|
|
207
212
|
}>;
|
|
213
|
+
/**
|
|
214
|
+
* Server-only authorization material used to reopen one registered controller.
|
|
215
|
+
*
|
|
216
|
+
* A product may let this manager open the PIN-protected seed directly, or may
|
|
217
|
+
* pass the same seed after its own passkey/session policy has authorized and
|
|
218
|
+
* unsealed it. `authorizedWalletSeed` must never cross a browser/API boundary.
|
|
219
|
+
*/
|
|
220
|
+
export type ServerOrganizationControllerOpenInput = Readonly<{
|
|
221
|
+
ownerId: string;
|
|
222
|
+
profileId: string;
|
|
223
|
+
/** Fresh signed OIDC id_token used as the HTTP bearer for this operation. */
|
|
224
|
+
idToken: string;
|
|
225
|
+
/** User-entered or product-managed profile PIN; mutually exclusive with authorizedWalletSeed. */
|
|
226
|
+
pin?: string;
|
|
227
|
+
/** Already-authorized server-only 32-byte base64url seed; mutually exclusive with pin. */
|
|
228
|
+
authorizedWalletSeed?: string;
|
|
229
|
+
}>;
|
|
230
|
+
/** High-level controller facade plus its immutable durable profile metadata. */
|
|
231
|
+
export type OpenedServerOrganizationController = Readonly<{
|
|
232
|
+
profile: ServerProfileRecord;
|
|
233
|
+
sdk: OrganizationControllerSdk;
|
|
234
|
+
}>;
|
|
235
|
+
/** Server-owned role evidence used to sign a fresh professional VP. */
|
|
236
|
+
export type ServerProfessionalProofInput = Readonly<{
|
|
237
|
+
role: string;
|
|
238
|
+
email?: string;
|
|
239
|
+
sameAs?: string | readonly string[];
|
|
240
|
+
telephone?: string;
|
|
241
|
+
credentialMaterial?: string;
|
|
242
|
+
}>;
|
|
243
|
+
/** Reopens one enrolled employee/professional profile behind the BFF boundary. */
|
|
244
|
+
export type ServerProfessionalOpenInput = Readonly<{
|
|
245
|
+
ownerId: string;
|
|
246
|
+
profileId: string;
|
|
247
|
+
/** Fresh signed OIDC id_token proving the authenticated account/email. */
|
|
248
|
+
idToken: string;
|
|
249
|
+
/** Role evidence signed by the managed DCR wallet, independently of idToken. */
|
|
250
|
+
professionalProof: ServerProfessionalProofInput;
|
|
251
|
+
/** Manager-owned PIN path; mutually exclusive with authorizedWalletSeed. */
|
|
252
|
+
pin?: string;
|
|
253
|
+
/** Server-only seed already authorized by a product passkey/session policy. */
|
|
254
|
+
authorizedWalletSeed?: string;
|
|
255
|
+
}>;
|
|
256
|
+
/** Business authorization requested from SMART; OpenID/JWT fields stay SDK-owned. */
|
|
257
|
+
export type ServerProfessionalSmartTokenInput = Readonly<{
|
|
258
|
+
subjectDid?: string;
|
|
259
|
+
purpose?: string;
|
|
260
|
+
scopes: string[];
|
|
261
|
+
acrValues?: string;
|
|
262
|
+
requestBodyClaims?: Record<string, unknown>;
|
|
263
|
+
tokenCacheKey?: string;
|
|
264
|
+
timeoutSeconds?: number;
|
|
265
|
+
intervalSeconds?: number;
|
|
266
|
+
}>;
|
|
267
|
+
/** Role-scoped professional facades with managed SMART proof methods. */
|
|
268
|
+
export type OpenedServerProfessional = Readonly<{
|
|
269
|
+
profile: ServerProfileRecord;
|
|
270
|
+
sdk: ProfessionalSdk;
|
|
271
|
+
digitalTwin: DigitalTwinSdk;
|
|
272
|
+
requestSmartToken(input: ServerProfessionalSmartTokenInput): Promise<SmartTokenExchangeResult>;
|
|
273
|
+
requestDigitalTwinSmartToken(input: ServerProfessionalSmartTokenInput): Promise<SmartTokenExchangeResult>;
|
|
274
|
+
searchDigitalTwins(input: Omit<DigitalTwinSearchInput, 'accessToken'>): Promise<DigitalTwinSearchResult>;
|
|
275
|
+
}>;
|
|
208
276
|
export type ServerProfileSessionManagerOptions = Readonly<{
|
|
209
277
|
store: ServerProfileStore;
|
|
210
278
|
sealer: ServerProfileSealer;
|
|
@@ -275,6 +343,25 @@ export declare class ServerProfileSessionManager {
|
|
|
275
343
|
*/
|
|
276
344
|
refreshSession(ownerId: string, sessionId: string, idToken: string): Promise<ResolvedServerProfileSession>;
|
|
277
345
|
resolveSession(ownerId: string, sessionId: string): Promise<ResolvedServerProfileSession>;
|
|
346
|
+
/**
|
|
347
|
+
* Opens the high-level organization-controller API without exposing wallet,
|
|
348
|
+
* DIDComm, DCR or HTTP-client plumbing to the integrating BFF.
|
|
349
|
+
*
|
|
350
|
+
* The durable profile remains authoritative for actor DID, provider DID,
|
|
351
|
+
* route and DCR client id. The reconstructed keys must match the public keys
|
|
352
|
+
* registered for that profile before any gateway request can be sent.
|
|
353
|
+
*/
|
|
354
|
+
openOrganizationController(input: ServerOrganizationControllerOpenInput): Promise<OpenedServerOrganizationController>;
|
|
355
|
+
/**
|
|
356
|
+
* Opens one registered organization employee/professional without exposing
|
|
357
|
+
* wallet reconstruction, private_key_jwt, VP or transport plumbing.
|
|
358
|
+
*
|
|
359
|
+
* The product authorizes the PIN/passkey/session and supplies business role
|
|
360
|
+
* evidence. The SDK revalidates the registered key set, signs a fresh role
|
|
361
|
+
* VP independently from the OIDC account proof, derives the SMART endpoint
|
|
362
|
+
* from the durable profile route and owns the client assertion.
|
|
363
|
+
*/
|
|
364
|
+
openProfessional(input: ServerProfessionalOpenInput): Promise<OpenedServerProfessional>;
|
|
278
365
|
lock(ownerId: string, sessionId: string): Promise<void>;
|
|
279
366
|
private createClient;
|
|
280
367
|
private createWallet;
|
|
@@ -6,6 +6,9 @@ import { addLegalRepresentativeCredential, addOrganizationCredential, addService
|
|
|
6
6
|
import { TransportProfiles, } from 'gdc-sdk-core-ts';
|
|
7
7
|
import { NodeManagedWallet } from './node-managed-wallet.js';
|
|
8
8
|
import { NodeHttpClient } from './node-runtime-client.js';
|
|
9
|
+
import { OrganizationControllerSdk } from './orchestration/organization-controller-sdk.js';
|
|
10
|
+
import { ProfessionalSdk } from './orchestration/professional-sdk.js';
|
|
11
|
+
import { DigitalTwinSdk } from './orchestration/digital-twin-sdk.js';
|
|
9
12
|
import { buildIdentityOpenIdSmartTokenPath } from './runtime-paths.js';
|
|
10
13
|
import { createProfileDeviceActivationRequest } from './device-activation.js';
|
|
11
14
|
import { ProfilePinRejectedError, openServerProfileSecret, protectServerProfileSecret, } from './server-profile-protection.js';
|
|
@@ -408,6 +411,159 @@ export class ServerProfileSessionManager {
|
|
|
408
411
|
},
|
|
409
412
|
};
|
|
410
413
|
}
|
|
414
|
+
/**
|
|
415
|
+
* Opens the high-level organization-controller API without exposing wallet,
|
|
416
|
+
* DIDComm, DCR or HTTP-client plumbing to the integrating BFF.
|
|
417
|
+
*
|
|
418
|
+
* The durable profile remains authoritative for actor DID, provider DID,
|
|
419
|
+
* route and DCR client id. The reconstructed keys must match the public keys
|
|
420
|
+
* registered for that profile before any gateway request can be sent.
|
|
421
|
+
*/
|
|
422
|
+
async openOrganizationController(input) {
|
|
423
|
+
const profile = await this.requireOwnedProfile(input.ownerId, input.profileId);
|
|
424
|
+
if (profile.actorKind !== ActorKinds.OrganizationController || profile.actorMode !== 'controller') {
|
|
425
|
+
throw new Error('Profile is not an organization controller.');
|
|
426
|
+
}
|
|
427
|
+
const idToken = String(input.idToken || '').trim();
|
|
428
|
+
if (!idToken)
|
|
429
|
+
throw new Error('Opening an organization controller requires a signed OIDC idToken.');
|
|
430
|
+
const hasPin = Boolean(String(input.pin || '').trim());
|
|
431
|
+
const hasAuthorizedSeed = Boolean(String(input.authorizedWalletSeed || '').trim());
|
|
432
|
+
if (hasPin === hasAuthorizedSeed) {
|
|
433
|
+
throw new Error('Opening an organization controller requires exactly one of pin or authorizedWalletSeed.');
|
|
434
|
+
}
|
|
435
|
+
const seed = hasAuthorizedSeed
|
|
436
|
+
? String(input.authorizedWalletSeed)
|
|
437
|
+
: await openServerProfileSecret(profile.protectedWalletSeed, String(input.pin), `${profile.profileId}:wallet-seed`, this.options.sealer);
|
|
438
|
+
requireBase64UrlSeed32(seed);
|
|
439
|
+
const walletKeyDerivationId = normalizedWalletKeyDerivationId(profile.walletKeyDerivationId, profile.profileId);
|
|
440
|
+
const wallet = await this.createWallet(walletKeyDerivationId, seed);
|
|
441
|
+
const context = walletContext(walletKeyDerivationId);
|
|
442
|
+
await requireRegisteredProfileKeys(wallet, context, profile);
|
|
443
|
+
const secureTransportAdapter = {
|
|
444
|
+
pack: (message) => wallet.packForRecipientWithContext(bindOrganizationControllerTransport(message, profile), profile.providerDid, { context }),
|
|
445
|
+
unpack: async (jwe) => (await wallet.unpackWithContext(jwe, { context })).content,
|
|
446
|
+
};
|
|
447
|
+
const client = new NodeHttpClient({
|
|
448
|
+
baseUrl: this.options.gatewayBaseUrl,
|
|
449
|
+
ctx: profile.routeContext,
|
|
450
|
+
bearerToken: idToken,
|
|
451
|
+
fetchImpl: this.options.fetchImpl,
|
|
452
|
+
appInfo: this.options.appInfo,
|
|
453
|
+
transportProfile: TransportProfiles.DidcommEncryptedForm,
|
|
454
|
+
secureTransportAdapter,
|
|
455
|
+
});
|
|
456
|
+
return { profile, sdk: new OrganizationControllerSdk(client) };
|
|
457
|
+
}
|
|
458
|
+
/**
|
|
459
|
+
* Opens one registered organization employee/professional without exposing
|
|
460
|
+
* wallet reconstruction, private_key_jwt, VP or transport plumbing.
|
|
461
|
+
*
|
|
462
|
+
* The product authorizes the PIN/passkey/session and supplies business role
|
|
463
|
+
* evidence. The SDK revalidates the registered key set, signs a fresh role
|
|
464
|
+
* VP independently from the OIDC account proof, derives the SMART endpoint
|
|
465
|
+
* from the durable profile route and owns the client assertion.
|
|
466
|
+
*/
|
|
467
|
+
async openProfessional(input) {
|
|
468
|
+
const profile = await this.requireOwnedProfile(input.ownerId, input.profileId);
|
|
469
|
+
const isProfessional = profile.actorKind === ActorKinds.OrganizationEmployee
|
|
470
|
+
|| profile.actorKind === ActorKinds.Professional;
|
|
471
|
+
if (!isProfessional || profile.actorMode !== 'member') {
|
|
472
|
+
throw new Error('Profile is not an organization employee or professional.');
|
|
473
|
+
}
|
|
474
|
+
const idToken = String(input.idToken || '').trim();
|
|
475
|
+
const role = String(input.professionalProof?.role || '').trim();
|
|
476
|
+
if (!idToken)
|
|
477
|
+
throw new Error('Opening a professional requires a signed OIDC idToken.');
|
|
478
|
+
if (!role)
|
|
479
|
+
throw new Error('Opening a professional requires role proof.');
|
|
480
|
+
const hasPin = Boolean(String(input.pin || '').trim());
|
|
481
|
+
const hasAuthorizedSeed = Boolean(String(input.authorizedWalletSeed || '').trim());
|
|
482
|
+
if (hasPin === hasAuthorizedSeed) {
|
|
483
|
+
throw new Error('Opening a professional requires exactly one of pin or authorizedWalletSeed.');
|
|
484
|
+
}
|
|
485
|
+
const seed = hasAuthorizedSeed
|
|
486
|
+
? String(input.authorizedWalletSeed)
|
|
487
|
+
: await openServerProfileSecret(profile.protectedWalletSeed, String(input.pin), `${profile.profileId}:wallet-seed`, this.options.sealer);
|
|
488
|
+
requireBase64UrlSeed32(seed);
|
|
489
|
+
const walletKeyDerivationId = normalizedWalletKeyDerivationId(profile.walletKeyDerivationId, profile.profileId);
|
|
490
|
+
const wallet = await this.createWallet(walletKeyDerivationId, seed);
|
|
491
|
+
const context = walletContext(walletKeyDerivationId);
|
|
492
|
+
await requireRegisteredProfileKeys(wallet, context, profile);
|
|
493
|
+
const secureTransportAdapter = {
|
|
494
|
+
pack: (message) => wallet.packForRecipientWithContext(bindOrganizationControllerTransport(message, profile), profile.providerDid, { context }),
|
|
495
|
+
unpack: async (jwe) => (await wallet.unpackWithContext(jwe, { context })).content,
|
|
496
|
+
};
|
|
497
|
+
const operationalClient = new NodeHttpClient({
|
|
498
|
+
baseUrl: this.options.gatewayBaseUrl,
|
|
499
|
+
ctx: profile.routeContext,
|
|
500
|
+
bearerToken: idToken,
|
|
501
|
+
fetchImpl: this.options.fetchImpl,
|
|
502
|
+
appInfo: this.options.appInfo,
|
|
503
|
+
transportProfile: TransportProfiles.DidcommEncryptedForm,
|
|
504
|
+
secureTransportAdapter,
|
|
505
|
+
});
|
|
506
|
+
// SMART/OpenID uses its own JSON/form protocol and must not be wrapped in
|
|
507
|
+
// the encrypted resource transport selected for later business actions.
|
|
508
|
+
const sdk = new ProfessionalSdk(this.createClient(profile.routeContext, idToken));
|
|
509
|
+
const digitalTwin = new DigitalTwinSdk(operationalClient, profile.actorDid);
|
|
510
|
+
let digitalTwinAccessToken;
|
|
511
|
+
const request = async (smart) => {
|
|
512
|
+
if (!smart.scopes?.length)
|
|
513
|
+
throw new Error('Professional SMART request requires scopes.');
|
|
514
|
+
const now = this.now();
|
|
515
|
+
const audience = [
|
|
516
|
+
this.options.gatewayBaseUrl.replace(/\/+$/, ''),
|
|
517
|
+
buildIdentityOpenIdSmartTokenPath(profile.routeContext),
|
|
518
|
+
].join('');
|
|
519
|
+
const clientAssertion = await buildWalletClientAssertion(wallet, profile, audience, now);
|
|
520
|
+
const vpToken = await buildManagedProfessionalVp(wallet, context, {
|
|
521
|
+
clientId: profile.clientId,
|
|
522
|
+
actorDid: profile.actorDid,
|
|
523
|
+
profileDid: profile.profileDid,
|
|
524
|
+
...input.professionalProof,
|
|
525
|
+
role,
|
|
526
|
+
});
|
|
527
|
+
return sdk.requestSmartToken({
|
|
528
|
+
...profile.routeContext,
|
|
529
|
+
actorDid: profile.actorDid,
|
|
530
|
+
subjectDid: String(smart.subjectDid || '').trim() || undefined,
|
|
531
|
+
clientId: profile.clientId,
|
|
532
|
+
issuer: profile.clientId,
|
|
533
|
+
audience,
|
|
534
|
+
idToken,
|
|
535
|
+
vpToken,
|
|
536
|
+
vpTokenFallback: 'omit',
|
|
537
|
+
clientAssertion,
|
|
538
|
+
clientAssertionType: 'private_key_jwt',
|
|
539
|
+
smartTokenKind: 'openid-smart',
|
|
540
|
+
acrValues: smart.acrValues || profileSmartAcrValues(profile.actorKind),
|
|
541
|
+
purpose: smart.purpose,
|
|
542
|
+
scopes: unique(smart.scopes),
|
|
543
|
+
requestBodyClaims: smart.requestBodyClaims,
|
|
544
|
+
tokenCacheKey: smart.tokenCacheKey,
|
|
545
|
+
timeoutSeconds: smart.timeoutSeconds,
|
|
546
|
+
intervalSeconds: smart.intervalSeconds,
|
|
547
|
+
});
|
|
548
|
+
};
|
|
549
|
+
return {
|
|
550
|
+
profile,
|
|
551
|
+
sdk,
|
|
552
|
+
digitalTwin,
|
|
553
|
+
requestSmartToken: request,
|
|
554
|
+
requestDigitalTwinSmartToken: async (smart) => {
|
|
555
|
+
const result = await request(smart);
|
|
556
|
+
if (result.accessToken)
|
|
557
|
+
digitalTwinAccessToken = result.accessToken;
|
|
558
|
+
return result;
|
|
559
|
+
},
|
|
560
|
+
searchDigitalTwins: (search) => {
|
|
561
|
+
if (!digitalTwinAccessToken)
|
|
562
|
+
throw new Error('Digital twin SMART token has not been granted.');
|
|
563
|
+
return digitalTwin.search(profile.routeContext, { ...search, accessToken: digitalTwinAccessToken });
|
|
564
|
+
},
|
|
565
|
+
};
|
|
566
|
+
}
|
|
411
567
|
async lock(ownerId, sessionId) {
|
|
412
568
|
const session = await this.options.store.getSession(sessionId);
|
|
413
569
|
if (session?.ownerId === ownerId)
|
|
@@ -489,6 +645,29 @@ export class ServerProfileSessionManager {
|
|
|
489
645
|
function bindTransportActor(message, actorDid, clientId) {
|
|
490
646
|
return { ...message, iss: actorDid, client_id: clientId };
|
|
491
647
|
}
|
|
648
|
+
/** Binds the encrypted request to the immutable controller registration. */
|
|
649
|
+
function bindOrganizationControllerTransport(message, profile) {
|
|
650
|
+
return {
|
|
651
|
+
...message,
|
|
652
|
+
iss: profile.actorDid,
|
|
653
|
+
aud: profile.providerDid,
|
|
654
|
+
client_id: profile.clientId,
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
/** Refuses a wrong or rotated seed before it can produce network traffic. */
|
|
658
|
+
async function requireRegisteredProfileKeys(wallet, context, profile) {
|
|
659
|
+
const derived = await wallet.getPublicJwks(context, {});
|
|
660
|
+
const derivedKids = new Set(derived.map((entry) => String(entry.kid || '').trim()).filter(Boolean));
|
|
661
|
+
const registeredKids = profile.publicJwks
|
|
662
|
+
.map((jwk) => String(jwk.kid || '').trim())
|
|
663
|
+
.filter(Boolean);
|
|
664
|
+
if (!registeredKids.length || registeredKids.some((kid) => !derivedKids.has(kid))) {
|
|
665
|
+
throw new Error('Authorized wallet seed does not match registered profile keys.');
|
|
666
|
+
}
|
|
667
|
+
if (profile.clientInstanceId && !derivedKids.has(profile.clientInstanceId)) {
|
|
668
|
+
throw new Error('Authorized wallet seed does not match the registered client instance.');
|
|
669
|
+
}
|
|
670
|
+
}
|
|
492
671
|
/** Keeps the OpenID proof class aligned with the durable actor profile. */
|
|
493
672
|
function profileSmartAcrValues(actorKind) {
|
|
494
673
|
return actorKind === ActorKinds.IndividualController
|