@aztec/node-keystore 0.0.1-commit.023c3e5

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.
@@ -0,0 +1,594 @@
1
+ /**
2
+ * Keystore Manager
3
+ *
4
+ * Manages keystore configuration and delegates signing operations to appropriate signers.
5
+ */ import { Buffer32 } from '@aztec/foundation/buffer';
6
+ import { Wallet } from '@ethersproject/wallet';
7
+ import { readFileSync, readdirSync, statSync } from 'fs';
8
+ import { extname, join } from 'path';
9
+ import { mnemonicToAccount } from 'viem/accounts';
10
+ import { ethPrivateKeySchema } from './schemas.js';
11
+ import { LocalSigner, RemoteSigner } from './signer.js';
12
+ /**
13
+ * Error thrown when keystore operations fail
14
+ */ export class KeystoreError extends Error {
15
+ cause;
16
+ constructor(message, cause){
17
+ super(message), this.cause = cause;
18
+ this.name = 'KeystoreError';
19
+ }
20
+ }
21
+ /**
22
+ * Keystore Manager - coordinates signing operations based on keystore configuration
23
+ */ export class KeystoreManager {
24
+ keystore;
25
+ /**
26
+ * Create a keystore manager from a parsed configuration.
27
+ * Performs a lightweight duplicate-attester check without decrypting JSON V3 or deriving mnemonics.
28
+ * @param keystore Parsed keystore configuration
29
+ */ constructor(keystore){
30
+ this.keystore = keystore;
31
+ this.validateUniqueAttesterAddresses();
32
+ }
33
+ /**
34
+ * Validates all remote signers in the keystore are accessible and have the required addresses.
35
+ * Should be called after construction if validation is needed.
36
+ */ async validateSigners() {
37
+ // Collect all remote signers with their addresses grouped by URL
38
+ const remoteSignersByUrl = new Map();
39
+ // Helper to extract remote signer URL from config
40
+ const getUrl = (config)=>{
41
+ return typeof config === 'string' ? config : config.remoteSignerUrl;
42
+ };
43
+ // Helper to collect remote signers from accounts
44
+ const collectRemoteSigners = (accounts, defaultRemoteSigner)=>{
45
+ const processAccount = (account)=>{
46
+ if (typeof account === 'object' && !('path' in account) && !('mnemonic' in account)) {
47
+ // This is a remote signer account
48
+ const remoteSigner = account;
49
+ const address = 'address' in remoteSigner ? remoteSigner.address : remoteSigner;
50
+ let url;
51
+ if ('remoteSignerUrl' in remoteSigner && remoteSigner.remoteSignerUrl) {
52
+ url = remoteSigner.remoteSignerUrl;
53
+ } else if (defaultRemoteSigner) {
54
+ url = getUrl(defaultRemoteSigner);
55
+ } else {
56
+ return; // No remote signer URL available
57
+ }
58
+ if (!remoteSignersByUrl.has(url)) {
59
+ remoteSignersByUrl.set(url, new Set());
60
+ }
61
+ remoteSignersByUrl.get(url).add(address.toString());
62
+ }
63
+ };
64
+ if (Array.isArray(accounts)) {
65
+ accounts.forEach((account)=>collectRemoteSigners(account, defaultRemoteSigner));
66
+ } else if (typeof accounts === 'object' && 'mnemonic' in accounts) {
67
+ // Skip mnemonic configs
68
+ } else {
69
+ processAccount(accounts);
70
+ }
71
+ };
72
+ // Collect from validators
73
+ const validatorCount = this.getValidatorCount();
74
+ for(let i = 0; i < validatorCount; i++){
75
+ const validator = this.getValidator(i);
76
+ const remoteSigner = validator.remoteSigner || this.keystore.remoteSigner;
77
+ collectRemoteSigners(this.extractEthAccountsFromAttester(validator.attester), remoteSigner);
78
+ if (validator.publisher) {
79
+ collectRemoteSigners(validator.publisher, remoteSigner);
80
+ }
81
+ }
82
+ // Collect from slasher
83
+ if (this.keystore.slasher) {
84
+ collectRemoteSigners(this.keystore.slasher, this.keystore.remoteSigner);
85
+ }
86
+ // Collect from prover
87
+ if (this.keystore.prover && typeof this.keystore.prover === 'object' && 'publisher' in this.keystore.prover) {
88
+ collectRemoteSigners(this.keystore.prover.publisher, this.keystore.remoteSigner);
89
+ }
90
+ // Validate each remote signer URL with all its addresses
91
+ for (const [url, addresses] of remoteSignersByUrl.entries()){
92
+ if (addresses.size > 0) {
93
+ await RemoteSigner.validateAccess(url, Array.from(addresses));
94
+ }
95
+ }
96
+ }
97
+ /**
98
+ * Validates that attester addresses are unique across all validators
99
+ * Only checks simple private key attesters, not JSON-V3 or mnemonic attesters,
100
+ * these are validated when decrypting the JSON-V3 keystore files
101
+ * @throws KeystoreError if duplicate attester addresses are found
102
+ */ validateUniqueAttesterAddresses() {
103
+ const seenAddresses = new Set();
104
+ const validatorCount = this.getValidatorCount();
105
+ for(let validatorIndex = 0; validatorIndex < validatorCount; validatorIndex++){
106
+ const validator = this.getValidator(validatorIndex);
107
+ const addresses = this.extractAddressesWithoutSensitiveOperations(validator.attester);
108
+ for (const addr of addresses){
109
+ const address = addr.toString().toLowerCase();
110
+ if (seenAddresses.has(address)) {
111
+ throw new KeystoreError(`Duplicate attester address found: ${addr.toString()}. An attester address may only appear once across all configuration blocks.`);
112
+ }
113
+ seenAddresses.add(address);
114
+ }
115
+ }
116
+ }
117
+ /**
118
+ * Best-effort address extraction that avoids decryption/derivation (no JSON-V3 or mnemonic processing).
119
+ * This is used at construction time to check for obvious duplicates without throwing for invalid inputs.
120
+ */ extractAddressesWithoutSensitiveOperations(accounts) {
121
+ const ethAccounts = this.extractEthAccountsFromAttester(accounts);
122
+ return this.extractAddressesFromEthAccountsNonSensitive(ethAccounts);
123
+ }
124
+ /**
125
+ * Extract addresses from EthAccounts without sensitive operations (no decryption/derivation).
126
+ */ extractAddressesFromEthAccountsNonSensitive(accounts) {
127
+ const results = [];
128
+ const handleAccount = (account)=>{
129
+ if (typeof account === 'string') {
130
+ if (account.startsWith('0x') && account.length === 66) {
131
+ try {
132
+ const signer = new LocalSigner(Buffer32.fromString(ethPrivateKeySchema.parse(account)));
133
+ results.push(signer.address);
134
+ } catch {
135
+ // ignore invalid private key at construction time
136
+ }
137
+ }
138
+ return;
139
+ }
140
+ if ('path' in account) {
141
+ return;
142
+ }
143
+ if ('mnemonic' in account) {
144
+ return;
145
+ }
146
+ const remoteSigner = account;
147
+ if ('address' in remoteSigner) {
148
+ results.push(remoteSigner.address);
149
+ return;
150
+ }
151
+ results.push(remoteSigner);
152
+ };
153
+ if (Array.isArray(accounts)) {
154
+ for (const account of accounts){
155
+ handleAccount(account);
156
+ }
157
+ return results;
158
+ }
159
+ if (typeof accounts === 'object' && accounts !== null && 'mnemonic' in accounts) {
160
+ return results;
161
+ }
162
+ handleAccount(accounts);
163
+ return results;
164
+ }
165
+ /**
166
+ * Create signers for validator attester accounts
167
+ */ createAttesterSigners(validatorIndex) {
168
+ const validator = this.getValidator(validatorIndex);
169
+ const ethAccounts = this.extractEthAccountsFromAttester(validator.attester);
170
+ return this.createSignersFromEthAccounts(ethAccounts, validator.remoteSigner || this.keystore.remoteSigner);
171
+ }
172
+ /**
173
+ * Create signers for validator publisher accounts (falls back to keystore-level publisher, then to attester if not specified)
174
+ */ createPublisherSigners(validatorIndex) {
175
+ const validator = this.getValidator(validatorIndex);
176
+ if (validator.publisher) {
177
+ return this.createSignersFromEthAccounts(validator.publisher, validator.remoteSigner || this.keystore.remoteSigner);
178
+ }
179
+ // Fall back to keystore-level publisher
180
+ if (this.keystore.publisher) {
181
+ return this.createSignersFromEthAccounts(this.keystore.publisher, validator.remoteSigner || this.keystore.remoteSigner);
182
+ }
183
+ // Fall back to attester signers
184
+ return this.createAttesterSigners(validatorIndex);
185
+ }
186
+ createAllValidatorPublisherSigners() {
187
+ const numValidators = this.getValidatorCount();
188
+ const allPublishers = [];
189
+ for(let i = 0; i < numValidators; i++){
190
+ allPublishers.push(...this.createPublisherSigners(i));
191
+ }
192
+ return allPublishers;
193
+ }
194
+ /**
195
+ * Create signers for slasher accounts
196
+ */ createSlasherSigners() {
197
+ if (!this.keystore.slasher) {
198
+ return [];
199
+ }
200
+ return this.createSignersFromEthAccounts(this.keystore.slasher, this.keystore.remoteSigner);
201
+ }
202
+ /**
203
+ * Create signers for prover accounts
204
+ */ createProverSigners() {
205
+ if (!this.keystore.prover) {
206
+ return undefined;
207
+ }
208
+ // Handle prover being a private key, JSON key store or remote signer with nested address
209
+ if (typeof this.keystore.prover === 'string' || 'path' in this.keystore.prover || 'address' in this.keystore.prover) {
210
+ const signers = this.createSignersFromEthAccounts(this.keystore.prover, this.keystore.remoteSigner);
211
+ return {
212
+ id: undefined,
213
+ signers
214
+ };
215
+ }
216
+ // Handle prover as Id and specified publishers
217
+ if ('id' in this.keystore.prover) {
218
+ const id = this.keystore.prover.id;
219
+ const signers = this.createSignersFromEthAccounts(this.keystore.prover.publisher, this.keystore.remoteSigner);
220
+ return {
221
+ id,
222
+ signers
223
+ };
224
+ }
225
+ // Here, prover is just an EthAddress for a remote signer
226
+ const signers = this.createSignersFromEthAccounts(this.keystore.prover, this.keystore.remoteSigner);
227
+ return {
228
+ id: undefined,
229
+ signers
230
+ };
231
+ }
232
+ /**
233
+ * Get validator configuration by index
234
+ */ getValidator(index) {
235
+ if (!this.keystore.validators || index >= this.keystore.validators.length || index < 0) {
236
+ throw new KeystoreError(`Validator index ${index} out of bounds`);
237
+ }
238
+ return this.keystore.validators[index];
239
+ }
240
+ /**
241
+ * Get validator count
242
+ */ getValidatorCount() {
243
+ return this.keystore.validators?.length || 0;
244
+ }
245
+ /**
246
+ * Get coinbase address for validator (falls back to keystore-level coinbase, then to the specific attester address)
247
+ */ getCoinbaseAddress(validatorIndex, attesterAddress) {
248
+ const validator = this.getValidator(validatorIndex);
249
+ if (validator.coinbase) {
250
+ return validator.coinbase;
251
+ }
252
+ // Fall back to keystore-level coinbase
253
+ if (this.keystore.coinbase) {
254
+ return this.keystore.coinbase;
255
+ }
256
+ // Fall back to the specific attester address
257
+ return attesterAddress;
258
+ }
259
+ /**
260
+ * Get fee recipient for validator (falls back to keystore-level feeRecipient)
261
+ */ getFeeRecipient(validatorIndex) {
262
+ const validator = this.getValidator(validatorIndex);
263
+ if (validator.feeRecipient) {
264
+ return validator.feeRecipient;
265
+ }
266
+ // Fall back to keystore-level feeRecipient
267
+ if (this.keystore.feeRecipient) {
268
+ return this.keystore.feeRecipient;
269
+ }
270
+ throw new KeystoreError(`No feeRecipient configured for validator ${validatorIndex}. You can set it at validator or keystore level.`);
271
+ }
272
+ /**
273
+ * Get the raw slasher configuration as provided in the keystore file.
274
+ * @returns The slasher accounts configuration or undefined if not set
275
+ */ getSlasherAccounts() {
276
+ return this.keystore.slasher;
277
+ }
278
+ /**
279
+ * Get the raw prover configuration as provided in the keystore file.
280
+ * @returns The prover configuration or undefined if not set
281
+ */ getProverConfig() {
282
+ return this.keystore.prover;
283
+ }
284
+ /**
285
+ * Resolves attester accounts (including JSON V3 and mnemonic) and checks for duplicate addresses across validators.
286
+ * Throws if the same resolved address appears in more than one validator configuration.
287
+ */ validateResolvedUniqueAttesterAddresses() {
288
+ const seenAddresses = new Set();
289
+ const validatorCount = this.getValidatorCount();
290
+ for(let validatorIndex = 0; validatorIndex < validatorCount; validatorIndex++){
291
+ const validator = this.getValidator(validatorIndex);
292
+ const signers = this.createSignersFromEthAccounts(this.extractEthAccountsFromAttester(validator.attester), validator.remoteSigner || this.keystore.remoteSigner);
293
+ for (const signer of signers){
294
+ const address = signer.address.toString().toLowerCase();
295
+ if (seenAddresses.has(address)) {
296
+ throw new KeystoreError(`Duplicate attester address found after resolving accounts: ${address}. An attester address may only appear once across all configuration blocks.`);
297
+ }
298
+ seenAddresses.add(address);
299
+ }
300
+ }
301
+ }
302
+ /**
303
+ * Create signers from EthAccounts configuration
304
+ */ createSignersFromEthAccounts(accounts, defaultRemoteSigner) {
305
+ if (typeof accounts === 'string') {
306
+ return [
307
+ this.createSignerFromEthAccount(accounts, defaultRemoteSigner)
308
+ ];
309
+ }
310
+ if (Array.isArray(accounts)) {
311
+ const signers = [];
312
+ for (const account of accounts){
313
+ const accountSigners = this.createSignersFromEthAccounts(account, defaultRemoteSigner);
314
+ signers.push(...accountSigners);
315
+ }
316
+ return signers;
317
+ }
318
+ // Mnemonic configuration
319
+ if ('mnemonic' in accounts) {
320
+ return this.createSignersFromMnemonic(accounts);
321
+ }
322
+ // Single account object - handle JSON V3 directory case
323
+ if ('path' in accounts) {
324
+ const result = this.createSignerFromJsonV3(accounts);
325
+ return result;
326
+ }
327
+ return [
328
+ this.createSignerFromEthAccount(accounts, defaultRemoteSigner)
329
+ ];
330
+ }
331
+ /**
332
+ * Create a signer from a single EthAccount configuration
333
+ */ createSignerFromEthAccount(account, defaultRemoteSigner) {
334
+ // Private key (hex string)
335
+ if (typeof account === 'string') {
336
+ if (account.startsWith('0x') && account.length === 66) {
337
+ // Private key
338
+ return new LocalSigner(Buffer32.fromString(ethPrivateKeySchema.parse(account)));
339
+ } else {
340
+ throw new Error(`Invalid private key`);
341
+ }
342
+ }
343
+ // JSON V3 keystore
344
+ if ('path' in account) {
345
+ const result = this.createSignerFromJsonV3(account);
346
+ return result[0];
347
+ }
348
+ // Remote signer account
349
+ const remoteSigner = account;
350
+ if ('address' in remoteSigner) {
351
+ // Remote signer with config
352
+ const config = remoteSigner.remoteSignerUrl ? {
353
+ remoteSignerUrl: remoteSigner.remoteSignerUrl,
354
+ certPath: remoteSigner.certPath,
355
+ certPass: remoteSigner.certPass
356
+ } : defaultRemoteSigner;
357
+ if (!config) {
358
+ throw new KeystoreError(`No remote signer configuration found for address ${remoteSigner.address}`);
359
+ }
360
+ return new RemoteSigner(remoteSigner.address, config);
361
+ }
362
+ // Just an address - use default config
363
+ if (!defaultRemoteSigner) {
364
+ throw new KeystoreError(`No remote signer configuration found for address ${remoteSigner}`);
365
+ }
366
+ return new RemoteSigner(remoteSigner, defaultRemoteSigner);
367
+ }
368
+ /**
369
+ * Create signer from JSON V3 keystore file or directory
370
+ */ createSignerFromJsonV3(config) {
371
+ try {
372
+ const stats = statSync(config.path);
373
+ if (stats.isDirectory()) {
374
+ // Handle directory - load all JSON files
375
+ const files = readdirSync(config.path);
376
+ const signers = [];
377
+ const seenAddresses = new Map(); // address -> file name
378
+ for (const file of files){
379
+ // Only process .json files
380
+ if (extname(file).toLowerCase() !== '.json') {
381
+ continue;
382
+ }
383
+ const filePath = join(config.path, file);
384
+ try {
385
+ const signer = this.createSignerFromSingleJsonV3File(filePath, config.password);
386
+ const addressString = signer.address.toString().toLowerCase();
387
+ const existingFile = seenAddresses.get(addressString);
388
+ if (existingFile) {
389
+ throw new KeystoreError(`Duplicate JSON V3 keystore address ${addressString} found in directory ${config.path} (files: ${existingFile} and ${file}). Each keystore must have a unique address.`);
390
+ }
391
+ seenAddresses.set(addressString, file);
392
+ signers.push(signer);
393
+ } catch (error) {
394
+ // Re-throw with file context
395
+ throw new KeystoreError(`Failed to load keystore file ${file}: ${error}`, error);
396
+ }
397
+ }
398
+ if (signers.length === 0) {
399
+ throw new KeystoreError(`No JSON keystore files found in directory ${config.path}`);
400
+ }
401
+ return signers;
402
+ } else {
403
+ // Single file
404
+ return [
405
+ this.createSignerFromSingleJsonV3File(config.path, config.password)
406
+ ];
407
+ }
408
+ } catch (error) {
409
+ if (error instanceof KeystoreError) {
410
+ throw error;
411
+ }
412
+ throw new KeystoreError(`Failed to access JSON V3 keystore ${config.path}: ${error}`, error);
413
+ }
414
+ }
415
+ /**
416
+ * Create signer from a single JSON V3 keystore file
417
+ */ createSignerFromSingleJsonV3File(filePath, password) {
418
+ try {
419
+ // Read the keystore file
420
+ const keystoreJson = readFileSync(filePath, 'utf8');
421
+ // Get password - prompt for it if not provided
422
+ const resolvedPassword = password;
423
+ if (!resolvedPassword) {
424
+ throw new KeystoreError(`No password provided for keystore ${filePath}. Provide password in config.`);
425
+ }
426
+ // Use @ethersproject/wallet to decrypt the JSON V3 keystore synchronously
427
+ const ethersWallet = Wallet.fromEncryptedJsonSync(keystoreJson, resolvedPassword);
428
+ // Convert the private key to our format
429
+ const privateKey = Buffer32.fromString(ethersWallet.privateKey);
430
+ return new LocalSigner(privateKey);
431
+ } catch (error) {
432
+ const err = error;
433
+ throw new KeystoreError(`Failed to decrypt JSON V3 keystore ${filePath}: ${err.message}`, err);
434
+ }
435
+ }
436
+ /**
437
+ * Create signers from mnemonic configuration using BIP44 derivation
438
+ */ createSignersFromMnemonic(config) {
439
+ const { mnemonic, addressIndex = 0, accountIndex = 0, addressCount = 1, accountCount = 1 } = config;
440
+ const signers = [];
441
+ try {
442
+ // Use viem's mnemonic derivation (imported at top of file)
443
+ // Normalize mnemonic by trimming whitespace
444
+ const normalizedMnemonic = mnemonic.trim();
445
+ for(let accIdx = accountIndex; accIdx < accountIndex + accountCount; accIdx++){
446
+ for(let addrIdx = addressIndex; addrIdx < addressIndex + addressCount; addrIdx++){
447
+ const viemAccount = mnemonicToAccount(normalizedMnemonic, {
448
+ accountIndex: accIdx,
449
+ addressIndex: addrIdx
450
+ });
451
+ // Extract the private key from the viem account
452
+ const privateKeyBytes = viemAccount.getHdKey().privateKey;
453
+ const privateKey = Buffer32.fromBuffer(Buffer.from(privateKeyBytes));
454
+ signers.push(new LocalSigner(privateKey));
455
+ }
456
+ }
457
+ return signers;
458
+ } catch (error) {
459
+ throw new KeystoreError(`Failed to derive accounts from mnemonic: ${error}`, error);
460
+ }
461
+ }
462
+ /**
463
+ * Sign message with a specific signer
464
+ */ async signMessage(signer, message) {
465
+ return await signer.signMessage(message);
466
+ }
467
+ /**
468
+ * Sign typed data with a specific signer
469
+ */ async signTypedData(signer, typedData) {
470
+ return await signer.signTypedData(typedData);
471
+ }
472
+ /**
473
+ * Get the effective remote signer configuration for a specific attester address
474
+ * Precedence: account-level override > validator-level config > file-level default
475
+ */ getEffectiveRemoteSignerConfig(validatorIndex, attesterAddress) {
476
+ const validator = this.getValidator(validatorIndex);
477
+ // Helper to get address from an account configuration
478
+ const getAddressFromAccount = (account)=>{
479
+ if (typeof account === 'string') {
480
+ if (account.startsWith('0x') && account.length === 66) {
481
+ // This is a private key - derive the address
482
+ try {
483
+ const signer = new LocalSigner(Buffer32.fromString(ethPrivateKeySchema.parse(account)));
484
+ return signer.address;
485
+ } catch {
486
+ return undefined;
487
+ }
488
+ }
489
+ return undefined;
490
+ }
491
+ // JSON V3 keystore
492
+ if ('path' in account) {
493
+ try {
494
+ const signers = this.createSignerFromJsonV3(account);
495
+ return signers.map((s)=>s.address);
496
+ } catch {
497
+ return undefined;
498
+ }
499
+ }
500
+ // Remote signer account, either it is an address or the address is nested
501
+ const remoteSigner = account;
502
+ if ('address' in remoteSigner) {
503
+ return remoteSigner.address;
504
+ }
505
+ return remoteSigner;
506
+ };
507
+ // Helper to check if account matches and get its remote signer config
508
+ const checkAccount = (account)=>{
509
+ const addresses = getAddressFromAccount(account);
510
+ if (!addresses) {
511
+ return undefined;
512
+ }
513
+ const addressArray = Array.isArray(addresses) ? addresses : [
514
+ addresses
515
+ ];
516
+ const matches = addressArray.some((addr)=>addr.equals(attesterAddress));
517
+ if (!matches) {
518
+ return undefined;
519
+ }
520
+ // Found a match - determine the config to return
521
+ if (typeof account === 'string') {
522
+ return undefined;
523
+ }
524
+ // JSON V3 - local signer, no remote config
525
+ if ('path' in account) {
526
+ return undefined;
527
+ }
528
+ // Remote signer account with potential override
529
+ const remoteSigner = account;
530
+ if ('address' in remoteSigner) {
531
+ // Has inline config
532
+ if (remoteSigner.remoteSignerUrl) {
533
+ return {
534
+ remoteSignerUrl: remoteSigner.remoteSignerUrl,
535
+ certPath: remoteSigner.certPath,
536
+ certPass: remoteSigner.certPass
537
+ };
538
+ } else {
539
+ // No URL specified, use defaults
540
+ return validator.remoteSigner || this.keystore.remoteSigner;
541
+ }
542
+ }
543
+ // Just an address, use defaults
544
+ return validator.remoteSigner || this.keystore.remoteSigner;
545
+ };
546
+ // Normalize attester to EthAccounts and search
547
+ const normalized = this.extractEthAccountsFromAttester(validator.attester);
548
+ const findInEthAccounts = (accs)=>{
549
+ if (typeof accs === 'string') {
550
+ return checkAccount(accs);
551
+ }
552
+ if (Array.isArray(accs)) {
553
+ for (const a of accs){
554
+ const res = checkAccount(a);
555
+ if (res !== undefined) {
556
+ return res;
557
+ }
558
+ }
559
+ return undefined;
560
+ }
561
+ if (typeof accs === 'object' && accs !== null && 'mnemonic' in accs) {
562
+ // mnemonic-derived keys are local signers; no remote signer config
563
+ return undefined;
564
+ }
565
+ return checkAccount(accs);
566
+ };
567
+ return findInEthAccounts(normalized);
568
+ }
569
+ /** Extract ETH accounts from AttesterAccounts */ extractEthAccountsFromAttester(attester) {
570
+ if (typeof attester === 'string') {
571
+ return attester;
572
+ }
573
+ if (Array.isArray(attester)) {
574
+ const out = [];
575
+ for (const item of attester){
576
+ if (typeof item === 'string') {
577
+ out.push(item);
578
+ } else if ('eth' in item) {
579
+ out.push(item.eth);
580
+ } else if (!('mnemonic' in item)) {
581
+ out.push(item);
582
+ }
583
+ }
584
+ return out;
585
+ }
586
+ if ('mnemonic' in attester) {
587
+ return attester;
588
+ }
589
+ if ('eth' in attester) {
590
+ return attester.eth;
591
+ }
592
+ return attester;
593
+ }
594
+ }
@@ -0,0 +1,62 @@
1
+ import type { KeyStore } from './types.js';
2
+ /**
3
+ * Error thrown when keystore loading fails
4
+ */
5
+ export declare class KeyStoreLoadError extends Error {
6
+ filePath: string;
7
+ cause?: Error | undefined;
8
+ constructor(message: string, filePath: string, cause?: Error | undefined);
9
+ }
10
+ /**
11
+ * Loads and validates a single keystore JSON file.
12
+ *
13
+ * @param filePath Absolute or relative path to a keystore JSON file.
14
+ * @returns Parsed keystore object adhering to the schema.
15
+ * @throws KeyStoreLoadError When JSON is invalid, schema validation fails, or other IO/parse errors occur.
16
+ */
17
+ export declare function loadKeystoreFile(filePath: string): KeyStore;
18
+ /**
19
+ * Loads keystore files from a directory (only .json files).
20
+ *
21
+ * @param dirPath Absolute or relative path to a directory containing keystore files.
22
+ * @returns Array of parsed keystores loaded from all .json files in the directory.
23
+ * @throws KeyStoreLoadError When the directory can't be read or contains no valid keystore files.
24
+ */
25
+ export declare function loadKeystoreDirectory(dirPath: string): KeyStore[];
26
+ /**
27
+ * Loads keystore(s) from a path (file or directory).
28
+ *
29
+ * If a file is provided, loads a single keystore. If a directory is provided,
30
+ * loads all keystore files within that directory.
31
+ *
32
+ * @param path File or directory path.
33
+ * @returns Array of parsed keystores.
34
+ * @throws KeyStoreLoadError When the path is invalid or cannot be accessed.
35
+ */
36
+ export declare function loadKeystores(path: string): KeyStore[];
37
+ /**
38
+ * Loads keystore(s) from multiple paths (comma-separated string or array).
39
+ *
40
+ * @param paths Comma-separated string or array of file/directory paths.
41
+ * @returns Flattened array of all parsed keystores from all paths.
42
+ * @throws KeyStoreLoadError When any path fails to load; includes context for which path list was used.
43
+ */
44
+ export declare function loadMultipleKeystores(paths: string | string[]): KeyStore[];
45
+ /**
46
+ * Merges multiple keystores into a single configuration.
47
+ *
48
+ * - Concatenates validator arrays and enforces unique attester addresses by simple structural keys
49
+ * - Accumulates all slasher accounts across inputs
50
+ * - Applies last-one-wins semantics for file-level remote signer defaults
51
+ * - Requires at most one prover configuration across inputs
52
+ *
53
+ * Note: Full duplicate detection (e.g., after resolving JSON V3 or mnemonics) is
54
+ * performed downstream by the validator client.
55
+ *
56
+ * @param keystores Array of keystores to merge.
57
+ * @returns A merged keystore object.
58
+ * @throws Error When keystore list is empty.
59
+ * @throws KeyStoreLoadError When duplicate attester keys are found or multiple prover configs exist.
60
+ */
61
+ export declare function mergeKeystores(keystores: KeyStore[]): KeyStore;
62
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibG9hZGVyLmQudHMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi9zcmMvbG9hZGVyLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQWNBLE9BQU8sS0FBSyxFQUFlLFFBQVEsRUFBRSxNQUFNLFlBQVksQ0FBQztBQUl4RDs7R0FFRztBQUNILHFCQUFhLGlCQUFrQixTQUFRLEtBQUs7SUFHakMsUUFBUSxFQUFFLE1BQU07SUFDUCxLQUFLLENBQUM7SUFIeEIsWUFDRSxPQUFPLEVBQUUsTUFBTSxFQUNSLFFBQVEsRUFBRSxNQUFNLEVBQ1AsS0FBSyxDQUFDLG1CQUFPLEVBSTlCO0NBQ0Y7QUFFRDs7Ozs7O0dBTUc7QUFDSCx3QkFBZ0IsZ0JBQWdCLENBQUMsUUFBUSxFQUFFLE1BQU0sR0FBRyxRQUFRLENBdUIzRDtBQUVEOzs7Ozs7R0FNRztBQUNILHdCQUFnQixxQkFBcUIsQ0FBQyxPQUFPLEVBQUUsTUFBTSxHQUFHLFFBQVEsRUFBRSxDQW1DakU7QUFFRDs7Ozs7Ozs7O0dBU0c7QUFDSCx3QkFBZ0IsYUFBYSxDQUFDLElBQUksRUFBRSxNQUFNLEdBQUcsUUFBUSxFQUFFLENBdUJ0RDtBQUVEOzs7Ozs7R0FNRztBQUNILHdCQUFnQixxQkFBcUIsQ0FBQyxLQUFLLEVBQUUsTUFBTSxHQUFHLE1BQU0sRUFBRSxHQUFHLFFBQVEsRUFBRSxDQThCMUU7QUFFRDs7Ozs7Ozs7Ozs7Ozs7O0dBZUc7QUFDSCx3QkFBZ0IsY0FBYyxDQUFDLFNBQVMsRUFBRSxRQUFRLEVBQUUsR0FBRyxRQUFRLENBdUk5RCJ9
@@ -0,0 +1 @@
1
+ {"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../src/loader.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAe,QAAQ,EAAE,MAAM,YAAY,CAAC;AAIxD;;GAEG;AACH,qBAAa,iBAAkB,SAAQ,KAAK;IAGjC,QAAQ,EAAE,MAAM;IACP,KAAK,CAAC;IAHxB,YACE,OAAO,EAAE,MAAM,EACR,QAAQ,EAAE,MAAM,EACP,KAAK,CAAC,mBAAO,EAI9B;CACF;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,QAAQ,CAuB3D;AAED;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,GAAG,QAAQ,EAAE,CAmCjE;AAED;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,QAAQ,EAAE,CAuBtD;AAED;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,QAAQ,EAAE,CA8B1E;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,cAAc,CAAC,SAAS,EAAE,QAAQ,EAAE,GAAG,QAAQ,CAuI9D"}