gdc-sdk-node-ts 2.4.42 → 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
+ }
@@ -74,6 +74,15 @@ export type ServerEmployeeProfileOtpRotationInput = Readonly<{
74
74
  clientName: string;
75
75
  }>;
76
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
+ }>;
77
86
  export type ServerProfileEnrollmentInput = Readonly<{
78
87
  /** Stable confidential id of the authenticated portal account that owns and lists this profile. */
79
88
  ownerId: string;
@@ -335,6 +344,12 @@ export declare class ServerProfileSessionManager {
335
344
  /** @deprecated Use `submitLegalOrganizationCredentialReissuanceWithBootstrapWallet`. */
336
345
  submitLegalOrganizationIssueWithBootstrapWallet(input: ServerProfileOrganizationIssueInput): Promise<SubmitAndPollResult>;
337
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>;
338
353
  unlock(input: ServerProfileUnlockInput): Promise<ResolvedServerProfileSession>;
339
354
  /**
340
355
  * Renews the short SMART bearer while retaining the already-open server
@@ -256,6 +256,38 @@ export class ServerProfileSessionManager {
256
256
  listProfiles(ownerId) {
257
257
  return this.options.store.listProfiles(ownerId);
258
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
+ }
259
291
  async unlock(input) {
260
292
  let profile = await this.requireOwnedProfile(input.ownerId, input.profileId);
261
293
  this.requireSubject(profile, input.subjectDid);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdc-sdk-node-ts",
3
- "version": "2.4.42",
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.",