zk-credits 0.2.1 → 0.2.2

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/README.md CHANGED
@@ -17,15 +17,17 @@ not mix versions between the sidecar, the adapter package, and the gateway:
17
17
 
18
18
  | Package | Version |
19
19
  | --- | --- |
20
- | `zk-credits` (this sidecar) | `0.2.0` |
20
+ | `zk-credits` (this sidecar) | `0.2.1` |
21
21
  | `@zk-credits/x402-zk-prepaid` (adapter) | `0.1.0` |
22
22
  | `@zk-credits/shared` | `0.1.0` |
23
23
 
24
- `zk-credits@0.2.0` is the currently published Base Sepolia pilot version. It
25
- uses the non-streaming `POST /v1/chat/completions` path below. The local
26
- `0.2.1` candidate adds a loopback Responses bridge for Codex and is reserved
27
- for the internal trial; it is not yet published or part of the pinned pilot
28
- set. Neither version carries a Stellar or evaluation path. The proving artifacts
24
+ `zk-credits@0.2.1` is the currently published Base Sepolia pilot version. It
25
+ uses the non-streaming `POST /v1/chat/completions` path below and includes the
26
+ loopback Responses bridge for Codex. The local `0.2.2` candidate adds local
27
+ discovery of the pinned proving bundle and witness, validates them against the
28
+ activated credential, and saves their paths for later runs. It is not yet
29
+ published or part of the pinned pilot set. Neither version carries a Stellar
30
+ or evaluation path. The proving artifacts
29
31
  (`private-credit-spend-bn254-dev-sepolia-v1`) are delivered directly through
30
32
  the invite channel, never from a public host.
31
33
 
@@ -60,31 +62,43 @@ still receives the class. A rejected request consumes no credit.
60
62
  1. Redeem an invite and download the encrypted credential. Credits are
61
63
  founder-provisioned test credits; there is no payment step.
62
64
  2. Install `zk-credits` and ask your pilot contact for the pinned proving
63
- bundle and public Base tree file. The package ships
65
+ bundle and public Base tree file. Put them in
66
+ `~/.zk-credits/artifacts/base-sepolia` and `~/.zk-credits/witnesses`, or
67
+ place them in `~/Downloads`. The package ships
64
68
  `circuits/manifest.json`, which fixes the SHA-256 of the frozen
65
69
  `private_credit_spend.wasm`, `private_credit_spend.zkey`, and
66
70
  `verification_key_private_credit.json`. The sidecar never fetches proving
67
- material at runtime. Set the paths to the downloaded files and gateway:
71
+ material at runtime. Set the credential and gateway:
68
72
 
69
73
  ```bash
70
74
  export ZK_CREDITS_CREDENTIAL_PATH="$HOME/Downloads/zk-credits-credential.json"
71
- export ZK_CREDITS_ARTIFACT_DIR="/path/to/pinned-bundle"
72
- export ZK_CREDITS_WITNESS_PATH="/path/to/base-tree.json"
73
75
  export ZK_CREDITS_GATEWAY_URL="https://zk-credits-gateway.onrender.com"
74
76
  ```
75
77
 
78
+ `zk-credits setup codex` searches those local folders, validates the bundle
79
+ hashes and witness for the activated credential, then saves the selected
80
+ local paths in `~/.zk-credits/config.json`. Set
81
+ `ZK_CREDITS_ARTIFACT_DIR` or `ZK_CREDITS_WITNESS_PATH` to select explicit
82
+ paths. Alternatively, set `BASE_RPC_URL` and
83
+ `BASE_PRIVATE_CREDIT_BOND_ADDRESS` (and optionally
84
+ `BASE_DEPLOYMENT_BLOCK`) to derive a witness from public Base events. Event
85
+ sync requires RPC access and is validated locally before setup completes.
86
+ The config file is owner-readable only and never stores the credential
87
+ password. If an RPC URL contains a provider key, it remains in that local
88
+ config file.
89
+
76
90
  A missing, relocated, symlinked-out, or altered artifact fails closed
77
91
  before the first prove. A hash mismatch is a proof failure: no payment
78
92
  leaves, and the local slot stays reusable.
79
93
 
80
- `ZK_CREDITS_WITNESS_PATH` points to a local witness artifact — either a
81
- prepared witness (`root`, `pathElements`, `pathIndices`) or a public tree
82
- (`leaves: [{ index, leaf, expiry? }]`, optional `root`) from which the
83
- sidecar derives the depth-20 path — or configure `BASE_RPC_URL`,
84
- `BASE_PRIVATE_CREDIT_BOND_ADDRESS`, and optionally
85
- `BASE_DEPLOYMENT_BLOCK` so the sidecar synchronizes public `BundleFunded`
86
- events and builds a local Merkle witness. The gateway never serves a path
87
- for a named leaf or commitment.
94
+ An explicit `ZK_CREDITS_WITNESS_PATH` points to a local witness artifact —
95
+ either a prepared witness (`root`, `pathElements`, `pathIndices`) or a
96
+ public tree (`leaves: [{ index, leaf, expiry? }]`, optional `root`) from
97
+ which the sidecar derives the depth-20 path — or configure `BASE_RPC_URL`,
98
+ `BASE_PRIVATE_CREDIT_BOND_ADDRESS`, and optionally `BASE_DEPLOYMENT_BLOCK`
99
+ so the sidecar synchronizes public `BundleFunded` events and builds a local
100
+ Merkle witness. The gateway never serves a path for a named leaf or
101
+ commitment.
88
102
 
89
103
  3. Start setup. It verifies the credential, pinned artifact hashes, and
90
104
  witness locally, then prompts for the backup password without echo:
package/dist/cli.js CHANGED
@@ -17,17 +17,26 @@ import { runCliCommand } from './cli-runtime.js';
17
17
  import { createNodeSidecarLifecycle } from './node-sidecar-lifecycle.js';
18
18
  import { activateSidecarServer } from './server-startup.js';
19
19
  import { createLoopbackToken, sidecarStatePaths } from './sidecar-config.js';
20
+ import { applySetupConfig, readSetupConfig, resolveSetupInputs, writeSetupConfig, } from './setup-config.js';
20
21
  import { ensureSidecarReady } from './sidecar-lifecycle.js';
21
22
  import { readLoopbackToken, writeLoopbackToken } from './sidecar-state.js';
22
23
  import { createSidecarServer } from './sidecar.js';
23
24
  import { runBaseRegisteredTrialExchange } from './base-registered-trial.js';
24
- import { setupPrerequisiteError } from './setup-preflight.js';
25
+ import { loadCircuitManifest } from './artifact-bundle.js';
25
26
  import { BASE_SEPOLIA_NETWORK } from '@zk-credits/x402-zk-prepaid';
26
27
  const DEFAULT_GATEWAY_URL = 'https://zk-credits-gateway.onrender.com';
27
28
  const DEFAULT_PORT = 3210;
28
29
  function stateDirectory() {
29
30
  return process.env.ZK_CREDITS_HOME || join(homedir(), '.zk-credits');
30
31
  }
32
+ function setupConfigPath(directory = stateDirectory()) {
33
+ return join(directory, 'config.json');
34
+ }
35
+ async function applySavedSetupConfig(environment, directory = stateDirectory()) {
36
+ const config = await readSetupConfig(setupConfigPath(directory));
37
+ if (config)
38
+ applySetupConfig(environment, config);
39
+ }
31
40
  function loopbackBaseUrl() {
32
41
  const port = Number(process.env.ZK_CREDITS_SIDECAR_PORT || DEFAULT_PORT);
33
42
  if (!Number.isInteger(port) || port < 1 || port > 65535) {
@@ -45,6 +54,45 @@ function readPort(args) {
45
54
  }
46
55
  return parsed;
47
56
  }
57
+ async function chooseSetupCandidate(kind, candidates) {
58
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
59
+ const variable = kind === 'proving bundle' ? 'ZK_CREDITS_ARTIFACT_DIR' : 'ZK_CREDITS_WITNESS_PATH';
60
+ throw new Error('Found more than one valid local ' + kind + '. Rerun in an interactive terminal or set ' + variable + ' explicitly.');
61
+ }
62
+ const input = createInterface({ input: process.stdin, output: process.stdout });
63
+ try {
64
+ console.log('Found multiple valid ' + kind + 's:');
65
+ candidates.forEach((candidate, index) => console.log(' ' + (index + 1) + ') ' + candidate));
66
+ const answer = await input.question('Select a number: ');
67
+ const index = Number(answer);
68
+ if (!Number.isInteger(index) || index < 1 || index > candidates.length) {
69
+ throw new Error('Choose one of the listed local candidates');
70
+ }
71
+ return candidates[index - 1];
72
+ }
73
+ finally {
74
+ input.close();
75
+ }
76
+ }
77
+ async function witnessProviderForSource(source, cachePath) {
78
+ if (source.kind === 'file') {
79
+ let value;
80
+ try {
81
+ value = JSON.parse(await readFile(source.path, 'utf8'));
82
+ }
83
+ catch {
84
+ throw new Error('The Base witness file could not be read as JSON');
85
+ }
86
+ return createFileWitnessProvider(value);
87
+ }
88
+ return createBaseEventWitnessProvider({
89
+ rpcUrl: source.rpcUrl,
90
+ contractAddress: source.contractAddress,
91
+ deploymentBlock: source.deploymentBlock === undefined ? undefined : BigInt(source.deploymentBlock),
92
+ confirmations: source.confirmations === undefined ? undefined : BigInt(source.confirmations),
93
+ cachePath,
94
+ });
95
+ }
48
96
  /** Reads a password written to the private stdin pipe by the foreground CLI. */
49
97
  async function readCredentialPasswordFromStdin() {
50
98
  const chunks = [];
@@ -125,9 +173,11 @@ non-streaming POST /v1/chat/completions; Codex Responses requests pass through
125
173
  the sidecar's bounded local compatibility bridge.
126
174
  "zk-credits trial-registered-adapter" runs one confirmed, billable internal
127
175
  trial request through the directly registered official x402 client adapter.
128
- Set ZK_CREDITS_CREDENTIAL_PATH, ZK_CREDITS_ARTIFACT_DIR, and a local witness
129
- source before setup. Setup prompts for the credential password without echo;
130
- the encrypted export is decrypted only in local memory.`);
176
+ Set ZK_CREDITS_CREDENTIAL_PATH before setup. The wizard searches Downloads and
177
+ ~/.zk-credits for the pinned bundle and local Base witness, validates both,
178
+ then saves their paths locally. Alternatively, set ZK_CREDITS_ARTIFACT_DIR and
179
+ ZK_CREDITS_WITNESS_PATH explicitly. Setup prompts for the credential password
180
+ without echo; the encrypted export is decrypted only in local memory.`);
131
181
  }
132
182
  /** Loads the same credential, witness, proof, and durable ledger used by serve. */
133
183
  async function loadBaseClientRuntime(passwordInput) {
@@ -472,6 +522,10 @@ async function main() {
472
522
  printHelp();
473
523
  return;
474
524
  }
525
+ const sidecarHome = stateDirectory();
526
+ if (args[0] !== 'setup' && args[0] !== 'status' && args[0] !== 'env') {
527
+ await applySavedSetupConfig(process.env, sidecarHome);
528
+ }
475
529
  if (args[0] === 'serve') {
476
530
  const passwordFromStdin = args.includes('--credential-password-stdin')
477
531
  ? await readCredentialPasswordFromStdin()
@@ -483,19 +537,26 @@ async function main() {
483
537
  await trialRegisteredAdapter();
484
538
  return;
485
539
  }
486
- const sidecarHome = stateDirectory();
487
540
  const statePaths = sidecarStatePaths(sidecarHome);
488
541
  const codexHome = resolveCodexHome(process.env, homedir());
489
542
  const cliEntryPath = process.argv[1];
490
543
  if (!cliEntryPath)
491
544
  throw new Error('Unable to resolve the zk-credits executable path');
545
+ let setupCredentialPassword;
492
546
  const lifecycle = createNodeSidecarLifecycle({
493
547
  loopbackBaseUrl: loopbackBaseUrl(),
494
548
  stateDirectory: sidecarHome,
495
549
  tokenPath: statePaths.tokenPath,
496
550
  logPath: statePaths.logPath,
497
551
  cliEntryPath,
498
- readCredentialPassword: () => readHiddenValue('Credential backup password: '),
552
+ readCredentialPassword: async () => {
553
+ if (setupCredentialPassword !== undefined) {
554
+ const password = setupCredentialPassword;
555
+ setupCredentialPassword = undefined;
556
+ return password;
557
+ }
558
+ return readHiddenValue('Credential backup password: ');
559
+ },
499
560
  });
500
561
  const exitCode = await runCliCommand(args, {
501
562
  loopbackBaseUrl: loopbackBaseUrl(),
@@ -514,22 +575,25 @@ async function main() {
514
575
  }
515
576
  },
516
577
  validateSetupPrerequisites: async () => {
517
- const prerequisiteError = setupPrerequisiteError(process.env);
518
- if (prerequisiteError)
519
- throw new Error(prerequisiteError);
520
- const artifactDirectory = process.env.ZK_CREDITS_ARTIFACT_DIR;
521
- if (!artifactDirectory?.trim())
522
- throw new Error('ZK_CREDITS_ARTIFACT_DIR is required.');
523
- const witnessPath = process.env.ZK_CREDITS_WITNESS_PATH;
524
- if (witnessPath) {
525
- try {
526
- const witnessFile = await readFile(witnessPath, 'utf8');
527
- createFileWitnessProvider(JSON.parse(witnessFile));
528
- }
529
- catch {
530
- throw new Error('The Base witness file could not be read. Check ZK_CREDITS_WITNESS_PATH.');
531
- }
532
- }
578
+ const credentialPath = process.env.ZK_CREDITS_CREDENTIAL_PATH;
579
+ if (!credentialPath?.trim())
580
+ throw new Error('Set ZK_CREDITS_CREDENTIAL_PATH to the encrypted browser export');
581
+ const password = await readHiddenValue('Credential backup password: ');
582
+ const credential = await readEncryptedCredential(credentialPath, password);
583
+ const manifest = await loadCircuitManifest();
584
+ const config = await resolveSetupInputs({
585
+ environment: process.env,
586
+ homeDirectory: homedir(),
587
+ stateDirectory: sidecarHome,
588
+ configPath: setupConfigPath(sidecarHome),
589
+ manifest,
590
+ validateWitness: async (source) => {
591
+ const provider = await witnessProviderForSource(source, join(stateDirectory(), 'base-event-sync.json'));
592
+ validateBaseCreditWitness(await provider.witnessForCredential(credential));
593
+ },
594
+ chooseCandidate: chooseSetupCandidate,
595
+ });
596
+ const artifactDirectory = config.artifactDirectory;
533
597
  try {
534
598
  await createPinnedBaseProofGenerator({
535
599
  artifactDirectory,
@@ -541,6 +605,11 @@ async function main() {
541
605
  throw new Error(`Pinned proving bundle check failed: ${error.message}`);
542
606
  throw new Error('Pinned proving bundle check failed. Reinstall the bundle from your pilot invite.');
543
607
  }
608
+ await writeSetupConfig(setupConfigPath(sidecarHome), config);
609
+ applySetupConfig(process.env, config);
610
+ setupCredentialPassword = password;
611
+ console.log('Pinned proving bundle and witness validated for this activated credential.');
612
+ console.log('Local setup paths saved to ' + setupConfigPath(sidecarHome));
544
613
  },
545
614
  configureCodex: async (model) => {
546
615
  await writeCodexProfile({ codexHome, loopbackBaseUrl: loopbackBaseUrl(), model });
@@ -0,0 +1,36 @@
1
+ import { type CircuitManifest } from './artifact-bundle.js';
2
+ export type SetupWitnessSource = {
3
+ kind: 'file';
4
+ path: string;
5
+ } | {
6
+ kind: 'base-events';
7
+ rpcUrl: string;
8
+ contractAddress: string;
9
+ deploymentBlock?: string;
10
+ confirmations?: string;
11
+ };
12
+ export interface SetupConfig {
13
+ version: 1;
14
+ artifactDirectory: string;
15
+ witness: SetupWitnessSource;
16
+ }
17
+ export interface ResolveSetupInputsOptions {
18
+ environment: NodeJS.ProcessEnv;
19
+ homeDirectory: string;
20
+ stateDirectory: string;
21
+ configPath: string;
22
+ manifest: CircuitManifest;
23
+ /** Must prove this source resolves a valid witness for the activated credential. */
24
+ validateWitness(source: SetupWitnessSource): Promise<void>;
25
+ /** Called only when more than one valid local candidate exists. */
26
+ chooseCandidate?(kind: 'proving bundle' | 'witness file', candidates: readonly string[]): Promise<string>;
27
+ workingDirectory?: string;
28
+ }
29
+ /** Finds only known local locations and validates both sources before setup continues. */
30
+ export declare function resolveSetupInputs(options: ResolveSetupInputsOptions): Promise<SetupConfig>;
31
+ /** Reads the operator-local setup config. Missing config is normal on first run. */
32
+ export declare function readSetupConfig(path: string): Promise<SetupConfig | undefined>;
33
+ /** Atomically writes paths and witness configuration with owner-only permissions. */
34
+ export declare function writeSetupConfig(path: string, config: SetupConfig): Promise<void>;
35
+ /** Applies saved paths to a process environment while preserving explicit overrides. */
36
+ export declare function applySetupConfig(environment: NodeJS.ProcessEnv, config: SetupConfig): void;
@@ -0,0 +1,320 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { chmod, lstat, mkdir, open, readFile, readdir, realpath, rename, rm, } from 'node:fs/promises';
3
+ import { dirname, join, resolve } from 'node:path';
4
+ import { resolvePinnedArtifactBundle } from './artifact-bundle.js';
5
+ const WITNESS_FILENAMES = new Set([
6
+ 'base-tree.json',
7
+ 'base-sepolia-tree.json',
8
+ 'base-tree-artifact.json',
9
+ 'public-tree.json',
10
+ 'base-witness.json',
11
+ 'zk-credits-witness.json',
12
+ 'witness.json',
13
+ 'tree.json',
14
+ ]);
15
+ function isRecord(value) {
16
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
17
+ }
18
+ function absolutePath(value, label) {
19
+ if (typeof value !== 'string' || value.trim().length === 0) {
20
+ throw new Error('Setup configuration ' + label + ' is required');
21
+ }
22
+ return resolve(value);
23
+ }
24
+ function optionalUnsignedString(value, label) {
25
+ if (value === undefined)
26
+ return undefined;
27
+ if (typeof value !== 'string' || !/^\d+$/u.test(value)) {
28
+ throw new Error('Local setup config ' + label + ' must be a non-negative integer');
29
+ }
30
+ return value;
31
+ }
32
+ function parseSetupConfig(value) {
33
+ if (!isRecord(value) || value.version !== 1 || !isRecord(value.witness)) {
34
+ throw new Error('Local setup config is malformed; remove ~/.zk-credits/config.json and run setup again.');
35
+ }
36
+ const artifactDirectory = absolutePath(value.artifactDirectory, 'artifactDirectory');
37
+ const witnessValue = value.witness;
38
+ let witness;
39
+ if (witnessValue.kind === 'file') {
40
+ witness = { kind: 'file', path: absolutePath(witnessValue.path, 'witness.path') };
41
+ }
42
+ else if (witnessValue.kind === 'base-events') {
43
+ if (typeof witnessValue.rpcUrl !== 'string' || !/^https?:\/\//u.test(witnessValue.rpcUrl)) {
44
+ throw new Error('Local setup config has an invalid Base RPC URL');
45
+ }
46
+ if (typeof witnessValue.contractAddress !== 'string' || !/^0x[0-9a-fA-F]{40}$/u.test(witnessValue.contractAddress)) {
47
+ throw new Error('Local setup config has an invalid Base bond contract address');
48
+ }
49
+ const deploymentBlock = optionalUnsignedString(witnessValue.deploymentBlock, 'deploymentBlock');
50
+ const confirmations = optionalUnsignedString(witnessValue.confirmations, 'confirmations');
51
+ witness = {
52
+ kind: 'base-events',
53
+ rpcUrl: witnessValue.rpcUrl,
54
+ contractAddress: witnessValue.contractAddress,
55
+ ...(deploymentBlock === undefined ? {} : { deploymentBlock }),
56
+ ...(confirmations === undefined ? {} : { confirmations }),
57
+ };
58
+ }
59
+ else {
60
+ throw new Error('Local setup config has an unknown witness source');
61
+ }
62
+ return { version: 1, artifactDirectory, witness };
63
+ }
64
+ function optionalEnvironmentInteger(environment, name) {
65
+ const value = environment[name]?.trim();
66
+ if (!value)
67
+ return undefined;
68
+ if (!/^\d+$/u.test(value))
69
+ throw new Error(name + ' must be a non-negative integer');
70
+ return value;
71
+ }
72
+ async function candidateDirectories(homeDirectory, stateDirectory) {
73
+ const roots = [
74
+ join(stateDirectory, 'artifacts', 'base-sepolia'),
75
+ join(homeDirectory, '.zk-credits', 'artifacts', 'base-sepolia'),
76
+ join(homeDirectory, 'Downloads'),
77
+ ];
78
+ const candidates = [...roots];
79
+ for (const root of roots) {
80
+ let entries;
81
+ try {
82
+ entries = await readdir(root, { withFileTypes: true });
83
+ }
84
+ catch {
85
+ continue;
86
+ }
87
+ for (const entry of entries) {
88
+ if (entry.isDirectory())
89
+ candidates.push(join(root, entry.name));
90
+ }
91
+ }
92
+ return [...new Set(candidates.map((path) => resolve(path)))];
93
+ }
94
+ async function chooseOne(kind, candidates, chooseCandidate) {
95
+ if (candidates.length === 1)
96
+ return candidates[0];
97
+ if (!chooseCandidate) {
98
+ const variable = kind === 'proving bundle' ? 'ZK_CREDITS_ARTIFACT_DIR' : 'ZK_CREDITS_WITNESS_PATH';
99
+ throw new Error('Found more than one valid ' + kind + '; set ' + variable + ' to choose one.');
100
+ }
101
+ const selected = await chooseCandidate(kind, candidates);
102
+ if (!candidates.includes(resolve(selected))) {
103
+ throw new Error('The selected ' + kind + ' is not one of the validated local candidates');
104
+ }
105
+ return resolve(selected);
106
+ }
107
+ async function resolveArtifactDirectory(options, saved) {
108
+ const explicit = options.environment.ZK_CREDITS_ARTIFACT_DIR?.trim();
109
+ const candidates = explicit
110
+ ? [resolve(options.workingDirectory ?? process.cwd(), explicit)]
111
+ : [
112
+ ...(saved ? [saved.artifactDirectory] : []),
113
+ ...await candidateDirectories(options.homeDirectory, options.stateDirectory),
114
+ ];
115
+ const valid = new Map();
116
+ let explicitError;
117
+ for (const candidate of candidates) {
118
+ try {
119
+ const bundle = await resolvePinnedArtifactBundle({
120
+ artifactDirectory: candidate,
121
+ manifest: options.manifest,
122
+ });
123
+ let identity = resolve(bundle.directory);
124
+ try {
125
+ identity = await realpath(bundle.directory);
126
+ }
127
+ catch { /* resolve already validated the directory */ }
128
+ valid.set(identity, bundle.directory);
129
+ }
130
+ catch (error) {
131
+ if (explicit)
132
+ explicitError = error;
133
+ }
134
+ }
135
+ if (explicit && valid.size === 0) {
136
+ const reason = explicitError instanceof Error ? ' ' + explicitError.message : '';
137
+ throw new Error('The configured proving bundle at ' + explicit + ' failed its pinned checks.' + reason);
138
+ }
139
+ if (valid.size === 0) {
140
+ throw new Error('Could not find a valid pinned proving bundle. Extract the bundle from your pilot contact into ~/.zk-credits/artifacts/base-sepolia or ~/Downloads, or set ZK_CREDITS_ARTIFACT_DIR.');
141
+ }
142
+ const selected = await chooseOne('proving bundle', [...valid.values()], options.chooseCandidate);
143
+ return resolve(selected);
144
+ }
145
+ function eventSourceFromEnvironment(environment) {
146
+ const rpcUrl = environment.BASE_RPC_URL?.trim();
147
+ const contractAddress = environment.BASE_PRIVATE_CREDIT_BOND_ADDRESS?.trim();
148
+ if (!rpcUrl || !contractAddress)
149
+ return undefined;
150
+ const deploymentBlock = optionalEnvironmentInteger(environment, 'BASE_DEPLOYMENT_BLOCK');
151
+ const confirmations = optionalEnvironmentInteger(environment, 'BASE_CONFIRMATIONS');
152
+ return {
153
+ kind: 'base-events',
154
+ rpcUrl,
155
+ contractAddress,
156
+ ...(deploymentBlock === undefined ? {} : { deploymentBlock }),
157
+ ...(confirmations === undefined ? {} : { confirmations }),
158
+ };
159
+ }
160
+ function witnessCandidates(homeDirectory, stateDirectory) {
161
+ const roots = [
162
+ join(stateDirectory, 'witnesses'),
163
+ join(stateDirectory, 'witness'),
164
+ join(homeDirectory, '.zk-credits', 'witnesses'),
165
+ join(homeDirectory, 'Downloads'),
166
+ ];
167
+ return [...new Set(roots.flatMap((root) => [...WITNESS_FILENAMES].map((name) => resolve(root, name))))];
168
+ }
169
+ async function resolveWitnessSource(options, saved) {
170
+ const environment = options.environment;
171
+ const explicitPath = environment.ZK_CREDITS_WITNESS_PATH?.trim();
172
+ if (explicitPath) {
173
+ const source = {
174
+ kind: 'file',
175
+ path: resolve(options.workingDirectory ?? process.cwd(), explicitPath),
176
+ };
177
+ try {
178
+ await options.validateWitness(source);
179
+ return source;
180
+ }
181
+ catch (error) {
182
+ const reason = error instanceof Error ? ' ' + error.message : '';
183
+ throw new Error('The configured Base witness at ' + explicitPath + ' failed validation.' + reason);
184
+ }
185
+ }
186
+ const environmentSource = eventSourceFromEnvironment(environment);
187
+ if (environmentSource) {
188
+ try {
189
+ await options.validateWitness(environmentSource);
190
+ return environmentSource;
191
+ }
192
+ catch (error) {
193
+ const reason = error instanceof Error ? ' ' + error.message : '';
194
+ throw new Error('Base event witness sync failed.' + reason);
195
+ }
196
+ }
197
+ if (saved?.witness.kind === 'base-events') {
198
+ try {
199
+ await options.validateWitness(saved.witness);
200
+ return saved.witness;
201
+ }
202
+ catch (error) {
203
+ const reason = error instanceof Error ? ' ' + error.message : '';
204
+ throw new Error('Saved Base event witness source failed validation.' + reason);
205
+ }
206
+ }
207
+ if (saved?.witness.kind === 'file') {
208
+ try {
209
+ await options.validateWitness(saved.witness);
210
+ return saved.witness;
211
+ }
212
+ catch {
213
+ // A saved local witness can become stale for a different activated credential.
214
+ }
215
+ }
216
+ const valid = [];
217
+ for (const path of witnessCandidates(options.homeDirectory, options.stateDirectory)) {
218
+ try {
219
+ const info = await lstat(path);
220
+ if (!info.isFile())
221
+ continue;
222
+ const source = { kind: 'file', path };
223
+ await options.validateWitness(source);
224
+ valid.push(path);
225
+ }
226
+ catch {
227
+ // Ignore absent, unrelated, and non-resolving local files while searching.
228
+ }
229
+ }
230
+ if (valid.length > 0) {
231
+ return { kind: 'file', path: await chooseOne('witness file', valid, options.chooseCandidate) };
232
+ }
233
+ throw new Error('Could not find a public Base witness for this credential. Put base-tree.json or witness.json in ~/.zk-credits/witnesses or ~/Downloads, set ZK_CREDITS_WITNESS_PATH, or configure BASE_RPC_URL and BASE_PRIVATE_CREDIT_BOND_ADDRESS.');
234
+ }
235
+ /** Finds only known local locations and validates both sources before setup continues. */
236
+ export async function resolveSetupInputs(options) {
237
+ const saved = await readSetupConfig(options.configPath);
238
+ const artifactDirectory = await resolveArtifactDirectory(options, saved);
239
+ const witness = await resolveWitnessSource(options, saved);
240
+ return { version: 1, artifactDirectory, witness };
241
+ }
242
+ /** Reads the operator-local setup config. Missing config is normal on first run. */
243
+ export async function readSetupConfig(path) {
244
+ let raw;
245
+ try {
246
+ const info = await lstat(path);
247
+ if (!info.isFile())
248
+ throw new Error('Local setup config must be a regular file');
249
+ raw = await readFile(path, 'utf8');
250
+ }
251
+ catch (error) {
252
+ if (error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT')
253
+ return undefined;
254
+ throw error;
255
+ }
256
+ try {
257
+ return parseSetupConfig(JSON.parse(raw));
258
+ }
259
+ catch (error) {
260
+ if (error instanceof SyntaxError) {
261
+ throw new Error('Local setup config is not valid JSON; remove ~/.zk-credits/config.json and run setup again.');
262
+ }
263
+ throw error;
264
+ }
265
+ }
266
+ /** Atomically writes paths and witness configuration with owner-only permissions. */
267
+ export async function writeSetupConfig(path, config) {
268
+ const normalized = parseSetupConfig(config);
269
+ const directory = dirname(resolve(path));
270
+ await mkdir(directory, { recursive: true, mode: 0o700 });
271
+ const destination = resolve(path);
272
+ try {
273
+ const existing = await lstat(destination);
274
+ if (existing.isSymbolicLink())
275
+ throw new Error('Local setup config cannot be a symbolic link');
276
+ }
277
+ catch (error) {
278
+ if (!(error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT'))
279
+ throw error;
280
+ }
281
+ const temporary = join(directory, '.config-' + randomUUID() + '.tmp');
282
+ try {
283
+ const file = await open(temporary, 'wx', 0o600);
284
+ try {
285
+ await file.writeFile(JSON.stringify(normalized, null, 2) + '\n', 'utf8');
286
+ await file.sync();
287
+ }
288
+ finally {
289
+ await file.close();
290
+ }
291
+ await rename(temporary, destination);
292
+ await chmod(destination, 0o600);
293
+ }
294
+ finally {
295
+ await rm(temporary, { force: true });
296
+ }
297
+ }
298
+ /** Applies saved paths to a process environment while preserving explicit overrides. */
299
+ export function applySetupConfig(environment, config) {
300
+ if (!environment.ZK_CREDITS_ARTIFACT_DIR?.trim()) {
301
+ environment.ZK_CREDITS_ARTIFACT_DIR = config.artifactDirectory;
302
+ }
303
+ if (config.witness.kind === 'file') {
304
+ if (!environment.ZK_CREDITS_WITNESS_PATH?.trim()) {
305
+ environment.ZK_CREDITS_WITNESS_PATH = config.witness.path;
306
+ }
307
+ return;
308
+ }
309
+ if (!environment.BASE_RPC_URL?.trim())
310
+ environment.BASE_RPC_URL = config.witness.rpcUrl;
311
+ if (!environment.BASE_PRIVATE_CREDIT_BOND_ADDRESS?.trim()) {
312
+ environment.BASE_PRIVATE_CREDIT_BOND_ADDRESS = config.witness.contractAddress;
313
+ }
314
+ if (!environment.BASE_DEPLOYMENT_BLOCK?.trim() && config.witness.deploymentBlock !== undefined) {
315
+ environment.BASE_DEPLOYMENT_BLOCK = config.witness.deploymentBlock;
316
+ }
317
+ if (!environment.BASE_CONFIRMATIONS?.trim() && config.witness.confirmations !== undefined) {
318
+ environment.BASE_CONFIRMATIONS = config.witness.confirmations;
319
+ }
320
+ }