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);
|