@cryptforge/auth 0.1.0

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.mjs ADDED
@@ -0,0 +1,1174 @@
1
+ // src/AuthClient.ts
2
+ import {
3
+ generateMnemonic,
4
+ validateMnemonic,
5
+ mnemonicToSeedSync
6
+ } from "@scure/bip39";
7
+ import { wordlist } from "@scure/bip39/wordlists/english.js";
8
+ import { HDKey } from "@scure/bip32";
9
+ var AuthClient = class {
10
+ state = {
11
+ identity: null,
12
+ keys: null,
13
+ currentChain: null,
14
+ isLocked: true
15
+ };
16
+ listeners = /* @__PURE__ */ new Set();
17
+ lockTimer;
18
+ decryptedMnemonic = null;
19
+ // Blockchain adapter registry
20
+ adapters = /* @__PURE__ */ new Map();
21
+ currentAdapter = null;
22
+ /**
23
+ * Register a blockchain adapter for a specific chain
24
+ * @param chainId - Unique chain identifier (e.g., 'ethereum', 'bitcoin')
25
+ * @param adapter - BlockchainAdapter implementation
26
+ */
27
+ registerAdapter = (chainId, adapter) => {
28
+ this.adapters.set(chainId, adapter);
29
+ };
30
+ /**
31
+ * Get the adapter for a specific chain
32
+ * @param chainId - Chain identifier
33
+ * @returns BlockchainAdapter instance
34
+ * @throws Error if no adapter is registered for the chain
35
+ */
36
+ getAdapter(chainId) {
37
+ const adapter = this.adapters.get(chainId);
38
+ if (!adapter) {
39
+ throw new Error(
40
+ `No adapter registered for chain: ${chainId}. Please register an adapter using registerAdapter().`
41
+ );
42
+ }
43
+ return adapter;
44
+ }
45
+ /**
46
+ * Get all registered chain IDs
47
+ * @returns Array of registered chain IDs
48
+ */
49
+ getRegisteredChains = () => {
50
+ return Array.from(this.adapters.keys());
51
+ };
52
+ // Identity Creation & Restoration
53
+ /**
54
+ * Generates a BIP39 mnemonic phrase (12 or 24 words).
55
+ * @param options - Optional configuration for word count
56
+ * @returns BIP39 mnemonic phrase
57
+ */
58
+ generateMnemonic = (options) => {
59
+ const wordCount = options?.wordCount || 12;
60
+ const strength = wordCount === 24 ? 256 : 128;
61
+ return generateMnemonic(wordlist, strength);
62
+ };
63
+ /**
64
+ * Creates a new identity from a mnemonic and encrypts it with a password.
65
+ * Optionally unlocks with a specific blockchain.
66
+ * @param options - Identity creation options including mnemonic, password, and optional chainId
67
+ * @returns Promise resolving to the created identity and keys
68
+ * @throws {Error} If mnemonic is invalid
69
+ */
70
+ createIdentity = async (options) => {
71
+ const { mnemonic, password, label, metadata = {}, chainId } = options;
72
+ if (!validateMnemonic(mnemonic, wordlist)) {
73
+ throw new Error("Invalid mnemonic");
74
+ }
75
+ const fingerprint = getMasterFingerprint(mnemonic);
76
+ const publicKey = getMasterPublicKey(mnemonic);
77
+ const id = "identity_" + fingerprint;
78
+ const keystoreJson = await encryptMnemonic(mnemonic, password);
79
+ const storedIdentity = {
80
+ id,
81
+ fingerprint,
82
+ publicKey,
83
+ label: label || "Unnamed Wallet",
84
+ metadata,
85
+ keystore: keystoreJson,
86
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
87
+ lastAccess: (/* @__PURE__ */ new Date()).toISOString()
88
+ };
89
+ await saveIdentityToDB(storedIdentity);
90
+ const identity = {
91
+ id: storedIdentity.id,
92
+ publicKey: storedIdentity.publicKey,
93
+ fingerprint: storedIdentity.fingerprint,
94
+ label: storedIdentity.label,
95
+ metadata: storedIdentity.metadata,
96
+ createdAt: new Date(storedIdentity.createdAt),
97
+ lastAccess: storedIdentity.lastAccess ? new Date(storedIdentity.lastAccess) : void 0
98
+ };
99
+ this.state.identity = identity;
100
+ this.decryptedMnemonic = mnemonic;
101
+ let keys = null;
102
+ let chain = null;
103
+ if (chainId) {
104
+ const adapter = this.getAdapter(chainId);
105
+ keys = await this.deriveKeysWithAdapter(
106
+ mnemonic,
107
+ chainId,
108
+ adapter,
109
+ identity
110
+ );
111
+ chain = {
112
+ id: chainId,
113
+ name: adapter.chainData.name,
114
+ symbol: adapter.chainData.symbol
115
+ };
116
+ this.state.keys = keys;
117
+ this.state.currentChain = chain;
118
+ this.currentAdapter = adapter;
119
+ this.state.isLocked = false;
120
+ } else {
121
+ this.state.isLocked = true;
122
+ }
123
+ this.notifyListeners("IDENTITY_CREATED", keys);
124
+ return { identity, keys };
125
+ };
126
+ // Key Management
127
+ /**
128
+ * Unlocks the wallet by decrypting the keystore and deriving keys for a blockchain.
129
+ * @param options - Unlock options including password, chainId, and optional duration
130
+ * @returns Promise resolving to the derived keys
131
+ * @throws {Error} If password is incorrect or no identity is selected
132
+ */
133
+ unlock = async (options) => {
134
+ const { password, identityId, chainId, duration } = options;
135
+ let stored;
136
+ if (identityId) {
137
+ stored = await getIdentityFromDB(identityId);
138
+ } else if (this.state.identity) {
139
+ stored = await getIdentityFromDB(this.state.identity.id);
140
+ }
141
+ if (!stored) throw new Error("No identity selected");
142
+ const mnemonic = await decryptMnemonic(stored.keystore, password);
143
+ if (!validateMnemonic(mnemonic, wordlist)) {
144
+ throw new Error("Decrypted mnemonic is invalid");
145
+ }
146
+ this.decryptedMnemonic = mnemonic;
147
+ const identity = {
148
+ id: stored.id,
149
+ publicKey: stored.publicKey,
150
+ fingerprint: stored.fingerprint,
151
+ label: stored.label,
152
+ metadata: stored.metadata,
153
+ createdAt: new Date(stored.createdAt),
154
+ lastAccess: stored.lastAccess ? new Date(stored.lastAccess) : void 0
155
+ };
156
+ if (!chainId) {
157
+ throw new Error(
158
+ "chainId is required to unlock. Please specify which blockchain to use."
159
+ );
160
+ }
161
+ const adapter = this.getAdapter(chainId);
162
+ const keys = await this.deriveKeysWithAdapter(
163
+ mnemonic,
164
+ chainId,
165
+ adapter,
166
+ identity,
167
+ duration
168
+ // Pass duration to keys
169
+ );
170
+ const targetChain = {
171
+ id: chainId,
172
+ name: adapter.chainData.name,
173
+ symbol: adapter.chainData.symbol
174
+ };
175
+ this.state.identity = identity;
176
+ this.state.keys = keys;
177
+ this.state.currentChain = targetChain;
178
+ this.currentAdapter = adapter;
179
+ this.state.isLocked = false;
180
+ stored.lastAccess = (/* @__PURE__ */ new Date()).toISOString();
181
+ await saveIdentityToDB(stored);
182
+ if (duration) {
183
+ this.setAutoLockTimer(duration);
184
+ }
185
+ this.notifyListeners("UNLOCKED", keys);
186
+ return { keys };
187
+ };
188
+ /**
189
+ * Locks the wallet and clears all keys from memory.
190
+ * The encrypted keystore remains in IndexedDB.
191
+ * @returns Promise that resolves when wallet is locked
192
+ */
193
+ lock = async () => {
194
+ if (this.decryptedMnemonic) {
195
+ this.decryptedMnemonic = null;
196
+ }
197
+ this.state.keys = null;
198
+ this.state.isLocked = true;
199
+ if (this.lockTimer) {
200
+ clearTimeout(this.lockTimer);
201
+ this.lockTimer = void 0;
202
+ }
203
+ this.notifyListeners("LOCKED", null);
204
+ };
205
+ /**
206
+ * Switches to a different blockchain while preserving the same identity.
207
+ * @param chainId - Target blockchain identifier
208
+ * @param password - Optional password if wallet is locked
209
+ * @returns Promise resolving to keys for the new chain
210
+ * @throws {Error} If locked without password or adapter not registered
211
+ */
212
+ switchChain = async (chainId, password) => {
213
+ if (this.state.isLocked && !password) {
214
+ throw new Error(
215
+ "Identity is locked. Password required to switch chains."
216
+ );
217
+ }
218
+ let mnemonic = this.decryptedMnemonic;
219
+ if (!mnemonic && password && this.state.identity) {
220
+ const stored = await getIdentityFromDB(this.state.identity.id);
221
+ if (!stored) throw new Error("Identity not found");
222
+ mnemonic = await decryptMnemonic(stored.keystore, password);
223
+ this.decryptedMnemonic = mnemonic;
224
+ }
225
+ if (!mnemonic || !this.state.identity) {
226
+ throw new Error("No active identity or mnemonic");
227
+ }
228
+ const adapter = this.getAdapter(chainId);
229
+ const duration = this.state.keys ? this.state.keys.expiresAt.getTime() - Date.now() : void 0;
230
+ const keys = await this.deriveKeysWithAdapter(
231
+ mnemonic,
232
+ chainId,
233
+ adapter,
234
+ this.state.identity,
235
+ duration && duration > 0 ? duration : void 0
236
+ );
237
+ const newChain = {
238
+ id: chainId,
239
+ name: adapter.chainData.name,
240
+ symbol: adapter.chainData.symbol
241
+ };
242
+ this.state.keys = keys;
243
+ this.state.currentChain = newChain;
244
+ this.currentAdapter = adapter;
245
+ this.state.isLocked = false;
246
+ this.notifyListeners("CHAIN_SWITCHED", keys);
247
+ return { keys };
248
+ };
249
+ /**
250
+ * Rotates to a new set of keys at the next address index or custom path.
251
+ * Preserves key expiration times from current session.
252
+ * @param newDerivationPath - Optional custom BIP44 path (defaults to next index)
253
+ * @returns Promise resolving to the new keys
254
+ * @throws {Error} If wallet is locked
255
+ */
256
+ rotateKeys = async (newDerivationPath) => {
257
+ if (this.state.isLocked || !this.decryptedMnemonic || !this.state.identity || !this.currentAdapter || !this.state.currentChain) {
258
+ throw new Error("Identity must be unlocked to rotate keys");
259
+ }
260
+ if (newDerivationPath) {
261
+ const keyData2 = await this.currentAdapter.deriveKeysAtPath(
262
+ this.decryptedMnemonic,
263
+ newDerivationPath
264
+ );
265
+ const expiresAt2 = this.state.keys?.expiresAt || new Date(Date.now() + 36e5);
266
+ const expiresIn2 = this.state.keys?.expiresIn || 3600;
267
+ const keys2 = {
268
+ privateKey: keyData2.privateKey,
269
+ privateKeyHex: keyData2.privateKeyHex,
270
+ publicKey: keyData2.publicKey,
271
+ publicKeyHex: keyData2.publicKeyHex,
272
+ address: keyData2.address,
273
+ derivationPath: keyData2.path,
274
+ chain: this.state.currentChain,
275
+ expiresAt: expiresAt2,
276
+ expiresIn: expiresIn2,
277
+ identity: this.state.identity
278
+ };
279
+ this.state.keys = keys2;
280
+ this.notifyListeners("KEYS_ROTATED", keys2);
281
+ return { keys: keys2 };
282
+ }
283
+ const currentPath = this.state.keys?.derivationPath || "";
284
+ const pathParts = currentPath.split("/");
285
+ const lastIndex = parseInt(pathParts[pathParts.length - 1]) || 0;
286
+ const newIndex = lastIndex + 1;
287
+ const keyData = await this.currentAdapter.deriveKeysAtIndex(
288
+ this.decryptedMnemonic,
289
+ newIndex
290
+ );
291
+ const expiresAt = this.state.keys?.expiresAt || new Date(Date.now() + 36e5);
292
+ const expiresIn = this.state.keys?.expiresIn || 3600;
293
+ const keys = {
294
+ privateKey: keyData.privateKey,
295
+ privateKeyHex: keyData.privateKeyHex,
296
+ publicKey: keyData.publicKey,
297
+ publicKeyHex: keyData.publicKeyHex,
298
+ address: keyData.address,
299
+ derivationPath: keyData.path,
300
+ chain: this.state.currentChain,
301
+ expiresAt,
302
+ expiresIn,
303
+ identity: this.state.identity
304
+ };
305
+ this.state.keys = keys;
306
+ this.notifyListeners("KEYS_ROTATED", keys);
307
+ return { keys };
308
+ };
309
+ /**
310
+ * Derives a one-time key at a custom BIP44 path without storing it in the session.
311
+ * Useful for signing with different addresses or accounts.
312
+ * @param options - Derivation options with custom path
313
+ * @returns Promise resolving to the derived key data
314
+ * @throws {Error} If wallet is locked
315
+ */
316
+ deriveKey = async (options) => {
317
+ if (this.state.isLocked || !this.decryptedMnemonic || !this.currentAdapter) {
318
+ throw new Error("Identity must be unlocked to derive keys");
319
+ }
320
+ const keyData = await this.currentAdapter.deriveKeysAtPath(
321
+ this.decryptedMnemonic,
322
+ options.path
323
+ );
324
+ return {
325
+ privateKey: keyData.privateKeyHex,
326
+ publicKey: keyData.publicKeyHex,
327
+ address: keyData.address,
328
+ path: keyData.path
329
+ };
330
+ };
331
+ /**
332
+ * Derives a deterministic document ID using BIP44-style hierarchical derivation.
333
+ * Returns a hex-encoded ID (32 bytes / 64 hex characters).
334
+ * Path: m/44'/[appId]'/[account]'/[purpose]/[index]
335
+ * @param options - BIP44 derivation parameters (appId required, others default to 0)
336
+ * @returns Promise resolving to hex-encoded document ID (64 characters)
337
+ * @throws {Error} If wallet is locked or parameters are out of range
338
+ */
339
+ deriveBIP44DocumentID = async (options) => {
340
+ if (this.state.isLocked || !this.decryptedMnemonic) {
341
+ throw new Error(
342
+ "Wallet is locked. Call unlock() first to derive document IDs."
343
+ );
344
+ }
345
+ const { appId, account = 0, purpose = 0, index = 0 } = options;
346
+ if (appId === void 0 || appId === null) {
347
+ throw new Error(
348
+ "appId is required but was undefined. Please provide a valid appId number."
349
+ );
350
+ }
351
+ if (typeof appId !== "number" || isNaN(appId)) {
352
+ throw new Error(
353
+ `appId must be a valid number, received: ${typeof appId}`
354
+ );
355
+ }
356
+ const MAX_HARDENED = 2147483647;
357
+ if (appId < 0 || appId > MAX_HARDENED) {
358
+ throw new Error(
359
+ `Invalid appId: ${appId}. Must be between 0 and ${MAX_HARDENED}`
360
+ );
361
+ }
362
+ if (account < 0 || account > MAX_HARDENED) {
363
+ throw new Error(
364
+ `Invalid account: ${account}. Must be between 0 and ${MAX_HARDENED}`
365
+ );
366
+ }
367
+ if (purpose < 0 || purpose > MAX_HARDENED) {
368
+ throw new Error(
369
+ `Invalid purpose: ${purpose}. Must be between 0 and ${MAX_HARDENED}`
370
+ );
371
+ }
372
+ if (index < 0 || index > MAX_HARDENED) {
373
+ throw new Error(
374
+ `Invalid index: ${index}. Must be between 0 and ${MAX_HARDENED}`
375
+ );
376
+ }
377
+ const path = `m/44'/${appId}'/${account}'/${purpose}/${index}`;
378
+ const seed = mnemonicToSeedSync(this.decryptedMnemonic);
379
+ const masterKey = HDKey.fromMasterSeed(seed);
380
+ const derivedKey = masterKey.derive(path);
381
+ if (!derivedKey.publicKey) {
382
+ throw new Error(`Failed to derive key at path: ${path}`);
383
+ }
384
+ const publicKeyBuffer = new Uint8Array(derivedKey.publicKey);
385
+ const hashBuffer = await crypto.subtle.digest("SHA-256", publicKeyBuffer);
386
+ const hash = new Uint8Array(hashBuffer);
387
+ return bufferToHex(hash);
388
+ };
389
+ /**
390
+ * Derives a data encryption key using HKDF for encrypting/decrypting data.
391
+ * This key is deterministic (derived from mnemonic) and chain-independent.
392
+ * Perfect for document encryption, file storage, etc.
393
+ *
394
+ * @param options - Derivation options including purpose, version, algorithm
395
+ * @returns CryptoKey ready for use with Web Crypto API
396
+ *
397
+ * @example
398
+ * ```typescript
399
+ * const key = await auth.deriveDataEncryptionKey({
400
+ * purpose: 'automerge-documents',
401
+ * version: 1,
402
+ * });
403
+ *
404
+ * // Use with Web Crypto API
405
+ * const encrypted = await crypto.subtle.encrypt(
406
+ * { name: 'AES-GCM', iv },
407
+ * key,
408
+ * data
409
+ * );
410
+ * ```
411
+ */
412
+ deriveDataEncryptionKey = async (options) => {
413
+ if (this.state.isLocked || !this.decryptedMnemonic) {
414
+ throw new Error(
415
+ "Wallet is locked. Call unlock() first to derive data encryption keys."
416
+ );
417
+ }
418
+ const purpose = options.purpose;
419
+ const version = options.version || 1;
420
+ const algorithm = options.algorithm || "AES-GCM";
421
+ const length = options.length || 256;
422
+ const extractable = options.extractable || false;
423
+ const seed = mnemonicToSeedSync(this.decryptedMnemonic);
424
+ const info = new TextEncoder().encode(`CryptForge-${purpose}-v${version}`);
425
+ const seedBuffer = new Uint8Array(seed);
426
+ const keyMaterial = await crypto.subtle.importKey(
427
+ "raw",
428
+ seedBuffer,
429
+ "HKDF",
430
+ false,
431
+ ["deriveKey"]
432
+ );
433
+ const key = await crypto.subtle.deriveKey(
434
+ {
435
+ name: "HKDF",
436
+ hash: "SHA-256",
437
+ salt: new Uint8Array(),
438
+ // Empty salt (could be customized if needed)
439
+ info
440
+ },
441
+ keyMaterial,
442
+ {
443
+ name: algorithm,
444
+ length
445
+ },
446
+ extractable,
447
+ ["encrypt", "decrypt"]
448
+ );
449
+ return key;
450
+ };
451
+ /**
452
+ * Gets the address for a specific blockchain at a given index.
453
+ * @param chainId - Blockchain identifier
454
+ * @param index - Address index (default: 0)
455
+ * @returns Promise resolving to address, public key, and derivation path
456
+ * @throws {Error} If wallet is locked or adapter not registered
457
+ */
458
+ getAddressForChain = async (chainId, index = 0) => {
459
+ if (this.state.isLocked || !this.decryptedMnemonic) {
460
+ throw new Error("Identity must be unlocked to get addresses");
461
+ }
462
+ const adapter = this.getAdapter(chainId);
463
+ const result = await adapter.getAddressAtIndex(
464
+ this.decryptedMnemonic,
465
+ index
466
+ );
467
+ return {
468
+ address: result.address,
469
+ publicKey: result.publicKey,
470
+ derivationPath: result.path
471
+ };
472
+ };
473
+ /**
474
+ * Verify password without unlocking wallet
475
+ * Useful for transaction confirmation
476
+ * @param password - Password to verify
477
+ * @returns Promise resolving to true if password is correct
478
+ */
479
+ verifyPassword = async (password) => {
480
+ if (!this.state.identity) {
481
+ throw new Error("No identity selected");
482
+ }
483
+ try {
484
+ const stored = await getIdentityFromDB(this.state.identity.id);
485
+ if (!stored) return false;
486
+ await decryptMnemonic(stored.keystore, password);
487
+ return true;
488
+ } catch (error) {
489
+ return false;
490
+ }
491
+ };
492
+ // Cryptographic Operations
493
+ /**
494
+ * Signs a message using the current or specified private key.
495
+ * Performs soft expiration check (warns but doesn't block).
496
+ * @param options - Message and optional derivation path
497
+ * @returns Promise resolving to signature, address, and public key
498
+ * @throws {Error} If wallet is locked
499
+ */
500
+ signMessage = async (options) => {
501
+ if (this.state.isLocked || !this.state.keys || !this.currentAdapter) {
502
+ throw new Error("Identity must be unlocked to sign messages");
503
+ }
504
+ this.checkKeysExpiration();
505
+ let privateKey = this.state.keys.privateKey;
506
+ let address = this.state.keys.address;
507
+ let publicKey = this.state.keys.publicKeyHex;
508
+ if (options.derivationPath && this.decryptedMnemonic) {
509
+ const keyData = await this.currentAdapter.deriveKeysAtPath(
510
+ this.decryptedMnemonic,
511
+ options.derivationPath
512
+ );
513
+ privateKey = keyData.privateKey;
514
+ address = keyData.address;
515
+ publicKey = keyData.publicKeyHex;
516
+ }
517
+ const result = await this.currentAdapter.signMessage(
518
+ privateKey,
519
+ options.message
520
+ );
521
+ return {
522
+ signature: result.signature,
523
+ address,
524
+ publicKey
525
+ };
526
+ };
527
+ /**
528
+ * Signs a blockchain transaction using the current or specified private key.
529
+ * Performs soft expiration check (warns but doesn't block).
530
+ * @param options - Transaction object and optional derivation path
531
+ * @returns Promise resolving to signed transaction and signature
532
+ * @throws {Error} If wallet is locked
533
+ */
534
+ signTransaction = async (options) => {
535
+ if (this.state.isLocked || !this.state.keys || !this.currentAdapter) {
536
+ throw new Error("Identity must be unlocked to sign transactions");
537
+ }
538
+ this.checkKeysExpiration();
539
+ let privateKey = this.state.keys.privateKey;
540
+ if (options.derivationPath && this.decryptedMnemonic) {
541
+ const keyData = await this.currentAdapter.deriveKeysAtPath(
542
+ this.decryptedMnemonic,
543
+ options.derivationPath
544
+ );
545
+ privateKey = keyData.privateKey;
546
+ }
547
+ const result = await this.currentAdapter.signTransaction(
548
+ privateKey,
549
+ options.transaction
550
+ );
551
+ return {
552
+ signedTransaction: result.signedTransaction,
553
+ signature: result.signature
554
+ };
555
+ };
556
+ /**
557
+ * Verifies a signature against a message and public key using the current adapter.
558
+ * @param message - Message that was signed (string or bytes)
559
+ * @param signature - Signature to verify
560
+ * @param publicKey - Public key to verify against
561
+ * @returns Promise resolving to true if signature is valid
562
+ * @throws {Error} If no adapter is selected
563
+ */
564
+ verifySignature = async (message, signature, publicKey) => {
565
+ if (!this.currentAdapter) {
566
+ throw new Error("No adapter selected. Please unlock with a chain first.");
567
+ }
568
+ return this.currentAdapter.verifySignature(message, signature, publicKey);
569
+ };
570
+ // Identity Management
571
+ /**
572
+ * Lists all identities stored in IndexedDB.
573
+ * @returns Promise resolving to array of all identities
574
+ */
575
+ listIdentities = async () => {
576
+ const stored = await getAllIdentitiesFromDB();
577
+ return stored.map((s) => ({
578
+ id: s.id,
579
+ publicKey: s.publicKey,
580
+ fingerprint: s.fingerprint,
581
+ label: s.label,
582
+ metadata: s.metadata,
583
+ createdAt: new Date(s.createdAt),
584
+ lastAccess: s.lastAccess ? new Date(s.lastAccess) : void 0
585
+ }));
586
+ };
587
+ /**
588
+ * Switches to a different identity. Locks the current wallet first.
589
+ * @param identityId - ID of the identity to switch to
590
+ * @returns Promise resolving to the selected identity
591
+ * @throws {Error} If identity not found
592
+ */
593
+ switchIdentity = async (identityId) => {
594
+ const stored = await getIdentityFromDB(identityId);
595
+ if (!stored) throw new Error("Identity not found");
596
+ await this.lock();
597
+ const identity = {
598
+ id: stored.id,
599
+ publicKey: stored.publicKey,
600
+ fingerprint: stored.fingerprint,
601
+ label: stored.label,
602
+ metadata: stored.metadata,
603
+ createdAt: new Date(stored.createdAt),
604
+ lastAccess: stored.lastAccess ? new Date(stored.lastAccess) : void 0
605
+ };
606
+ this.state.identity = identity;
607
+ this.notifyListeners("IDENTITY_SWITCHED", null);
608
+ return { identity };
609
+ };
610
+ /**
611
+ * Updates the label or metadata for an identity.
612
+ * @param identityId - ID of the identity to update
613
+ * @param updates - Label and/or metadata to update
614
+ * @returns Promise resolving to the updated identity
615
+ * @throws {Error} If identity not found
616
+ */
617
+ updateIdentity = async (identityId, updates) => {
618
+ const stored = await getIdentityFromDB(identityId);
619
+ if (!stored) throw new Error("Identity not found");
620
+ if (updates.label !== void 0) {
621
+ stored.label = updates.label;
622
+ }
623
+ if (updates.metadata !== void 0) {
624
+ stored.metadata = { ...stored.metadata, ...updates.metadata };
625
+ }
626
+ await saveIdentityToDB(stored);
627
+ const identity = {
628
+ id: stored.id,
629
+ publicKey: stored.publicKey,
630
+ fingerprint: stored.fingerprint,
631
+ label: stored.label,
632
+ metadata: stored.metadata,
633
+ createdAt: new Date(stored.createdAt),
634
+ lastAccess: stored.lastAccess ? new Date(stored.lastAccess) : void 0
635
+ };
636
+ if (this.state.identity?.id === identityId) {
637
+ this.state.identity = identity;
638
+ }
639
+ this.notifyListeners("IDENTITY_UPDATED", this.state.keys);
640
+ return { identity };
641
+ };
642
+ /**
643
+ * Permanently deletes an identity from IndexedDB. Requires password verification.
644
+ * Locks wallet if deleting current identity.
645
+ * @param identityId - ID of the identity to delete
646
+ * @param password - Password to verify ownership
647
+ * @returns Promise that resolves when deleted
648
+ * @throws {Error} If identity not found or password incorrect
649
+ */
650
+ deleteIdentity = async (identityId, password) => {
651
+ const stored = await getIdentityFromDB(identityId);
652
+ if (!stored) throw new Error("Identity not found");
653
+ try {
654
+ await decryptMnemonic(stored.keystore, password);
655
+ } catch (e) {
656
+ throw new Error("Invalid password");
657
+ }
658
+ if (this.state.identity?.id === identityId) {
659
+ await this.lock();
660
+ this.state.identity = null;
661
+ }
662
+ await deleteIdentityFromDB(identityId);
663
+ this.notifyListeners("IDENTITY_DELETED", this.state.keys);
664
+ };
665
+ /**
666
+ * Changes the password for an identity's encrypted keystore.
667
+ * Re-encrypts the mnemonic with the new password.
668
+ * @param identityId - ID of the identity
669
+ * @param oldPassword - Current password
670
+ * @param newPassword - New password
671
+ * @returns Promise that resolves when password is changed
672
+ * @throws {Error} If identity not found or old password incorrect
673
+ */
674
+ changePassword = async (identityId, oldPassword, newPassword) => {
675
+ const stored = await getIdentityFromDB(identityId);
676
+ if (!stored) throw new Error("Identity not found");
677
+ const mnemonic = await decryptMnemonic(stored.keystore, oldPassword);
678
+ const newKeystore = await encryptMnemonic(mnemonic, newPassword);
679
+ stored.keystore = newKeystore;
680
+ await saveIdentityToDB(stored);
681
+ this.notifyListeners("PASSWORD_CHANGED", this.state.keys);
682
+ };
683
+ // Import/Export
684
+ /**
685
+ * Exports the mnemonic phrase for an identity.
686
+ * Requires password verification to decrypt the keystore.
687
+ * ⚠️ Security: Only export mnemonics to secure locations (password managers, paper backup).
688
+ * @param identityId - ID of the identity to export
689
+ * @param password - Password to decrypt the keystore
690
+ * @returns Promise resolving to the mnemonic phrase
691
+ * @throws {Error} If identity not found or password incorrect
692
+ */
693
+ exportMnemonic = async (identityId, password) => {
694
+ const stored = await getIdentityFromDB(identityId);
695
+ if (!stored) throw new Error("Identity not found");
696
+ const mnemonic = await decryptMnemonic(stored.keystore, password);
697
+ return mnemonic;
698
+ };
699
+ /**
700
+ * Exports the encrypted keystore JSON for an identity.
701
+ * No password required - keystore is already encrypted.
702
+ * Perfect for encrypted backup files.
703
+ * @param identityId - ID of the identity to export
704
+ * @returns Promise resolving to encrypted keystore JSON string
705
+ * @throws {Error} If identity not found
706
+ */
707
+ exportKeystore = async (identityId) => {
708
+ const stored = await getIdentityFromDB(identityId);
709
+ if (!stored) throw new Error("Identity not found");
710
+ return stored.keystore;
711
+ };
712
+ /**
713
+ * Exports the complete StoredIdentity object including metadata.
714
+ * No password required - keystore within is already encrypted.
715
+ * Perfect for migrating identities between devices/browsers with all metadata intact.
716
+ * @param identityId - ID of the identity to export
717
+ * @returns Promise resolving to complete StoredIdentity object
718
+ * @throws {Error} If identity not found
719
+ */
720
+ exportIdentity = async (identityId) => {
721
+ const stored = await getIdentityFromDB(identityId);
722
+ if (!stored) throw new Error("Identity not found");
723
+ return stored;
724
+ };
725
+ /**
726
+ * Imports an identity from a mnemonic phrase.
727
+ * Creates and encrypts a new identity in IndexedDB with the provided password.
728
+ * @param mnemonic - BIP39 mnemonic phrase (12 or 24 words)
729
+ * @param password - Password to encrypt the new identity
730
+ * @param label - Optional label for the wallet (default: "Imported Wallet")
731
+ * @returns Promise resolving to the created identity
732
+ * @throws {Error} If mnemonic is invalid
733
+ */
734
+ importMnemonic = async (mnemonic, password, label) => {
735
+ const trimmedMnemonic = mnemonic.trim();
736
+ if (!validateMnemonic(trimmedMnemonic, wordlist)) {
737
+ throw new Error("Invalid mnemonic phrase");
738
+ }
739
+ const result = await this.createIdentity({
740
+ mnemonic: trimmedMnemonic,
741
+ password,
742
+ label: label || "Imported Wallet"
743
+ });
744
+ return { identity: result.identity };
745
+ };
746
+ /**
747
+ * Imports an identity from an encrypted keystore JSON.
748
+ * Decrypts the keystore and creates a new identity in IndexedDB.
749
+ * @param keystoreJson - Encrypted keystore JSON string
750
+ * @param password - Password to decrypt the keystore
751
+ * @param label - Optional label for the wallet (default: "Imported Wallet")
752
+ * @returns Promise resolving to the created identity
753
+ * @throws {Error} If keystore is invalid or password incorrect
754
+ */
755
+ importKeystore = async (keystoreJson, password, label) => {
756
+ const mnemonic = await decryptMnemonic(keystoreJson, password);
757
+ if (!validateMnemonic(mnemonic, wordlist)) {
758
+ throw new Error("Decrypted mnemonic is invalid");
759
+ }
760
+ const result = await this.createIdentity({
761
+ mnemonic,
762
+ password,
763
+ label: label || "Imported Wallet"
764
+ });
765
+ return { identity: result.identity };
766
+ };
767
+ /**
768
+ * Imports a complete StoredIdentity object.
769
+ * No password required - keystore is already encrypted.
770
+ * Perfect for migrating identities between devices/browsers with all metadata intact.
771
+ * Note: This will overwrite any existing identity with the same ID.
772
+ * @param storedIdentity - Complete StoredIdentity object to import
773
+ * @returns Promise resolving to the imported identity
774
+ * @throws {Error} If stored identity data is invalid
775
+ */
776
+ importIdentity = async (storedIdentity) => {
777
+ if (!storedIdentity.id || !storedIdentity.keystore || !storedIdentity.fingerprint) {
778
+ throw new Error(
779
+ "Invalid StoredIdentity object - missing required fields"
780
+ );
781
+ }
782
+ await saveIdentityToDB(storedIdentity);
783
+ const identity = {
784
+ id: storedIdentity.id,
785
+ publicKey: storedIdentity.publicKey,
786
+ fingerprint: storedIdentity.fingerprint,
787
+ label: storedIdentity.label,
788
+ metadata: storedIdentity.metadata,
789
+ createdAt: new Date(storedIdentity.createdAt),
790
+ lastAccess: storedIdentity.lastAccess ? new Date(storedIdentity.lastAccess) : void 0
791
+ };
792
+ this.notifyListeners("IDENTITY_IMPORTED", null);
793
+ return { identity };
794
+ };
795
+ // Address Management
796
+ /**
797
+ * Gets multiple addresses for a blockchain starting from a specific index.
798
+ * @param chainId - Blockchain identifier
799
+ * @param start - Starting index (default: 0)
800
+ * @param count - Number of addresses to generate (default: 20)
801
+ * @returns Promise resolving to array of addresses with paths
802
+ * @throws {Error} If wallet is locked or adapter not registered
803
+ */
804
+ getAddresses = async (chainId, start = 0, count = 20) => {
805
+ if (this.state.isLocked || !this.decryptedMnemonic) {
806
+ throw new Error("Identity must be unlocked to get addresses");
807
+ }
808
+ const adapter = this.getAdapter(chainId);
809
+ return adapter.getAddresses(this.decryptedMnemonic, start, count);
810
+ };
811
+ /**
812
+ * Finds all used addresses by checking balances with BIP44 gap limit of 20.
813
+ * Useful for account discovery when restoring wallets.
814
+ * @param chainId - Blockchain identifier
815
+ * @param checkBalance - Function that returns true if address has balance
816
+ * @returns Promise resolving to array of used addresses
817
+ * @throws {Error} If wallet is locked or adapter not registered
818
+ */
819
+ findUsedAddresses = async (chainId, checkBalance) => {
820
+ if (this.state.isLocked || !this.decryptedMnemonic) {
821
+ throw new Error("Identity must be unlocked to find addresses");
822
+ }
823
+ const adapter = this.getAdapter(chainId);
824
+ const usedAddresses = [];
825
+ let gapCount = 0;
826
+ const gapLimit = 20;
827
+ let index = 0;
828
+ while (gapCount < gapLimit) {
829
+ const addresses = await adapter.getAddresses(
830
+ this.decryptedMnemonic,
831
+ index,
832
+ 1
833
+ );
834
+ const addressData = addresses[0];
835
+ const hasBalance = await checkBalance(addressData.address);
836
+ if (hasBalance) {
837
+ usedAddresses.push(addressData);
838
+ gapCount = 0;
839
+ } else {
840
+ gapCount++;
841
+ }
842
+ index++;
843
+ }
844
+ return usedAddresses;
845
+ };
846
+ // State Management
847
+ /**
848
+ * Subscribes to authentication state changes.
849
+ * @param callback - Function called on each auth event with event type and keys
850
+ * @returns Unsubscribe function
851
+ */
852
+ onAuthStateChange = (callback) => {
853
+ this.listeners.add(callback);
854
+ return () => {
855
+ this.listeners.delete(callback);
856
+ };
857
+ };
858
+ // Getters for current state
859
+ /** Gets the current active identity, or null if none selected. */
860
+ get currentIdentity() {
861
+ return this.state.identity;
862
+ }
863
+ /** Gets the current derived keys, or null if wallet is locked. */
864
+ get currentKeys() {
865
+ return this.state.keys;
866
+ }
867
+ /** Gets the current blockchain, or null if none selected. */
868
+ get currentChain() {
869
+ return this.state.currentChain;
870
+ }
871
+ /** Gets the current blockchain address, or null if locked. */
872
+ get currentAddress() {
873
+ return this.state.keys?.address ?? null;
874
+ }
875
+ /** Gets the current public key as hex string, or null if locked. */
876
+ get currentPublicKey() {
877
+ return this.state.keys?.publicKeyHex ?? null;
878
+ }
879
+ /** Gets the key expiration date, or null if no expiration set. */
880
+ get currentExpiresAt() {
881
+ return this.state.keys?.expiresAt ?? null;
882
+ }
883
+ /** Gets the remaining seconds until key expiration, or null if no expiration set. */
884
+ get currentExpiresIn() {
885
+ if (!this.state.keys?.expiresAt) return null;
886
+ const remaining = Math.floor(
887
+ (this.state.keys.expiresAt.getTime() - Date.now()) / 1e3
888
+ );
889
+ return Math.max(0, remaining);
890
+ }
891
+ /** Returns true if wallet is locked (keys cleared from memory). */
892
+ get isLocked() {
893
+ return this.state.isLocked;
894
+ }
895
+ /** Returns true if wallet is unlocked with active keys. */
896
+ get isUnlocked() {
897
+ return !this.state.isLocked && this.state.keys !== null;
898
+ }
899
+ /** Returns true if an identity has been created or selected. */
900
+ get hasIdentity() {
901
+ return this.state.identity !== null;
902
+ }
903
+ /**
904
+ * Gets the chain-independent master public key (33 bytes, compressed secp256k1).
905
+ * Safe to expose publicly - does NOT include chaincode.
906
+ * Perfect for document ownership and cross-chain identity verification.
907
+ * @returns Hex-encoded master public key, or null if locked
908
+ */
909
+ get masterPublicKey() {
910
+ if (this.state.isLocked || !this.decryptedMnemonic) {
911
+ return null;
912
+ }
913
+ const seed = mnemonicToSeedSync(this.decryptedMnemonic);
914
+ const masterNode = HDKey.fromMasterSeed(seed);
915
+ return bufferToHex(masterNode.publicKey);
916
+ }
917
+ // Private Methods
918
+ isKeysExpired() {
919
+ if (!this.state.keys?.expiresAt) return false;
920
+ return Date.now() >= this.state.keys.expiresAt.getTime();
921
+ }
922
+ checkKeysExpiration() {
923
+ if (this.isKeysExpired()) {
924
+ console.warn(
925
+ "CryptForge: Keys have expired. Consider unlocking again for security."
926
+ );
927
+ this.notifyListeners("KEYS_EXPIRED", null);
928
+ }
929
+ }
930
+ async deriveKeysWithAdapter(mnemonic, chainId, adapter, identity, duration) {
931
+ const keyData = await adapter.deriveKeys(mnemonic);
932
+ const durationMs = duration || 36e5;
933
+ const expiresAt = new Date(Date.now() + durationMs);
934
+ const expiresIn = Math.floor(durationMs / 1e3);
935
+ return {
936
+ privateKey: keyData.privateKey,
937
+ privateKeyHex: keyData.privateKeyHex,
938
+ publicKey: keyData.publicKey,
939
+ publicKeyHex: keyData.publicKeyHex,
940
+ address: keyData.address,
941
+ derivationPath: keyData.path,
942
+ chain: {
943
+ id: chainId,
944
+ name: adapter.chainData.name,
945
+ symbol: adapter.chainData.symbol
946
+ },
947
+ expiresAt,
948
+ expiresIn,
949
+ identity
950
+ };
951
+ }
952
+ notifyListeners(event, keys) {
953
+ this.listeners.forEach((callback) => callback(event, keys));
954
+ }
955
+ setAutoLockTimer(duration) {
956
+ if (this.lockTimer) {
957
+ window.clearTimeout(this.lockTimer);
958
+ }
959
+ this.lockTimer = window.setTimeout(() => this.lock(), duration);
960
+ }
961
+ };
962
+ var createAuthClient = () => {
963
+ return new AuthClient();
964
+ };
965
+ var getMasterNode = (mnemonic) => {
966
+ const seed = mnemonicToSeedSync(mnemonic);
967
+ return HDKey.fromMasterSeed(seed);
968
+ };
969
+ var getMasterFingerprint = (mnemonic) => {
970
+ const root = getMasterNode(mnemonic);
971
+ return root.fingerprint.toString(16).padStart(8, "0").toUpperCase();
972
+ };
973
+ var getMasterPublicKey = (mnemonic) => {
974
+ const root = getMasterNode(mnemonic);
975
+ return bufferToHex(root.publicKey);
976
+ };
977
+ function bufferToHex(buf) {
978
+ return Array.from(buf).map((b) => b.toString(16).padStart(2, "0")).join("");
979
+ }
980
+ function concatUint8(a, b) {
981
+ const c = new Uint8Array(a.length + b.length);
982
+ c.set(a, 0);
983
+ c.set(b, a.length);
984
+ return c;
985
+ }
986
+ function hexToUint8(hex) {
987
+ if (hex.length % 2 !== 0) throw new Error("Invalid hex string");
988
+ const arr = new Uint8Array(hex.length / 2);
989
+ for (let i = 0; i < arr.length; i++) {
990
+ arr[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
991
+ }
992
+ return arr;
993
+ }
994
+ function arraysEqual(a, b) {
995
+ if (a.length !== b.length) return false;
996
+ for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
997
+ return true;
998
+ }
999
+ var PBKDF2_ITERATIONS = 1e5;
1000
+ var KEY_LENGTH = 32;
1001
+ function randomBytes(length) {
1002
+ const arr = new Uint8Array(length);
1003
+ crypto.getRandomValues(arr);
1004
+ return arr;
1005
+ }
1006
+ var encryptMnemonic = async (mnemonic, password) => {
1007
+ const salt = randomBytes(32);
1008
+ const iv = randomBytes(16);
1009
+ const keyMaterial = await crypto.subtle.importKey(
1010
+ "raw",
1011
+ new TextEncoder().encode(password),
1012
+ "PBKDF2",
1013
+ false,
1014
+ ["deriveKey"]
1015
+ );
1016
+ const key = await crypto.subtle.deriveKey(
1017
+ {
1018
+ name: "PBKDF2",
1019
+ salt,
1020
+ iterations: PBKDF2_ITERATIONS,
1021
+ hash: "SHA-256"
1022
+ },
1023
+ keyMaterial,
1024
+ { name: "AES-CBC", length: 256 },
1025
+ true,
1026
+ ["encrypt", "decrypt"]
1027
+ );
1028
+ const encryptedBuffer = await crypto.subtle.encrypt(
1029
+ { name: "AES-CBC", iv },
1030
+ key,
1031
+ new TextEncoder().encode(mnemonic)
1032
+ );
1033
+ const ciphertext = new Uint8Array(encryptedBuffer);
1034
+ const macKey = await crypto.subtle.importKey(
1035
+ "raw",
1036
+ await crypto.subtle.exportKey("raw", key),
1037
+ { name: "HMAC", hash: "SHA-256" },
1038
+ false,
1039
+ ["sign"]
1040
+ );
1041
+ const macBuffer = await crypto.subtle.sign(
1042
+ "HMAC",
1043
+ macKey,
1044
+ concatUint8(ciphertext, iv)
1045
+ );
1046
+ const keystore = {
1047
+ version: 1,
1048
+ id: bufferToHex(randomBytes(16)),
1049
+ crypto: {
1050
+ cipher: "aes-256-cbc",
1051
+ ciphertext: bufferToHex(ciphertext),
1052
+ cipherparams: { iv: bufferToHex(iv) },
1053
+ kdf: "pbkdf2",
1054
+ kdfparams: {
1055
+ salt: bufferToHex(salt),
1056
+ iterations: PBKDF2_ITERATIONS,
1057
+ keylen: KEY_LENGTH
1058
+ },
1059
+ mac: bufferToHex(new Uint8Array(macBuffer))
1060
+ }
1061
+ };
1062
+ return JSON.stringify(keystore);
1063
+ };
1064
+ async function decryptMnemonic(keystoreJson, password) {
1065
+ const keystore = JSON.parse(keystoreJson);
1066
+ const salt = hexToUint8(keystore.crypto.kdfparams.salt);
1067
+ const iv = hexToUint8(keystore.crypto.cipherparams.iv);
1068
+ const ciphertext = hexToUint8(keystore.crypto.ciphertext);
1069
+ const storedMac = hexToUint8(keystore.crypto.mac);
1070
+ const keyMaterial = await crypto.subtle.importKey(
1071
+ "raw",
1072
+ new TextEncoder().encode(password),
1073
+ "PBKDF2",
1074
+ false,
1075
+ ["deriveKey"]
1076
+ );
1077
+ const key = await crypto.subtle.deriveKey(
1078
+ {
1079
+ name: "PBKDF2",
1080
+ salt,
1081
+ iterations: keystore.crypto.kdfparams.iterations,
1082
+ hash: "SHA-256"
1083
+ },
1084
+ keyMaterial,
1085
+ { name: "AES-CBC", length: 256 },
1086
+ true,
1087
+ ["encrypt", "decrypt"]
1088
+ );
1089
+ const macKey = await crypto.subtle.importKey(
1090
+ "raw",
1091
+ await crypto.subtle.exportKey("raw", key),
1092
+ { name: "HMAC", hash: "SHA-256" },
1093
+ false,
1094
+ ["sign"]
1095
+ );
1096
+ const computedMacBuffer = await crypto.subtle.sign(
1097
+ "HMAC",
1098
+ macKey,
1099
+ concatUint8(ciphertext, iv)
1100
+ );
1101
+ const computedMac = new Uint8Array(computedMacBuffer);
1102
+ if (!arraysEqual(computedMac, storedMac)) {
1103
+ throw new Error("Invalid password - MAC verification failed");
1104
+ }
1105
+ const decryptedBuffer = await crypto.subtle.decrypt(
1106
+ { name: "AES-CBC", iv },
1107
+ key,
1108
+ ciphertext
1109
+ );
1110
+ const mnemonic = new TextDecoder().decode(decryptedBuffer);
1111
+ if (!mnemonic) {
1112
+ throw new Error("Invalid password - decryption failed");
1113
+ }
1114
+ return mnemonic;
1115
+ }
1116
+ var DB_NAME = "CryptoAuthDB";
1117
+ var STORE_NAME = "identities";
1118
+ var openDB = () => {
1119
+ return new Promise((resolve, reject) => {
1120
+ const request = indexedDB.open(DB_NAME, 1);
1121
+ request.onerror = () => reject(request.error);
1122
+ request.onsuccess = () => resolve(request.result);
1123
+ request.onupgradeneeded = (event) => {
1124
+ const db = event.target.result;
1125
+ if (!db.objectStoreNames.contains(STORE_NAME)) {
1126
+ db.createObjectStore(STORE_NAME, { keyPath: "id" });
1127
+ }
1128
+ };
1129
+ });
1130
+ };
1131
+ var saveIdentityToDB = async (identity) => {
1132
+ const db = await openDB();
1133
+ return new Promise((resolve, reject) => {
1134
+ const transaction = db.transaction([STORE_NAME], "readwrite");
1135
+ const store = transaction.objectStore(STORE_NAME);
1136
+ const request = store.put(identity);
1137
+ request.onsuccess = () => resolve();
1138
+ request.onerror = () => reject(request.error);
1139
+ });
1140
+ };
1141
+ var getAllIdentitiesFromDB = async () => {
1142
+ const db = await openDB();
1143
+ return new Promise((resolve, reject) => {
1144
+ const transaction = db.transaction([STORE_NAME], "readonly");
1145
+ const store = transaction.objectStore(STORE_NAME);
1146
+ const request = store.getAll();
1147
+ request.onsuccess = () => resolve(request.result);
1148
+ request.onerror = () => reject(request.error);
1149
+ });
1150
+ };
1151
+ var getIdentityFromDB = async (id) => {
1152
+ const db = await openDB();
1153
+ return new Promise((resolve, reject) => {
1154
+ const transaction = db.transaction([STORE_NAME], "readonly");
1155
+ const store = transaction.objectStore(STORE_NAME);
1156
+ const request = store.get(id);
1157
+ request.onsuccess = () => resolve(request.result);
1158
+ request.onerror = () => reject(request.error);
1159
+ });
1160
+ };
1161
+ var deleteIdentityFromDB = async (id) => {
1162
+ const db = await openDB();
1163
+ return new Promise((resolve, reject) => {
1164
+ const transaction = db.transaction([STORE_NAME], "readwrite");
1165
+ const store = transaction.objectStore(STORE_NAME);
1166
+ const request = store.delete(id);
1167
+ request.onsuccess = () => resolve();
1168
+ request.onerror = () => reject(request.error);
1169
+ });
1170
+ };
1171
+ export {
1172
+ AuthClient,
1173
+ createAuthClient
1174
+ };