@aztec/node-keystore 0.0.1-commit.21caa21

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/dest/loader.js ADDED
@@ -0,0 +1,274 @@
1
+ /**
2
+ * Keystore File Loader
3
+ *
4
+ * Handles loading and parsing keystore configuration files.
5
+ */ import { createLogger } from '@aztec/foundation/log';
6
+ import { readFileSync, readdirSync, statSync } from 'fs';
7
+ import { extname, join } from 'path';
8
+ import { keystoreSchema } from './schemas.js';
9
+ const logger = createLogger('node-keystore:loader');
10
+ /**
11
+ * Error thrown when keystore loading fails
12
+ */ export class KeyStoreLoadError extends Error {
13
+ filePath;
14
+ cause;
15
+ constructor(message, filePath, cause){
16
+ super(`Failed to load keystore from ${filePath}: ${message}`), this.filePath = filePath, this.cause = cause;
17
+ this.name = 'KeyStoreLoadError';
18
+ }
19
+ }
20
+ /**
21
+ * Loads and validates a single keystore JSON file.
22
+ *
23
+ * @param filePath Absolute or relative path to a keystore JSON file.
24
+ * @returns Parsed keystore object adhering to the schema.
25
+ * @throws KeyStoreLoadError When JSON is invalid, schema validation fails, or other IO/parse errors occur.
26
+ */ export function loadKeystoreFile(filePath) {
27
+ try {
28
+ const content = readFileSync(filePath, 'utf-8');
29
+ // Parse JSON and validate with Zod schema (following Aztec patterns)
30
+ return keystoreSchema.parse(JSON.parse(content));
31
+ } catch (error) {
32
+ if (error instanceof SyntaxError) {
33
+ throw new KeyStoreLoadError('Invalid JSON format', filePath, error);
34
+ }
35
+ if (error && typeof error === 'object' && 'issues' in error) {
36
+ const issues = error.issues ?? [];
37
+ const message = issues.map((e)=>{
38
+ const path = Array.isArray(e.path) ? e.path.join('.') : String(e.path ?? 'root');
39
+ return `${e.message} (${path})`;
40
+ }).join('. ') || 'Schema validation error';
41
+ throw new KeyStoreLoadError(`Schema validation failed: ${message}`, filePath, error);
42
+ }
43
+ throw new KeyStoreLoadError(`Unexpected error: ${String(error)}`, filePath, error);
44
+ }
45
+ }
46
+ /**
47
+ * Loads keystore files from a directory (only .json files).
48
+ *
49
+ * @param dirPath Absolute or relative path to a directory containing keystore files.
50
+ * @returns Array of parsed keystores loaded from all .json files in the directory.
51
+ * @throws KeyStoreLoadError When the directory can't be read or contains no valid keystore files.
52
+ */ export function loadKeystoreDirectory(dirPath) {
53
+ try {
54
+ const files = readdirSync(dirPath);
55
+ const keystores = [];
56
+ for (const file of files){
57
+ // Only process .json files
58
+ if (extname(file).toLowerCase() !== '.json') {
59
+ continue;
60
+ }
61
+ const filePath = join(dirPath, file);
62
+ try {
63
+ const keystore = loadKeystoreFile(filePath);
64
+ keystores.push(keystore);
65
+ } catch (error) {
66
+ // Re-throw with directory context
67
+ if (error instanceof KeyStoreLoadError) {
68
+ throw error;
69
+ }
70
+ throw new KeyStoreLoadError(`Failed to load file ${file}`, filePath, error);
71
+ }
72
+ }
73
+ if (keystores.length === 0) {
74
+ throw new KeyStoreLoadError('No valid keystore files found', dirPath);
75
+ }
76
+ return keystores;
77
+ } catch (error) {
78
+ if (error instanceof KeyStoreLoadError) {
79
+ throw error;
80
+ }
81
+ throw new KeyStoreLoadError(`Failed to read directory`, dirPath, error);
82
+ }
83
+ }
84
+ /**
85
+ * Loads keystore(s) from a path (file or directory).
86
+ *
87
+ * If a file is provided, loads a single keystore. If a directory is provided,
88
+ * loads all keystore files within that directory.
89
+ *
90
+ * @param path File or directory path.
91
+ * @returns Array of parsed keystores.
92
+ * @throws KeyStoreLoadError When the path is invalid or cannot be accessed.
93
+ */ export function loadKeystores(path) {
94
+ try {
95
+ const stats = statSync(path);
96
+ if (stats.isFile()) {
97
+ return [
98
+ loadKeystoreFile(path)
99
+ ];
100
+ } else if (stats.isDirectory()) {
101
+ return loadKeystoreDirectory(path);
102
+ } else {
103
+ throw new KeyStoreLoadError('Path is neither a file nor directory', path);
104
+ }
105
+ } catch (error) {
106
+ if (error instanceof KeyStoreLoadError) {
107
+ throw error;
108
+ }
109
+ const err = error;
110
+ if (err?.code === 'ENOENT') {
111
+ throw new KeyStoreLoadError('File or directory not found', path, error);
112
+ }
113
+ throw new KeyStoreLoadError(`Failed to access path: ${err?.code ?? 'UNKNOWN'}`, path, error);
114
+ }
115
+ }
116
+ /**
117
+ * Loads keystore(s) from multiple paths (comma-separated string or array).
118
+ *
119
+ * @param paths Comma-separated string or array of file/directory paths.
120
+ * @returns Flattened array of all parsed keystores from all paths.
121
+ * @throws KeyStoreLoadError When any path fails to load; includes context for which path list was used.
122
+ */ export function loadMultipleKeystores(paths) {
123
+ const pathArray = typeof paths === 'string' ? paths.split(',').map((p)=>p.trim()) : paths;
124
+ const allKeystores = [];
125
+ for (const path of pathArray){
126
+ if (!path) {
127
+ continue;
128
+ } // Skip empty paths
129
+ try {
130
+ const keystores = loadKeystores(path);
131
+ allKeystores.push(...keystores);
132
+ } catch (error) {
133
+ // Add context about which path failed
134
+ if (error instanceof KeyStoreLoadError) {
135
+ throw new KeyStoreLoadError(`${error.message} (from path list: ${pathArray.join(', ')})`, error.filePath, error.cause);
136
+ }
137
+ throw error;
138
+ }
139
+ }
140
+ if (allKeystores.length === 0) {
141
+ throw new KeyStoreLoadError('No keystore files found in any of the provided paths', pathArray.join(', '));
142
+ }
143
+ return allKeystores;
144
+ }
145
+ /**
146
+ * Merges multiple keystores into a single configuration.
147
+ *
148
+ * - Concatenates validator arrays and enforces unique attester addresses by simple structural keys
149
+ * - Accumulates all slasher accounts across inputs
150
+ * - Applies last-one-wins semantics for file-level remote signer defaults
151
+ * - Requires at most one prover configuration across inputs
152
+ *
153
+ * Note: Full duplicate detection (e.g., after resolving JSON V3 or mnemonics) is
154
+ * performed downstream by the validator client.
155
+ *
156
+ * @param keystores Array of keystores to merge.
157
+ * @returns A merged keystore object.
158
+ * @throws Error When keystore list is empty.
159
+ * @throws KeyStoreLoadError When duplicate attester keys are found or multiple prover configs exist.
160
+ */ export function mergeKeystores(keystores) {
161
+ if (keystores.length === 0) {
162
+ throw new Error('Cannot merge empty keystore list');
163
+ }
164
+ if (keystores.length === 1) {
165
+ return keystores[0];
166
+ }
167
+ // Track attester addresses to prevent duplicates
168
+ const attesterAddresses = new Set();
169
+ const merged = {
170
+ schemaVersion: 1,
171
+ validators: [],
172
+ slasher: undefined,
173
+ remoteSigner: undefined,
174
+ prover: undefined
175
+ };
176
+ for(let i = 0; i < keystores.length; i++){
177
+ const keystore = keystores[i];
178
+ // Merge validators
179
+ if (keystore.validators) {
180
+ for (const validator of keystore.validators){
181
+ // Check for duplicate attester addresses
182
+ const attesterKeys = extractAttesterKeys(validator.attester);
183
+ for (const key of attesterKeys){
184
+ if (attesterAddresses.has(key)) {
185
+ throw new KeyStoreLoadError(`Duplicate attester address ${key} found across keystore files`, `keystores[${i}].validators`);
186
+ }
187
+ attesterAddresses.add(key);
188
+ }
189
+ }
190
+ merged.validators.push(...keystore.validators);
191
+ }
192
+ // Merge slasher (accumulate all)
193
+ if (keystore.slasher) {
194
+ if (!merged.slasher) {
195
+ merged.slasher = keystore.slasher;
196
+ } else {
197
+ const toArray = (accounts)=>Array.isArray(accounts) ? accounts : [
198
+ accounts
199
+ ];
200
+ const combined = [
201
+ ...toArray(merged.slasher),
202
+ ...toArray(keystore.slasher)
203
+ ];
204
+ // Cast is safe at runtime: consumer handles arrays with mixed account configs
205
+ merged.slasher = combined;
206
+ }
207
+ }
208
+ // Merge remote signer (last one wins, but warn about conflicts)
209
+ if (keystore.remoteSigner) {
210
+ if (merged.remoteSigner) {
211
+ logger.warn('Multiple default remote signer configurations found, using the last one');
212
+ }
213
+ merged.remoteSigner = keystore.remoteSigner;
214
+ }
215
+ // Merge prover (error if multiple)
216
+ if (keystore.prover) {
217
+ if (merged.prover) {
218
+ throw new KeyStoreLoadError('Multiple prover configurations found across keystore files. Only one prover configuration is allowed.', `keystores[${i}].prover`);
219
+ }
220
+ merged.prover = keystore.prover;
221
+ }
222
+ }
223
+ // Clean up empty arrays
224
+ if (merged.validators.length === 0) {
225
+ delete merged.validators;
226
+ }
227
+ return merged;
228
+ }
229
+ /**
230
+ * Extracts attester addresses/keys for coarse duplicate checking during merge.
231
+ *
232
+ * This avoids expensive resolution/decryption and is intended as a best-effort
233
+ * guard only. Full duplicate detection is done in the validator client after
234
+ * accounts are fully resolved.
235
+ *
236
+ * @param attester The attester configuration in any supported shape.
237
+ * @returns Array of string keys used to detect duplicates.
238
+ */ function extractAttesterKeys(attester) {
239
+ // String forms (private key or other) - return as-is for coarse uniqueness
240
+ if (typeof attester === 'string') {
241
+ return [
242
+ attester
243
+ ];
244
+ }
245
+ // Arrays of attester items
246
+ if (Array.isArray(attester)) {
247
+ const keys = [];
248
+ for (const item of attester){
249
+ keys.push(...extractAttesterKeys(item));
250
+ }
251
+ return keys;
252
+ }
253
+ if (attester && typeof attester === 'object') {
254
+ const obj = attester;
255
+ // New shape: { eth: EthAccount, bls?: BLSAccount }
256
+ if ('eth' in obj) {
257
+ return extractAttesterKeys(obj.eth);
258
+ }
259
+ // Remote signer account object shape: { address, remoteSignerUrl?, ... }
260
+ if ('address' in obj) {
261
+ return [
262
+ String(obj.address)
263
+ ];
264
+ }
265
+ // Mnemonic or other object shapes: stringify
266
+ return [
267
+ JSON.stringify(attester)
268
+ ];
269
+ }
270
+ // Fallback stringify for anything else (null/undefined)
271
+ return [
272
+ JSON.stringify(attester)
273
+ ];
274
+ }