gdc-sdk-node-ts 2.4.41 → 2.4.43

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/dist/index.d.ts CHANGED
@@ -4,6 +4,7 @@ export * from './identity-bootstrap.js';
4
4
  export * from './async-polling.js';
5
5
  export * from './backend-profile-runtime.js';
6
6
  export * from './server-profile-session.js';
7
+ export * from './profile-recovery-envelope.js';
7
8
  export * from './server-profile-protection.js';
8
9
  export * from './backend-profile-workspace.js';
9
10
  export * from './individual-controller-backend-runtime.js';
package/dist/index.js CHANGED
@@ -5,6 +5,7 @@ export * from './identity-bootstrap.js';
5
5
  export * from './async-polling.js';
6
6
  export * from './backend-profile-runtime.js';
7
7
  export * from './server-profile-session.js';
8
+ export * from './profile-recovery-envelope.js';
8
9
  export * from './server-profile-protection.js';
9
10
  export * from './backend-profile-workspace.js';
10
11
  export * from './individual-controller-backend-runtime.js';
@@ -0,0 +1,37 @@
1
+ export type PortableProfileRecoveryEnvelope = Readonly<{
2
+ version: 'gdc-portable-profile-recovery-v1';
3
+ profileId: string;
4
+ walletKeyDerivationId: string;
5
+ kdf: Readonly<{
6
+ name: 'scrypt';
7
+ saltBase64Url: string;
8
+ cost: number;
9
+ blockSize: number;
10
+ parallelization: number;
11
+ keyLength: 32;
12
+ }>;
13
+ cipher: 'A256GCM';
14
+ ivBase64Url: string;
15
+ ciphertextBase64Url: string;
16
+ tagBase64Url: string;
17
+ }>;
18
+ export type PortableProfileRecoveryProtection = Readonly<{
19
+ cost?: number;
20
+ blockSize?: number;
21
+ parallelization?: number;
22
+ }>;
23
+ export declare function createPortableProfileRecoveryEnvelope(input: Readonly<{
24
+ profileId: string;
25
+ walletSeed: string;
26
+ walletKeyDerivationId: string;
27
+ recoverySecret: string;
28
+ protection?: PortableProfileRecoveryProtection;
29
+ }>): PortableProfileRecoveryEnvelope;
30
+ export declare function openPortableProfileRecoveryEnvelope(input: Readonly<{
31
+ envelope: PortableProfileRecoveryEnvelope;
32
+ recoverySecret: string;
33
+ }>): Readonly<{
34
+ profileId: string;
35
+ walletSeed: string;
36
+ walletKeyDerivationId: string;
37
+ }>;
@@ -0,0 +1,113 @@
1
+ // Copyright 2026 Antifraud Services Inc. under the Apache License, Version 2.0.
2
+ import { createCipheriv, createDecipheriv, randomBytes, scryptSync } from 'node:crypto';
3
+ export function createPortableProfileRecoveryEnvelope(input) {
4
+ const profileId = requiredText(input.profileId, 'profileId');
5
+ const walletKeyDerivationId = requiredText(input.walletKeyDerivationId, 'walletKeyDerivationId');
6
+ requireSeed(input.walletSeed);
7
+ requireRecoverySecret(input.recoverySecret);
8
+ const kdf = {
9
+ name: 'scrypt',
10
+ saltBase64Url: randomBytes(16).toString('base64url'),
11
+ cost: input.protection?.cost ?? 65536,
12
+ blockSize: input.protection?.blockSize ?? 8,
13
+ parallelization: input.protection?.parallelization ?? 1,
14
+ keyLength: 32,
15
+ };
16
+ const key = deriveKey(input.recoverySecret, kdf);
17
+ const iv = randomBytes(12);
18
+ const aad = envelopeAad(profileId, walletKeyDerivationId);
19
+ try {
20
+ const cipher = createCipheriv('aes-256-gcm', key, iv);
21
+ cipher.setAAD(aad);
22
+ const ciphertext = Buffer.concat([
23
+ cipher.update(JSON.stringify({ walletSeed: input.walletSeed }), 'utf8'),
24
+ cipher.final(),
25
+ ]);
26
+ return {
27
+ version: 'gdc-portable-profile-recovery-v1',
28
+ profileId,
29
+ walletKeyDerivationId,
30
+ kdf,
31
+ cipher: 'A256GCM',
32
+ ivBase64Url: iv.toString('base64url'),
33
+ ciphertextBase64Url: ciphertext.toString('base64url'),
34
+ tagBase64Url: cipher.getAuthTag().toString('base64url'),
35
+ };
36
+ }
37
+ finally {
38
+ key.fill(0);
39
+ }
40
+ }
41
+ export function openPortableProfileRecoveryEnvelope(input) {
42
+ const envelope = input.envelope;
43
+ try {
44
+ validateEnvelope(envelope);
45
+ requireRecoverySecret(input.recoverySecret);
46
+ const key = deriveKey(input.recoverySecret, envelope.kdf);
47
+ try {
48
+ const decipher = createDecipheriv('aes-256-gcm', key, Buffer.from(envelope.ivBase64Url, 'base64url'));
49
+ decipher.setAAD(envelopeAad(envelope.profileId, envelope.walletKeyDerivationId));
50
+ decipher.setAuthTag(Buffer.from(envelope.tagBase64Url, 'base64url'));
51
+ const cleartext = Buffer.concat([
52
+ decipher.update(Buffer.from(envelope.ciphertextBase64Url, 'base64url')),
53
+ decipher.final(),
54
+ ]).toString('utf8');
55
+ const payload = JSON.parse(cleartext);
56
+ const walletSeed = String(payload.walletSeed || '');
57
+ requireSeed(walletSeed);
58
+ return {
59
+ profileId: envelope.profileId,
60
+ walletSeed,
61
+ walletKeyDerivationId: envelope.walletKeyDerivationId,
62
+ };
63
+ }
64
+ finally {
65
+ key.fill(0);
66
+ }
67
+ }
68
+ catch {
69
+ throw new Error('Portable profile recovery envelope could not be opened.');
70
+ }
71
+ }
72
+ function deriveKey(secret, kdf) {
73
+ return scryptSync(secret, Buffer.from(kdf.saltBase64Url, 'base64url'), kdf.keyLength, {
74
+ N: kdf.cost,
75
+ r: kdf.blockSize,
76
+ p: kdf.parallelization,
77
+ maxmem: Math.max(128 * 1024 * 1024, 256 * kdf.cost * kdf.blockSize),
78
+ });
79
+ }
80
+ function envelopeAad(profileId, walletKeyDerivationId) {
81
+ return Buffer.from(JSON.stringify({
82
+ version: 'gdc-portable-profile-recovery-v1',
83
+ profileId,
84
+ walletKeyDerivationId,
85
+ }));
86
+ }
87
+ function requireRecoverySecret(value) {
88
+ const secret = String(value || '');
89
+ if (secret.length < 16 || /^\d+$/.test(secret)) {
90
+ throw new Error('Portable recovery requires a high-entropy recovery secret, not a short PIN.');
91
+ }
92
+ }
93
+ function requireSeed(value) {
94
+ if (!/^[A-Za-z0-9_-]{43}$/.test(value) || Buffer.from(value, 'base64url').byteLength !== 32) {
95
+ throw new Error('Portable recovery requires a 32-byte base64url wallet seed.');
96
+ }
97
+ }
98
+ function requiredText(value, name) {
99
+ const normalized = String(value || '').trim();
100
+ if (!normalized)
101
+ throw new Error(`Portable recovery requires ${name}.`);
102
+ return normalized;
103
+ }
104
+ function validateEnvelope(value) {
105
+ if (value?.version !== 'gdc-portable-profile-recovery-v1'
106
+ || value.cipher !== 'A256GCM'
107
+ || value.kdf?.name !== 'scrypt'
108
+ || value.kdf.keyLength !== 32
109
+ || !value.profileId
110
+ || !value.walletKeyDerivationId) {
111
+ throw new Error('Unsupported portable profile recovery envelope.');
112
+ }
113
+ }
@@ -3,6 +3,10 @@ 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
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';
6
10
  import type { RouteContext } from './individual-onboarding.js';
7
11
  import type { HostRouteContext } from './host-onboarding.js';
8
12
  import type { SecureDidcommTransportAdapter } from 'gdc-sdk-core-ts';
@@ -70,6 +74,15 @@ export type ServerEmployeeProfileOtpRotationInput = Readonly<{
70
74
  clientName: string;
71
75
  }>;
72
76
  export type ServerEmployeeProfileOtpRotationResult = ServerProfileRecord;
77
+ export type ServerProfilePinReplacementInput = Readonly<{
78
+ ownerId: string;
79
+ profileId: string;
80
+ /** Server-authorized seed obtained from a passkey, active wallet session or recovery envelope. */
81
+ authorizedWalletSeed: string;
82
+ /** Required when the durable profile still carries a separately protected legacy VP. */
83
+ authorizedVpToken?: string;
84
+ newPin: string;
85
+ }>;
73
86
  export type ServerProfileEnrollmentInput = Readonly<{
74
87
  /** Stable confidential id of the authenticated portal account that owns and lists this profile. */
75
88
  ownerId: string;
@@ -228,6 +241,47 @@ export type OpenedServerOrganizationController = Readonly<{
228
241
  profile: ServerProfileRecord;
229
242
  sdk: OrganizationControllerSdk;
230
243
  }>;
244
+ /** Server-owned role evidence used to sign a fresh professional VP. */
245
+ export type ServerProfessionalProofInput = Readonly<{
246
+ role: string;
247
+ email?: string;
248
+ sameAs?: string | readonly string[];
249
+ telephone?: string;
250
+ credentialMaterial?: string;
251
+ }>;
252
+ /** Reopens one enrolled employee/professional profile behind the BFF boundary. */
253
+ export type ServerProfessionalOpenInput = Readonly<{
254
+ ownerId: string;
255
+ profileId: string;
256
+ /** Fresh signed OIDC id_token proving the authenticated account/email. */
257
+ idToken: string;
258
+ /** Role evidence signed by the managed DCR wallet, independently of idToken. */
259
+ professionalProof: ServerProfessionalProofInput;
260
+ /** Manager-owned PIN path; mutually exclusive with authorizedWalletSeed. */
261
+ pin?: string;
262
+ /** Server-only seed already authorized by a product passkey/session policy. */
263
+ authorizedWalletSeed?: string;
264
+ }>;
265
+ /** Business authorization requested from SMART; OpenID/JWT fields stay SDK-owned. */
266
+ export type ServerProfessionalSmartTokenInput = Readonly<{
267
+ subjectDid?: string;
268
+ purpose?: string;
269
+ scopes: string[];
270
+ acrValues?: string;
271
+ requestBodyClaims?: Record<string, unknown>;
272
+ tokenCacheKey?: string;
273
+ timeoutSeconds?: number;
274
+ intervalSeconds?: number;
275
+ }>;
276
+ /** Role-scoped professional facades with managed SMART proof methods. */
277
+ export type OpenedServerProfessional = Readonly<{
278
+ profile: ServerProfileRecord;
279
+ sdk: ProfessionalSdk;
280
+ digitalTwin: DigitalTwinSdk;
281
+ requestSmartToken(input: ServerProfessionalSmartTokenInput): Promise<SmartTokenExchangeResult>;
282
+ requestDigitalTwinSmartToken(input: ServerProfessionalSmartTokenInput): Promise<SmartTokenExchangeResult>;
283
+ searchDigitalTwins(input: Omit<DigitalTwinSearchInput, 'accessToken'>): Promise<DigitalTwinSearchResult>;
284
+ }>;
231
285
  export type ServerProfileSessionManagerOptions = Readonly<{
232
286
  store: ServerProfileStore;
233
287
  sealer: ServerProfileSealer;
@@ -290,6 +344,12 @@ export declare class ServerProfileSessionManager {
290
344
  /** @deprecated Use `submitLegalOrganizationCredentialReissuanceWithBootstrapWallet`. */
291
345
  submitLegalOrganizationIssueWithBootstrapWallet(input: ServerProfileOrganizationIssueInput): Promise<SubmitAndPollResult>;
292
346
  listProfiles(ownerId: string): Promise<ServerProfileRecord[]>;
347
+ /**
348
+ * Rewraps the exact registered wallet under a new PIN after a separate
349
+ * server-authorized recovery factor has released its secrets. No GW call or
350
+ * DCR occurs because proof-of-possession and every public key remain intact.
351
+ */
352
+ replaceProfilePinFromAuthorizedSecrets(input: ServerProfilePinReplacementInput): Promise<ServerProfileRecord>;
293
353
  unlock(input: ServerProfileUnlockInput): Promise<ResolvedServerProfileSession>;
294
354
  /**
295
355
  * Renews the short SMART bearer while retaining the already-open server
@@ -307,6 +367,16 @@ export declare class ServerProfileSessionManager {
307
367
  * registered for that profile before any gateway request can be sent.
308
368
  */
309
369
  openOrganizationController(input: ServerOrganizationControllerOpenInput): Promise<OpenedServerOrganizationController>;
370
+ /**
371
+ * Opens one registered organization employee/professional without exposing
372
+ * wallet reconstruction, private_key_jwt, VP or transport plumbing.
373
+ *
374
+ * The product authorizes the PIN/passkey/session and supplies business role
375
+ * evidence. The SDK revalidates the registered key set, signs a fresh role
376
+ * VP independently from the OIDC account proof, derives the SMART endpoint
377
+ * from the durable profile route and owns the client assertion.
378
+ */
379
+ openProfessional(input: ServerProfessionalOpenInput): Promise<OpenedServerProfessional>;
310
380
  lock(ownerId: string, sessionId: string): Promise<void>;
311
381
  private createClient;
312
382
  private createWallet;
@@ -7,6 +7,8 @@ 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
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';
10
12
  import { buildIdentityOpenIdSmartTokenPath } from './runtime-paths.js';
11
13
  import { createProfileDeviceActivationRequest } from './device-activation.js';
12
14
  import { ProfilePinRejectedError, openServerProfileSecret, protectServerProfileSecret, } from './server-profile-protection.js';
@@ -254,6 +256,38 @@ export class ServerProfileSessionManager {
254
256
  listProfiles(ownerId) {
255
257
  return this.options.store.listProfiles(ownerId);
256
258
  }
259
+ /**
260
+ * Rewraps the exact registered wallet under a new PIN after a separate
261
+ * server-authorized recovery factor has released its secrets. No GW call or
262
+ * DCR occurs because proof-of-possession and every public key remain intact.
263
+ */
264
+ async replaceProfilePinFromAuthorizedSecrets(input) {
265
+ const profile = await this.requireOwnedProfile(input.ownerId, input.profileId);
266
+ const seed = String(input.authorizedWalletSeed || '').trim();
267
+ requireBase64UrlSeed32(seed);
268
+ if (!this.options.store.deleteSessionsForProfile) {
269
+ throw new Error('Profile PIN replacement requires profile-session invalidation support.');
270
+ }
271
+ if (profile.protectedVpToken && !String(input.authorizedVpToken || '').trim()) {
272
+ throw new Error('Profile PIN replacement requires the authorized VP token.');
273
+ }
274
+ const walletKeyDerivationId = normalizedWalletKeyDerivationId(profile.walletKeyDerivationId, profile.profileId);
275
+ const wallet = await this.createWallet(walletKeyDerivationId, seed);
276
+ await requireRegisteredProfileKeys(wallet, walletContext(walletKeyDerivationId), profile);
277
+ const updated = {
278
+ ...profile,
279
+ protectedWalletSeed: await protectServerProfileSecret(seed, input.newPin, `${profile.profileId}:wallet-seed`, this.options.sealer, this.options.profileProtection),
280
+ ...(profile.protectedVpToken ? {
281
+ protectedVpToken: await protectServerProfileSecret(String(input.authorizedVpToken), input.newPin, `${profile.profileId}:vp-token`, this.options.sealer, this.options.profileProtection),
282
+ } : {}),
283
+ failedUnlocks: 0,
284
+ lockedUntil: undefined,
285
+ updatedAt: this.now().toISOString(),
286
+ };
287
+ await this.options.store.putProfile(updated);
288
+ await this.options.store.deleteSessionsForProfile(profile.profileId);
289
+ return updated;
290
+ }
257
291
  async unlock(input) {
258
292
  let profile = await this.requireOwnedProfile(input.ownerId, input.profileId);
259
293
  this.requireSubject(profile, input.subjectDid);
@@ -453,6 +487,115 @@ export class ServerProfileSessionManager {
453
487
  });
454
488
  return { profile, sdk: new OrganizationControllerSdk(client) };
455
489
  }
490
+ /**
491
+ * Opens one registered organization employee/professional without exposing
492
+ * wallet reconstruction, private_key_jwt, VP or transport plumbing.
493
+ *
494
+ * The product authorizes the PIN/passkey/session and supplies business role
495
+ * evidence. The SDK revalidates the registered key set, signs a fresh role
496
+ * VP independently from the OIDC account proof, derives the SMART endpoint
497
+ * from the durable profile route and owns the client assertion.
498
+ */
499
+ async openProfessional(input) {
500
+ const profile = await this.requireOwnedProfile(input.ownerId, input.profileId);
501
+ const isProfessional = profile.actorKind === ActorKinds.OrganizationEmployee
502
+ || profile.actorKind === ActorKinds.Professional;
503
+ if (!isProfessional || profile.actorMode !== 'member') {
504
+ throw new Error('Profile is not an organization employee or professional.');
505
+ }
506
+ const idToken = String(input.idToken || '').trim();
507
+ const role = String(input.professionalProof?.role || '').trim();
508
+ if (!idToken)
509
+ throw new Error('Opening a professional requires a signed OIDC idToken.');
510
+ if (!role)
511
+ throw new Error('Opening a professional requires role proof.');
512
+ const hasPin = Boolean(String(input.pin || '').trim());
513
+ const hasAuthorizedSeed = Boolean(String(input.authorizedWalletSeed || '').trim());
514
+ if (hasPin === hasAuthorizedSeed) {
515
+ throw new Error('Opening a professional requires exactly one of pin or authorizedWalletSeed.');
516
+ }
517
+ const seed = hasAuthorizedSeed
518
+ ? String(input.authorizedWalletSeed)
519
+ : await openServerProfileSecret(profile.protectedWalletSeed, String(input.pin), `${profile.profileId}:wallet-seed`, this.options.sealer);
520
+ requireBase64UrlSeed32(seed);
521
+ const walletKeyDerivationId = normalizedWalletKeyDerivationId(profile.walletKeyDerivationId, profile.profileId);
522
+ const wallet = await this.createWallet(walletKeyDerivationId, seed);
523
+ const context = walletContext(walletKeyDerivationId);
524
+ await requireRegisteredProfileKeys(wallet, context, profile);
525
+ const secureTransportAdapter = {
526
+ pack: (message) => wallet.packForRecipientWithContext(bindOrganizationControllerTransport(message, profile), profile.providerDid, { context }),
527
+ unpack: async (jwe) => (await wallet.unpackWithContext(jwe, { context })).content,
528
+ };
529
+ const operationalClient = new NodeHttpClient({
530
+ baseUrl: this.options.gatewayBaseUrl,
531
+ ctx: profile.routeContext,
532
+ bearerToken: idToken,
533
+ fetchImpl: this.options.fetchImpl,
534
+ appInfo: this.options.appInfo,
535
+ transportProfile: TransportProfiles.DidcommEncryptedForm,
536
+ secureTransportAdapter,
537
+ });
538
+ // SMART/OpenID uses its own JSON/form protocol and must not be wrapped in
539
+ // the encrypted resource transport selected for later business actions.
540
+ const sdk = new ProfessionalSdk(this.createClient(profile.routeContext, idToken));
541
+ const digitalTwin = new DigitalTwinSdk(operationalClient, profile.actorDid);
542
+ let digitalTwinAccessToken;
543
+ const request = async (smart) => {
544
+ if (!smart.scopes?.length)
545
+ throw new Error('Professional SMART request requires scopes.');
546
+ const now = this.now();
547
+ const audience = [
548
+ this.options.gatewayBaseUrl.replace(/\/+$/, ''),
549
+ buildIdentityOpenIdSmartTokenPath(profile.routeContext),
550
+ ].join('');
551
+ const clientAssertion = await buildWalletClientAssertion(wallet, profile, audience, now);
552
+ const vpToken = await buildManagedProfessionalVp(wallet, context, {
553
+ clientId: profile.clientId,
554
+ actorDid: profile.actorDid,
555
+ profileDid: profile.profileDid,
556
+ ...input.professionalProof,
557
+ role,
558
+ });
559
+ return sdk.requestSmartToken({
560
+ ...profile.routeContext,
561
+ actorDid: profile.actorDid,
562
+ subjectDid: String(smart.subjectDid || '').trim() || undefined,
563
+ clientId: profile.clientId,
564
+ issuer: profile.clientId,
565
+ audience,
566
+ idToken,
567
+ vpToken,
568
+ vpTokenFallback: 'omit',
569
+ clientAssertion,
570
+ clientAssertionType: 'private_key_jwt',
571
+ smartTokenKind: 'openid-smart',
572
+ acrValues: smart.acrValues || profileSmartAcrValues(profile.actorKind),
573
+ purpose: smart.purpose,
574
+ scopes: unique(smart.scopes),
575
+ requestBodyClaims: smart.requestBodyClaims,
576
+ tokenCacheKey: smart.tokenCacheKey,
577
+ timeoutSeconds: smart.timeoutSeconds,
578
+ intervalSeconds: smart.intervalSeconds,
579
+ });
580
+ };
581
+ return {
582
+ profile,
583
+ sdk,
584
+ digitalTwin,
585
+ requestSmartToken: request,
586
+ requestDigitalTwinSmartToken: async (smart) => {
587
+ const result = await request(smart);
588
+ if (result.accessToken)
589
+ digitalTwinAccessToken = result.accessToken;
590
+ return result;
591
+ },
592
+ searchDigitalTwins: (search) => {
593
+ if (!digitalTwinAccessToken)
594
+ throw new Error('Digital twin SMART token has not been granted.');
595
+ return digitalTwin.search(profile.routeContext, { ...search, accessToken: digitalTwinAccessToken });
596
+ },
597
+ };
598
+ }
456
599
  async lock(ownerId, sessionId) {
457
600
  const session = await this.options.store.getSession(sessionId);
458
601
  if (session?.ownerId === ownerId)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdc-sdk-node-ts",
3
- "version": "2.4.41",
3
+ "version": "2.4.43",
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.",