@hivemind-os/collective-daemon 0.2.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.
@@ -0,0 +1,656 @@
1
+ import {
2
+ getDefaultIpcPath
3
+ } from "./chunk-NXIFS427.js";
4
+
5
+ // src/config.ts
6
+ import { randomUUID } from "crypto";
7
+ import * as fs from "fs";
8
+ import * as fsPromises from "fs/promises";
9
+ import { homedir } from "os";
10
+ import { dirname, extname, join, resolve } from "path";
11
+ import { isDeepStrictEqual } from "util";
12
+ import {
13
+ PaymentRail,
14
+ NETWORK_PRESETS,
15
+ getNetworkPreset
16
+ } from "@hivemind-os/collective-types";
17
+ import yaml from "js-yaml";
18
+ var LOG_LEVELS = /* @__PURE__ */ new Set(["debug", "info", "warn", "error"]);
19
+ var configSaveLocks = /* @__PURE__ */ new Map();
20
+ var configIo = {
21
+ mkdir: fsPromises.mkdir,
22
+ writeFile: fsPromises.writeFile,
23
+ rename: fsPromises.rename,
24
+ rm: fsPromises.rm
25
+ };
26
+ function getDefaultConfig() {
27
+ return buildDefaultConfig(resolve(homedir(), ".hivemind-os/collective"));
28
+ }
29
+ function getConfigPath(configPath) {
30
+ return resolve(expandHome(configPath ?? join(getEnvDataDir() ?? resolve(homedir(), ".hivemind-os/collective"), "config.yaml")));
31
+ }
32
+ function loadConfig(configPath) {
33
+ const resolvedConfigPath = getConfigPath(configPath);
34
+ const parsed = loadConfigFile(resolvedConfigPath);
35
+ const config = buildResolvedConfig(parsed);
36
+ validateConfig(config);
37
+ if (!fs.existsSync(resolvedConfigPath)) {
38
+ fs.mkdirSync(dirname(resolvedConfigPath), { recursive: true, mode: 448 });
39
+ writePrivateConfigFile(resolvedConfigPath, formatConfigContents(serializeConfig(config), resolvedConfigPath));
40
+ } else {
41
+ enforcePrivateFilePermissions(resolvedConfigPath);
42
+ }
43
+ return config;
44
+ }
45
+ async function saveConfig(config, configPath = getConfigPath()) {
46
+ const resolvedConfigPath = getConfigPath(configPath);
47
+ validateConfig(config);
48
+ await withConfigSaveLock(resolvedConfigPath, async () => {
49
+ const parsed = loadConfigFile(resolvedConfigPath);
50
+ const currentConfig = buildResolvedConfig(parsed);
51
+ const updates = collectConfigDiffs(serializeConfig(currentConfig), serializeConfig(config));
52
+ const nextParsed = applyConfigDiffs(parsed, updates);
53
+ await writePrivateConfigFileAtomic(
54
+ resolvedConfigPath,
55
+ formatConfigContents(nextParsed, resolvedConfigPath)
56
+ );
57
+ });
58
+ return resolvedConfigPath;
59
+ }
60
+ function writePrivateConfigFile(configPath, contents) {
61
+ fs.writeFileSync(configPath, contents, { encoding: "utf8", mode: 384 });
62
+ enforcePrivateFilePermissions(configPath);
63
+ }
64
+ async function writePrivateConfigFileAtomic(configPath, contents) {
65
+ const tempPath = `${configPath}.${process.pid}.${randomUUID()}.tmp`;
66
+ await configIo.mkdir(dirname(configPath), { recursive: true, mode: 448 });
67
+ try {
68
+ await configIo.writeFile(tempPath, contents, { encoding: "utf8", mode: 384 });
69
+ await configIo.rename(tempPath, configPath);
70
+ enforcePrivateFilePermissions(configPath);
71
+ } finally {
72
+ await configIo.rm(tempPath, { force: true }).catch(() => void 0);
73
+ }
74
+ }
75
+ function enforcePrivateFilePermissions(path) {
76
+ fs.chmodSync(path, 384);
77
+ }
78
+ function loadConfigFile(configPath) {
79
+ if (!fs.existsSync(configPath)) {
80
+ return {};
81
+ }
82
+ const loaded = yaml.load(fs.readFileSync(configPath, "utf8"));
83
+ return isRecord(loaded) ? loaded : {};
84
+ }
85
+ function buildResolvedConfig(parsed) {
86
+ const baseDataDir = normalizePath(
87
+ getEnvDataDir() ?? readString(getNestedValue(parsed, "daemon", "dataDir")) ?? resolve(homedir(), ".hivemind-os/collective")
88
+ );
89
+ const hasExplicitIpcPath = readString(getNestedValue(parsed, "daemon", "ipcPath")) !== void 0;
90
+ return applyEnvironmentOverrides(mergeConfig(buildDefaultConfig(baseDataDir), parsed), { hasExplicitIpcPath });
91
+ }
92
+ async function withConfigSaveLock(configPath, operation) {
93
+ const previous = configSaveLocks.get(configPath) ?? Promise.resolve();
94
+ let release;
95
+ const gate = new Promise((resolve2) => {
96
+ release = resolve2;
97
+ });
98
+ const chain = previous.catch(() => void 0).then(() => gate);
99
+ configSaveLocks.set(configPath, chain);
100
+ await previous.catch(() => void 0);
101
+ try {
102
+ return await operation();
103
+ } finally {
104
+ release();
105
+ if (configSaveLocks.get(configPath) === chain) {
106
+ configSaveLocks.delete(configPath);
107
+ }
108
+ }
109
+ }
110
+ function collectConfigDiffs(current, next, path = []) {
111
+ if (isRecord(current) && isRecord(next)) {
112
+ const diffs = [];
113
+ const keys = /* @__PURE__ */ new Set([...Object.keys(current), ...Object.keys(next)]);
114
+ for (const key of keys) {
115
+ const hasCurrent = Object.hasOwn(current, key);
116
+ const hasNext = Object.hasOwn(next, key);
117
+ if (!hasNext) {
118
+ diffs.push({ path: [...path, key], delete: true });
119
+ continue;
120
+ }
121
+ if (!hasCurrent) {
122
+ diffs.push({ path: [...path, key], value: structuredClone(next[key]) });
123
+ continue;
124
+ }
125
+ diffs.push(...collectConfigDiffs(current[key], next[key], [...path, key]));
126
+ }
127
+ return diffs;
128
+ }
129
+ return isDeepStrictEqual(current, next) ? [] : [{ path, value: structuredClone(next) }];
130
+ }
131
+ function applyConfigDiffs(parsed, diffs) {
132
+ const next = structuredClone(parsed);
133
+ for (const diff of diffs) {
134
+ if (diff.path.length === 0) {
135
+ return isRecord(diff.value) ? structuredClone(diff.value) : {};
136
+ }
137
+ if (diff.delete) {
138
+ deleteNestedValue(next, diff.path);
139
+ continue;
140
+ }
141
+ setNestedValue(next, diff.path, structuredClone(diff.value));
142
+ }
143
+ return next;
144
+ }
145
+ function setNestedValue(record, path, value) {
146
+ let current = record;
147
+ for (const segment of path.slice(0, -1)) {
148
+ const next = current[segment];
149
+ if (!isRecord(next)) {
150
+ current[segment] = {};
151
+ }
152
+ current = current[segment];
153
+ }
154
+ current[path[path.length - 1]] = value;
155
+ }
156
+ function deleteNestedValue(record, path) {
157
+ const parents = [];
158
+ let current = record;
159
+ for (const segment of path.slice(0, -1)) {
160
+ if (!current || !isRecord(current[segment])) {
161
+ return;
162
+ }
163
+ parents.push({ record: current, key: segment });
164
+ current = current[segment];
165
+ }
166
+ if (!current) {
167
+ return;
168
+ }
169
+ delete current[path[path.length - 1]];
170
+ for (let index = parents.length - 1; index >= 0; index -= 1) {
171
+ const parent = parents[index];
172
+ const child = parent.record[parent.key];
173
+ if (!isRecord(child) || Object.keys(child).length > 0) {
174
+ break;
175
+ }
176
+ delete parent.record[parent.key];
177
+ }
178
+ }
179
+ function formatConfigContents(config, configPath) {
180
+ if (extname(configPath).toLowerCase() === ".json") {
181
+ return `${JSON.stringify(config, null, 2)}
182
+ `;
183
+ }
184
+ return yaml.dump(config, { lineWidth: 120 });
185
+ }
186
+ function buildDefaultConfig(dataDir) {
187
+ const resolvedDataDir = normalizePath(dataDir);
188
+ const defaultNetwork = NETWORK_PRESETS.testnet;
189
+ return {
190
+ network: {
191
+ rpcUrl: defaultNetwork.rpcUrl,
192
+ faucetUrl: defaultNetwork.faucetUrl,
193
+ packageId: defaultNetwork.packageId,
194
+ registryId: defaultNetwork.registryId
195
+ },
196
+ identity: {
197
+ dataDir: join(resolvedDataDir, "identity")
198
+ },
199
+ auth: {
200
+ mode: "ed25519",
201
+ portal: {
202
+ port: 19876
203
+ }
204
+ },
205
+ spending: {
206
+ defaultRail: PaymentRail.SUI_ESCROW,
207
+ limits: [{ amount: 1000000000n, interval: "day", currency: "MIST" }]
208
+ },
209
+ payment: {
210
+ preferredRail: "auto",
211
+ evm: {
212
+ enabled: false,
213
+ network: "base"
214
+ }
215
+ },
216
+ daemon: {
217
+ ipcPath: getDefaultIpcPath(resolvedDataDir),
218
+ dataDir: resolvedDataDir,
219
+ pidFile: join(resolvedDataDir, "daemon.pid"),
220
+ logLevel: "info"
221
+ },
222
+ relay: {
223
+ enabled: false,
224
+ endpoints: [],
225
+ autoConnect: true,
226
+ providerMode: false,
227
+ reconnectIntervalMs: 5e3,
228
+ heartbeatIntervalMs: 3e4
229
+ },
230
+ blobstore: {
231
+ mode: "filesystem",
232
+ filesystem: {
233
+ dataDir: join(resolvedDataDir, "blobs")
234
+ }
235
+ },
236
+ encryption: {
237
+ enabled: true,
238
+ requireEncryption: false
239
+ }
240
+ };
241
+ }
242
+ function mergeConfig(defaults, parsed) {
243
+ const network = isRecord(parsed.network) ? parsed.network : {};
244
+ const identity = isRecord(parsed.identity) ? parsed.identity : {};
245
+ const auth = isRecord(parsed.auth) ? parsed.auth : {};
246
+ const payment = isRecord(parsed.payment) ? parsed.payment : {};
247
+ const daemon = isRecord(parsed.daemon) ? parsed.daemon : {};
248
+ const relay = isRecord(parsed.relay) ? parsed.relay : {};
249
+ const blobstore = isRecord(parsed.blobstore) ? parsed.blobstore : {};
250
+ const encryption = isRecord(parsed.encryption) ? parsed.encryption : {};
251
+ const provider = isRecord(parsed.provider) ? parsed.provider : void 0;
252
+ return {
253
+ network: {
254
+ ...resolveNetworkFromConfig(network, defaults.network)
255
+ },
256
+ identity: {
257
+ dataDir: normalizePath(readString(identity.dataDir) ?? defaults.identity.dataDir)
258
+ },
259
+ auth: normalizeAuthConfig(auth, defaults.auth),
260
+ spending: normalizeSpendingPolicy(parsed.spending, defaults.spending),
261
+ payment: normalizePaymentConfig(payment, defaults.payment),
262
+ daemon: {
263
+ ipcPath: readString(daemon.ipcPath) ?? defaults.daemon.ipcPath,
264
+ dataDir: normalizePath(readString(daemon.dataDir) ?? defaults.daemon.dataDir),
265
+ pidFile: normalizePath(readString(daemon.pidFile) ?? defaults.daemon.pidFile),
266
+ logLevel: normalizeLogLevel(daemon.logLevel, defaults.daemon.logLevel),
267
+ logFile: readString(daemon.logFile) ? normalizePath(readString(daemon.logFile)) : void 0
268
+ },
269
+ relay: normalizeRelayConfig(relay, defaults.relay),
270
+ blobstore: normalizeBlobStoreConfig(blobstore, defaults.blobstore),
271
+ encryption: normalizeEncryptionConfig(encryption, defaults.encryption),
272
+ provider: normalizeProviderConfig(provider)
273
+ };
274
+ }
275
+ function applyEnvironmentOverrides(config, options = { hasExplicitIpcPath: false }) {
276
+ const envDataDir = getEnvDataDir();
277
+ const withDataDir = envDataDir ? {
278
+ ...config,
279
+ identity: { dataDir: join(envDataDir, "identity") },
280
+ daemon: {
281
+ ...config.daemon,
282
+ dataDir: envDataDir,
283
+ pidFile: join(envDataDir, "daemon.pid"),
284
+ ipcPath: options.hasExplicitIpcPath ? config.daemon.ipcPath : getDefaultIpcPath(envDataDir)
285
+ },
286
+ blobstore: applyBlobStoreDataDirOverride(config.blobstore, join(envDataDir, "blobs"))
287
+ } : config;
288
+ return {
289
+ ...withDataDir,
290
+ network: {
291
+ ...withDataDir.network,
292
+ ...resolveNetworkEnvOverrides(withDataDir.network)
293
+ },
294
+ daemon: {
295
+ ...withDataDir.daemon,
296
+ logLevel: normalizeLogLevel(process.env.COLLECTIVE_LOG_LEVEL, withDataDir.daemon.logLevel)
297
+ }
298
+ };
299
+ }
300
+ function resolveNetworkEnvOverrides(base) {
301
+ const networkName = process.env.COLLECTIVE_NETWORK;
302
+ const preset = networkName ? getNetworkPreset(networkName) : void 0;
303
+ const merged = preset ? { ...base, ...preset } : base;
304
+ return {
305
+ rpcUrl: process.env.COLLECTIVE_RPC_URL ?? merged.rpcUrl,
306
+ faucetUrl: merged.faucetUrl,
307
+ packageId: process.env.COLLECTIVE_PACKAGE_ID ?? merged.packageId,
308
+ registryId: process.env.COLLECTIVE_REGISTRY_ID ?? merged.registryId
309
+ };
310
+ }
311
+ function normalizeAuthConfig(value, defaults) {
312
+ const google = isRecord(value.google) ? value.google : {};
313
+ const apple = isRecord(value.apple) ? value.apple : {};
314
+ const portal = isRecord(value.portal) ? value.portal : {};
315
+ return {
316
+ mode: value.mode === "zklogin" ? "zklogin" : defaults.mode,
317
+ google: readString(google.clientId) ? {
318
+ clientId: readString(google.clientId)
319
+ } : defaults.google,
320
+ apple: readString(apple.clientId) ? {
321
+ clientId: readString(apple.clientId)
322
+ } : defaults.apple,
323
+ portal: {
324
+ port: readPositiveInteger(portal.port, "auth.portal.port") ?? defaults.portal?.port ?? 19876
325
+ }
326
+ };
327
+ }
328
+ function normalizePaymentConfig(value, defaults) {
329
+ const evm = isRecord(value.evm) ? value.evm : {};
330
+ const preferredRail = readString(value.preferredRail);
331
+ const network = readString(evm.network);
332
+ return {
333
+ preferredRail: preferredRail === "sui" || preferredRail === "x402" || preferredRail === "auto" ? preferredRail : defaults.preferredRail,
334
+ evm: {
335
+ enabled: readBoolean(evm.enabled) ?? defaults.evm?.enabled ?? false,
336
+ network: network === "base" || network === "base-sepolia" || network === "localhost" ? network : defaults.evm?.network ?? "base",
337
+ rpcUrl: readString(evm.rpcUrl) ?? defaults.evm?.rpcUrl
338
+ }
339
+ };
340
+ }
341
+ function normalizeRelayConfig(value, defaults) {
342
+ const endpoints = Array.isArray(value.endpoints) ? value.endpoints.map((entry) => {
343
+ if (!isRecord(entry)) {
344
+ return null;
345
+ }
346
+ const url = readString(entry.url);
347
+ if (!url) {
348
+ return null;
349
+ }
350
+ const relayDid = readString(entry.relayDid);
351
+ return relayDid ? { url, relayDid } : { url };
352
+ }).filter((entry) => entry !== null) : defaults.endpoints;
353
+ return {
354
+ enabled: readBoolean(value.enabled) ?? defaults.enabled,
355
+ endpoints,
356
+ autoConnect: readBoolean(value.autoConnect) ?? defaults.autoConnect,
357
+ providerMode: readBoolean(value.providerMode) ?? defaults.providerMode,
358
+ reconnectIntervalMs: readPositiveInteger(value.reconnectIntervalMs, "relay.reconnectIntervalMs") ?? defaults.reconnectIntervalMs,
359
+ heartbeatIntervalMs: readPositiveInteger(value.heartbeatIntervalMs, "relay.heartbeatIntervalMs") ?? defaults.heartbeatIntervalMs
360
+ };
361
+ }
362
+ function normalizeEncryptionConfig(value, defaults) {
363
+ return {
364
+ enabled: readBoolean(value.enabled) ?? defaults.enabled,
365
+ requireEncryption: readBoolean(value.requireEncryption) ?? defaults.requireEncryption
366
+ };
367
+ }
368
+ function normalizeBlobStoreConfig(value, defaults) {
369
+ const blobstore = isRecord(value) ? value : {};
370
+ const filesystem = isRecord(blobstore.filesystem) ? blobstore.filesystem : {};
371
+ const walrus = isRecord(blobstore.walrus) ? blobstore.walrus : {};
372
+ const hybrid = isRecord(blobstore.hybrid) ? blobstore.hybrid : {};
373
+ const mode = normalizeBlobStoreMode(readString(blobstore.mode) ?? readString(blobstore.type) ?? defaults.mode);
374
+ const filesystemDataDir = readString(filesystem.dataDir) ?? readString(blobstore.baseDir) ?? defaults.filesystem?.dataDir;
375
+ const publisherUrl = readString(walrus.publisherUrl) ?? readString(blobstore.publisherUrl) ?? defaults.walrus?.publisherUrl;
376
+ const aggregatorUrl = readString(walrus.aggregatorUrl) ?? readString(blobstore.aggregatorUrl) ?? defaults.walrus?.aggregatorUrl;
377
+ return {
378
+ mode,
379
+ filesystem: filesystemDataDir ? {
380
+ dataDir: normalizePath(filesystemDataDir)
381
+ } : defaults.filesystem,
382
+ walrus: publisherUrl || aggregatorUrl || defaults.walrus ? {
383
+ publisherUrl: publisherUrl ?? "",
384
+ aggregatorUrl: aggregatorUrl ?? "",
385
+ epochs: readPositiveInteger(walrus.epochs, "blobstore.walrus.epochs") ?? defaults.walrus?.epochs,
386
+ maxBlobSize: readPositiveInteger(walrus.maxBlobSize, "blobstore.walrus.maxBlobSize") ?? defaults.walrus?.maxBlobSize,
387
+ retryAttempts: readPositiveInteger(walrus.retryAttempts, "blobstore.walrus.retryAttempts") ?? defaults.walrus?.retryAttempts,
388
+ retryDelayMs: readPositiveInteger(walrus.retryDelayMs, "blobstore.walrus.retryDelayMs") ?? defaults.walrus?.retryDelayMs,
389
+ timeoutMs: readPositiveInteger(walrus.timeoutMs, "blobstore.walrus.timeoutMs") ?? defaults.walrus?.timeoutMs
390
+ } : defaults.walrus,
391
+ hybrid: {
392
+ cacheLocally: readBoolean(hybrid.cacheLocally) ?? defaults.hybrid?.cacheLocally ?? true,
393
+ preferWalrus: readBoolean(hybrid.preferWalrus) ?? defaults.hybrid?.preferWalrus ?? true
394
+ }
395
+ };
396
+ }
397
+ function applyBlobStoreDataDirOverride(config, dataDir) {
398
+ if (config.mode !== "filesystem" && config.mode !== "hybrid") {
399
+ return config;
400
+ }
401
+ return {
402
+ ...config,
403
+ filesystem: {
404
+ dataDir
405
+ }
406
+ };
407
+ }
408
+ function normalizeBlobStoreMode(value) {
409
+ if (value === "filesystem" || value === "walrus" || value === "hybrid") {
410
+ return value;
411
+ }
412
+ throw new Error(`Unsupported blobstore mode: ${value}`);
413
+ }
414
+ function normalizeSpendingPolicy(value, defaults) {
415
+ if (!isRecord(value)) {
416
+ return defaults;
417
+ }
418
+ return {
419
+ defaultRail: normalizeRail(value.defaultRail) ?? defaults.defaultRail,
420
+ requireConfirmationAbove: value.requireConfirmationAbove === void 0 ? defaults.requireConfirmationAbove : parseBigInt(value.requireConfirmationAbove, "spending.requireConfirmationAbove"),
421
+ allowlist: Array.isArray(value.allowlist) ? value.allowlist.filter((entry) => typeof entry === "string") : defaults.allowlist,
422
+ denylist: Array.isArray(value.denylist) ? value.denylist.filter((entry) => typeof entry === "string") : defaults.denylist,
423
+ limits: Array.isArray(value.limits) && value.limits.length > 0 ? value.limits.map((limit, index) => normalizeSpendingLimit(limit, index, "spending.limits")) : defaults.limits,
424
+ perApp: normalizePerAppSpendingConfig(value.perApp, defaults.perApp)
425
+ };
426
+ }
427
+ function normalizePerAppSpendingConfig(value, defaults) {
428
+ if (!isRecord(value)) {
429
+ return defaults;
430
+ }
431
+ const perApp = Object.entries(value).map(([appName, config]) => {
432
+ if (!isRecord(config)) {
433
+ throw new Error(`spending.perApp.${appName} must be an object.`);
434
+ }
435
+ if (!Array.isArray(config.limits) || config.limits.length === 0) {
436
+ throw new Error(`spending.perApp.${appName}.limits must contain at least one limit.`);
437
+ }
438
+ return [
439
+ appName,
440
+ {
441
+ limits: config.limits.map(
442
+ (limit, index) => normalizeSpendingLimit(limit, index, `spending.perApp.${appName}.limits`)
443
+ )
444
+ }
445
+ ];
446
+ });
447
+ return Object.fromEntries(perApp);
448
+ }
449
+ function normalizeSpendingLimit(value, index, path) {
450
+ if (!isRecord(value)) {
451
+ throw new Error(`${path}[${index}] must be an object.`);
452
+ }
453
+ const interval = readString(value.interval);
454
+ if (!interval || !["transaction", "hour", "day", "month", "lifetime"].includes(interval)) {
455
+ throw new Error(`${path}[${index}].interval is invalid.`);
456
+ }
457
+ const normalizedInterval = interval;
458
+ return {
459
+ amount: parseBigInt(value.amount, `${path}[${index}].amount`),
460
+ interval: normalizedInterval,
461
+ rail: normalizeRail(value.rail),
462
+ currency: readString(value.currency)?.toUpperCase(),
463
+ scope: readString(value.scope) ?? void 0
464
+ };
465
+ }
466
+ function normalizeProviderConfig(value) {
467
+ if (!value) {
468
+ return void 0;
469
+ }
470
+ const capabilities = Array.isArray(value.capabilities) ? value.capabilities.map((entry, index) => normalizeProviderCapability(entry, index)) : [];
471
+ return {
472
+ enabled: readBoolean(value.enabled) ?? false,
473
+ capabilities,
474
+ maxConcurrency: readPositiveInteger(value.maxConcurrency, "provider.maxConcurrency"),
475
+ autoRegister: readBoolean(value.autoRegister)
476
+ };
477
+ }
478
+ function normalizeProviderCapability(value, index) {
479
+ if (!isRecord(value)) {
480
+ throw new Error(`provider.capabilities[${index}] must be an object.`);
481
+ }
482
+ const name = readString(value.name);
483
+ const description = readString(value.description);
484
+ const version = readString(value.version);
485
+ const adapter = readString(value.adapter);
486
+ const priceMist = readPositiveInteger(value.priceMist, `provider.capabilities[${index}].priceMist`);
487
+ if (!name || !description || !version || !adapter || priceMist === void 0) {
488
+ throw new Error(`provider.capabilities[${index}] is incomplete.`);
489
+ }
490
+ return {
491
+ name,
492
+ description,
493
+ version,
494
+ priceMist,
495
+ currency: readString(value.currency),
496
+ adapter,
497
+ adapterConfig: isRecord(value.adapterConfig) ? value.adapterConfig : void 0
498
+ };
499
+ }
500
+ function validateConfig(config) {
501
+ if (!config.network.rpcUrl) {
502
+ throw new Error("network.rpcUrl is required.");
503
+ }
504
+ if (!config.identity.dataDir) {
505
+ throw new Error("identity.dataDir is required.");
506
+ }
507
+ if (!config.auth.mode) {
508
+ throw new Error("auth.mode is required.");
509
+ }
510
+ if (config.auth.mode === "zklogin" && !config.auth.google?.clientId) {
511
+ throw new Error("auth.google.clientId is required when auth.mode is zklogin.");
512
+ }
513
+ if (!config.payment.evm) {
514
+ throw new Error("payment.evm configuration is required.");
515
+ }
516
+ if (!config.daemon.ipcPath || !config.daemon.dataDir || !config.daemon.pidFile) {
517
+ throw new Error("daemon configuration is incomplete.");
518
+ }
519
+ if (config.relay.enabled && config.relay.endpoints.length === 0) {
520
+ throw new Error("relay.endpoints must contain at least one entry when relay is enabled.");
521
+ }
522
+ for (const endpoint of config.relay.endpoints) {
523
+ if (!/^wss?:\/\//i.test(endpoint.url)) {
524
+ throw new Error(`Invalid relay endpoint URL: ${endpoint.url}`);
525
+ }
526
+ }
527
+ if (config.blobstore.mode === "filesystem" || config.blobstore.mode === "hybrid") {
528
+ if (!config.blobstore.filesystem?.dataDir) {
529
+ throw new Error("blobstore.filesystem.dataDir is required.");
530
+ }
531
+ }
532
+ if (config.blobstore.mode === "walrus" || config.blobstore.mode === "hybrid") {
533
+ if (!config.blobstore.walrus?.publisherUrl || !config.blobstore.walrus.aggregatorUrl) {
534
+ throw new Error("blobstore.walrus.publisherUrl and blobstore.walrus.aggregatorUrl are required.");
535
+ }
536
+ }
537
+ if (!LOG_LEVELS.has(config.daemon.logLevel)) {
538
+ throw new Error(`Invalid log level: ${config.daemon.logLevel}`);
539
+ }
540
+ if (config.encryption.requireEncryption && !config.encryption.enabled) {
541
+ throw new Error("encryption.requireEncryption cannot be true when encryption.enabled is false.");
542
+ }
543
+ }
544
+ function serializeConfig(config) {
545
+ return serializeValue(config);
546
+ }
547
+ function serializeValue(value) {
548
+ if (typeof value === "bigint") {
549
+ return value.toString();
550
+ }
551
+ if (Array.isArray(value)) {
552
+ return value.map((entry) => serializeValue(entry));
553
+ }
554
+ if (isRecord(value)) {
555
+ return Object.fromEntries(
556
+ Object.entries(value).filter(([, entry]) => entry !== void 0).map(([key, entry]) => [key, serializeValue(entry)])
557
+ );
558
+ }
559
+ return value;
560
+ }
561
+ function normalizeLogLevel(value, fallback) {
562
+ return typeof value === "string" && LOG_LEVELS.has(value) ? value : fallback;
563
+ }
564
+ function normalizeRail(value) {
565
+ if (value === PaymentRail.SUI_ESCROW || value === PaymentRail.SUI_TRANSFER || value === PaymentRail.X402_BASE) {
566
+ return value;
567
+ }
568
+ return void 0;
569
+ }
570
+ function parseBigInt(value, field) {
571
+ if (typeof value === "bigint") {
572
+ return value;
573
+ }
574
+ if (typeof value === "number" && Number.isSafeInteger(value) && value >= 0) {
575
+ return BigInt(value);
576
+ }
577
+ if (typeof value === "string" && /^\d+$/.test(value.trim())) {
578
+ return BigInt(value.trim());
579
+ }
580
+ throw new Error(`${field} must be a non-negative integer.`);
581
+ }
582
+ function getEnvDataDir() {
583
+ return process.env.COLLECTIVE_DATA_DIR ? normalizePath(process.env.COLLECTIVE_DATA_DIR) : void 0;
584
+ }
585
+ function getNestedValue(record, ...keys) {
586
+ let current = record;
587
+ for (const key of keys) {
588
+ if (!isRecord(current)) {
589
+ return void 0;
590
+ }
591
+ current = current[key];
592
+ }
593
+ return current;
594
+ }
595
+ function normalizePath(value) {
596
+ return resolve(expandHome(value));
597
+ }
598
+ function expandHome(value) {
599
+ if (value === "~") {
600
+ return homedir();
601
+ }
602
+ if (value.startsWith("~/") || value.startsWith("~\\")) {
603
+ return join(homedir(), value.slice(2));
604
+ }
605
+ return value;
606
+ }
607
+ function resolveNetworkFromConfig(network, defaults) {
608
+ const nameField = readString(network.name);
609
+ const preset = nameField ? getNetworkPreset(nameField) : void 0;
610
+ const base = preset ?? defaults;
611
+ return {
612
+ rpcUrl: readString(network.rpcUrl) ?? base.rpcUrl,
613
+ faucetUrl: readString(network.faucetUrl) ?? base.faucetUrl,
614
+ packageId: readHexString(network.packageId) ?? base.packageId,
615
+ registryId: readHexString(network.registryId) ?? base.registryId
616
+ };
617
+ }
618
+ function readString(value) {
619
+ return typeof value === "string" && value.trim() ? value.trim() : void 0;
620
+ }
621
+ function readBoolean(value) {
622
+ return typeof value === "boolean" ? value : void 0;
623
+ }
624
+ function readPositiveInteger(value, field) {
625
+ if (value === void 0) {
626
+ return void 0;
627
+ }
628
+ if (typeof value === "number" && Number.isSafeInteger(value) && value >= 0) {
629
+ return value;
630
+ }
631
+ if (typeof value === "string" && /^\d+$/.test(value.trim())) {
632
+ return Number(value.trim());
633
+ }
634
+ throw new Error(`${field} must be a non-negative integer.`);
635
+ }
636
+ function readHexString(value) {
637
+ if (typeof value === "string" && value.trim()) {
638
+ return value.trim();
639
+ }
640
+ if (typeof value === "number" && Number.isSafeInteger(value) && value >= 0) {
641
+ return `0x${value.toString(16)}`;
642
+ }
643
+ return void 0;
644
+ }
645
+ function isRecord(value) {
646
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
647
+ }
648
+
649
+ export {
650
+ configIo,
651
+ getDefaultConfig,
652
+ getConfigPath,
653
+ loadConfig,
654
+ saveConfig
655
+ };
656
+ //# sourceMappingURL=chunk-CJHYQ7RR.js.map