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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdc-sdk-node-ts",
3
- "version": "2.4.40",
3
+ "version": "2.4.42",
4
4
  "description": "Next-generation Node runtime package for the GDC SDK family",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Antifraud Services Inc.",