zk-credits 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/circuits/manifest.json +16 -0
- package/circuits/rln_nullifier.wasm +0 -0
- package/circuits/rln_nullifier_final.zkey +0 -0
- package/circuits/verification_key_rln.json +109 -0
- package/dist/artifact-manifest.d.ts +8 -0
- package/dist/artifact-manifest.js +29 -0
- package/dist/cli-runtime.d.ts +15 -0
- package/dist/cli-runtime.js +55 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +175 -0
- package/dist/codex-launcher.d.ts +10 -0
- package/dist/codex-launcher.js +17 -0
- package/dist/codex-profile.d.ts +16 -0
- package/dist/codex-profile.js +103 -0
- package/dist/identity.d.ts +18 -0
- package/dist/identity.js +48 -0
- package/dist/local-prover.d.ts +18 -0
- package/dist/local-prover.js +32 -0
- package/dist/membership-client.d.ts +14 -0
- package/dist/membership-client.js +66 -0
- package/dist/node-sidecar-lifecycle.d.ts +21 -0
- package/dist/node-sidecar-lifecycle.js +65 -0
- package/dist/server-startup.d.ts +6 -0
- package/dist/server-startup.js +6 -0
- package/dist/sidecar-config.d.ts +9 -0
- package/dist/sidecar-config.js +23 -0
- package/dist/sidecar-lifecycle.d.ts +13 -0
- package/dist/sidecar-lifecycle.js +14 -0
- package/dist/sidecar-state.d.ts +2 -0
- package/dist/sidecar-state.js +18 -0
- package/dist/sidecar.d.ts +27 -0
- package/dist/sidecar.js +153 -0
- package/dist/ticket-ledger.d.ts +22 -0
- package/dist/ticket-ledger.js +91 -0
- package/dist/zk-credits.js +1087 -0
- package/package.json +36 -0
|
@@ -0,0 +1,1087 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire as __createNodeRequire } from 'node:module'; const require = __createNodeRequire(import.meta.url);
|
|
3
|
+
|
|
4
|
+
// dist/cli.js
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
|
+
import { join as join4 } from "node:path";
|
|
7
|
+
|
|
8
|
+
// dist/codex-launcher.js
|
|
9
|
+
import { spawn } from "node:child_process";
|
|
10
|
+
|
|
11
|
+
// dist/codex-profile.js
|
|
12
|
+
import { randomBytes } from "node:crypto";
|
|
13
|
+
import { chmod, mkdir, rename, stat, writeFile } from "node:fs/promises";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
var CODEX_PROFILE_NAME = "zk-credits";
|
|
16
|
+
var DEFAULT_CODEX_MODEL = "openai/gpt-4o-mini";
|
|
17
|
+
function resolveCodexHome(environment, userHome) {
|
|
18
|
+
return environment.CODEX_HOME || join(userHome, ".codex");
|
|
19
|
+
}
|
|
20
|
+
function tomlString(value) {
|
|
21
|
+
return JSON.stringify(value);
|
|
22
|
+
}
|
|
23
|
+
function providerBaseUrl(loopbackBaseUrl2) {
|
|
24
|
+
const url = new URL(loopbackBaseUrl2);
|
|
25
|
+
if (url.protocol !== "http:" || url.hostname !== "127.0.0.1") {
|
|
26
|
+
throw new Error("Codex provider must use a 127.0.0.1 HTTP sidecar URL");
|
|
27
|
+
}
|
|
28
|
+
return `${url.origin}/v1`;
|
|
29
|
+
}
|
|
30
|
+
function codexProfilePath(codexHome) {
|
|
31
|
+
return join(codexHome, `${CODEX_PROFILE_NAME}.config.toml`);
|
|
32
|
+
}
|
|
33
|
+
function renderCodexProfile(options) {
|
|
34
|
+
const model = options.model?.trim() || DEFAULT_CODEX_MODEL;
|
|
35
|
+
return [
|
|
36
|
+
`model = ${tomlString(model)}`,
|
|
37
|
+
'model_provider = "zk_credits"',
|
|
38
|
+
"",
|
|
39
|
+
"[model_providers.zk_credits]",
|
|
40
|
+
'name = "ZK Credits"',
|
|
41
|
+
`base_url = ${tomlString(providerBaseUrl(options.loopbackBaseUrl))}`,
|
|
42
|
+
'wire_api = "responses"',
|
|
43
|
+
"",
|
|
44
|
+
"[model_providers.zk_credits.auth]",
|
|
45
|
+
'command = "zk-credits"',
|
|
46
|
+
'args = ["token"]',
|
|
47
|
+
"refresh_interval_ms = 0",
|
|
48
|
+
""
|
|
49
|
+
].join("\n");
|
|
50
|
+
}
|
|
51
|
+
function codexModelsResponse() {
|
|
52
|
+
return {
|
|
53
|
+
models: [{
|
|
54
|
+
slug: DEFAULT_CODEX_MODEL,
|
|
55
|
+
display_name: "GPT-4o Mini (ZK Credits)",
|
|
56
|
+
description: "OpenRouter coding model paid with private ZK Credits tickets.",
|
|
57
|
+
default_reasoning_level: null,
|
|
58
|
+
supported_reasoning_levels: [],
|
|
59
|
+
shell_type: "shell_command",
|
|
60
|
+
visibility: "list",
|
|
61
|
+
supported_in_api: true,
|
|
62
|
+
priority: 1,
|
|
63
|
+
additional_speed_tiers: [],
|
|
64
|
+
service_tiers: [],
|
|
65
|
+
default_service_tier: null,
|
|
66
|
+
availability_nux: null,
|
|
67
|
+
upgrade: null,
|
|
68
|
+
base_instructions: [
|
|
69
|
+
"You are Codex, a coding agent. Follow the user's instructions carefully.",
|
|
70
|
+
"Use the provided tools when needed, preserve existing work, and verify changes before claiming completion."
|
|
71
|
+
].join(" "),
|
|
72
|
+
include_skills_usage_instructions: true,
|
|
73
|
+
include_plugin_usage_instructions: true,
|
|
74
|
+
include_apps_usage_instructions: true,
|
|
75
|
+
supports_reasoning_summary_parameter: false,
|
|
76
|
+
default_reasoning_summary: "none",
|
|
77
|
+
support_verbosity: false,
|
|
78
|
+
default_verbosity: null,
|
|
79
|
+
apply_patch_tool_type: "freeform",
|
|
80
|
+
web_search_tool_type: "text",
|
|
81
|
+
truncation_policy: { mode: "tokens", limit: 1e4 },
|
|
82
|
+
supports_parallel_tool_calls: true,
|
|
83
|
+
supports_image_detail_original: false,
|
|
84
|
+
context_window: 128e3,
|
|
85
|
+
max_context_window: 128e3,
|
|
86
|
+
experimental_supported_tools: [],
|
|
87
|
+
input_modalities: ["text", "image"],
|
|
88
|
+
supports_search_tool: false,
|
|
89
|
+
use_responses_lite: false,
|
|
90
|
+
tool_mode: "direct"
|
|
91
|
+
}]
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
async function writeCodexProfile(options) {
|
|
95
|
+
await mkdir(options.codexHome, { recursive: true, mode: 448 });
|
|
96
|
+
const profilePath = codexProfilePath(options.codexHome);
|
|
97
|
+
const temporaryPath = `${profilePath}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
|
|
98
|
+
await writeFile(temporaryPath, renderCodexProfile(options), { encoding: "utf8", mode: 384 });
|
|
99
|
+
await chmod(temporaryPath, 384);
|
|
100
|
+
await rename(temporaryPath, profilePath);
|
|
101
|
+
await chmod(profilePath, 384);
|
|
102
|
+
return profilePath;
|
|
103
|
+
}
|
|
104
|
+
async function isCodexProfileInstalled(codexHome) {
|
|
105
|
+
try {
|
|
106
|
+
return (await stat(codexProfilePath(codexHome))).isFile();
|
|
107
|
+
} catch (error) {
|
|
108
|
+
if (error.code === "ENOENT")
|
|
109
|
+
return false;
|
|
110
|
+
throw error;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// dist/codex-launcher.js
|
|
115
|
+
var spawnCommand = (command, args, options) => spawn(command, args, options);
|
|
116
|
+
async function launchCodexProcess(args, spawnImpl = spawnCommand) {
|
|
117
|
+
const child = spawnImpl("codex", ["--profile", CODEX_PROFILE_NAME, ...args], { stdio: "inherit" });
|
|
118
|
+
return new Promise((resolve2, reject) => {
|
|
119
|
+
child.once("error", (error) => {
|
|
120
|
+
if (error.code === "ENOENT") {
|
|
121
|
+
reject(new Error("Codex CLI was not found; install it before running zk-credits codex"));
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
reject(error);
|
|
125
|
+
});
|
|
126
|
+
child.once("exit", (code) => resolve2(code ?? 1));
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// dist/identity.js
|
|
131
|
+
import * as keytar from "keytar";
|
|
132
|
+
|
|
133
|
+
// ../zk-credits-shared/dist/crypto.js
|
|
134
|
+
import { entropyToMnemonic, mnemonicToEntropy } from "@scure/bip39";
|
|
135
|
+
import { wordlist } from "@scure/bip39/wordlists/english.js";
|
|
136
|
+
var FR_ORDER = "0x73eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000001";
|
|
137
|
+
var FIELD_ORDER = BigInt(FR_ORDER);
|
|
138
|
+
var MIMC_ROUNDS = 220;
|
|
139
|
+
var MEMBERSHIP_TREE_DEPTH = 3;
|
|
140
|
+
var MEMBERSHIP_TREE_CAPACITY = 2 ** MEMBERSHIP_TREE_DEPTH;
|
|
141
|
+
var mimcConstantsPromise = null;
|
|
142
|
+
function toHexBytes(secretK) {
|
|
143
|
+
return Array.from(secretK).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
144
|
+
}
|
|
145
|
+
function recoverSecretK(mnemonic) {
|
|
146
|
+
return mnemonicToEntropy(mnemonic.trim(), wordlist);
|
|
147
|
+
}
|
|
148
|
+
function skToField(secretK) {
|
|
149
|
+
const val = BigInt("0x" + toHexBytes(secretK));
|
|
150
|
+
return (val % FIELD_ORDER).toString();
|
|
151
|
+
}
|
|
152
|
+
function mod(value) {
|
|
153
|
+
const reduced = value % FIELD_ORDER;
|
|
154
|
+
return reduced < 0n ? reduced + FIELD_ORDER : reduced;
|
|
155
|
+
}
|
|
156
|
+
async function mimcConstants() {
|
|
157
|
+
if (!mimcConstantsPromise) {
|
|
158
|
+
const { buildMimcSponge } = await import("circomlibjs");
|
|
159
|
+
mimcConstantsPromise = buildMimcSponge().then((mimc) => mimc.cts.map((constant) => BigInt(mimc.F.toObject(constant).toString())));
|
|
160
|
+
}
|
|
161
|
+
return mimcConstantsPromise;
|
|
162
|
+
}
|
|
163
|
+
function pow5(value) {
|
|
164
|
+
const square = mod(value * value);
|
|
165
|
+
return mod(mod(square * square) * value);
|
|
166
|
+
}
|
|
167
|
+
function mimcFeistel(leftInput, rightInput, constants) {
|
|
168
|
+
let left = mod(leftInput);
|
|
169
|
+
let right = mod(rightInput);
|
|
170
|
+
for (let round = 0; round < MIMC_ROUNDS; round += 1) {
|
|
171
|
+
const constant = round === 0 || round === MIMC_ROUNDS - 1 ? 0n : constants[round];
|
|
172
|
+
const previousRight = right;
|
|
173
|
+
const powered = pow5(left + constant);
|
|
174
|
+
if (round < MIMC_ROUNDS - 1) {
|
|
175
|
+
right = left;
|
|
176
|
+
left = mod(previousRight + powered);
|
|
177
|
+
} else {
|
|
178
|
+
right = mod(previousRight + powered);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return { left, right };
|
|
182
|
+
}
|
|
183
|
+
async function mimcHash(inputs) {
|
|
184
|
+
if (inputs.length === 0)
|
|
185
|
+
throw new Error("MiMCSponge requires an input");
|
|
186
|
+
const constants = await mimcConstants();
|
|
187
|
+
let rate = 0n;
|
|
188
|
+
let capacity = 0n;
|
|
189
|
+
for (const input of inputs) {
|
|
190
|
+
rate = mod(rate + input);
|
|
191
|
+
const state = mimcFeistel(rate, capacity, constants);
|
|
192
|
+
rate = state.left;
|
|
193
|
+
capacity = state.right;
|
|
194
|
+
}
|
|
195
|
+
return rate.toString();
|
|
196
|
+
}
|
|
197
|
+
function normalizeField(value, label) {
|
|
198
|
+
let field;
|
|
199
|
+
try {
|
|
200
|
+
field = BigInt(value);
|
|
201
|
+
} catch {
|
|
202
|
+
throw new Error(`Invalid ${label}`);
|
|
203
|
+
}
|
|
204
|
+
if (field < 0n || field >= FIELD_ORDER)
|
|
205
|
+
throw new Error(`Invalid ${label}`);
|
|
206
|
+
return field.toString();
|
|
207
|
+
}
|
|
208
|
+
async function setSnapshotLeaf(layers, leafIndex, leaf) {
|
|
209
|
+
layers[0][leafIndex] = leaf;
|
|
210
|
+
let index = leafIndex;
|
|
211
|
+
for (let level = 0; level < MEMBERSHIP_TREE_DEPTH; level++) {
|
|
212
|
+
const left = BigInt(layers[level][index & ~1]);
|
|
213
|
+
const right = BigInt(layers[level][index & ~1 | 1]);
|
|
214
|
+
const parentIndex = Math.floor(index / 2);
|
|
215
|
+
layers[level + 1][parentIndex] = await mimcHash([left, right]);
|
|
216
|
+
index = parentIndex;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
async function normalizedSnapshotLayers(snapshot) {
|
|
220
|
+
if (snapshot.depth !== MEMBERSHIP_TREE_DEPTH) {
|
|
221
|
+
throw new Error(`Unsupported membership-tree depth: ${snapshot.depth}`);
|
|
222
|
+
}
|
|
223
|
+
if (!Array.isArray(snapshot.leaves) || snapshot.leaves.length !== MEMBERSHIP_TREE_CAPACITY) {
|
|
224
|
+
throw new Error(`Expected ${MEMBERSHIP_TREE_CAPACITY} membership leaves`);
|
|
225
|
+
}
|
|
226
|
+
const leaves = snapshot.leaves.map((leaf, index) => normalizeField(leaf, `membership leaf ${index}`));
|
|
227
|
+
if (!snapshot.layers) {
|
|
228
|
+
const layers2 = Array.from({ length: MEMBERSHIP_TREE_DEPTH + 1 }, (_, level) => Array(2 ** (MEMBERSHIP_TREE_DEPTH - level)).fill("0"));
|
|
229
|
+
for (let index = 0; index < leaves.length; index++) {
|
|
230
|
+
if (leaves[index] !== "0")
|
|
231
|
+
await setSnapshotLeaf(layers2, index, leaves[index]);
|
|
232
|
+
}
|
|
233
|
+
return layers2;
|
|
234
|
+
}
|
|
235
|
+
if (!Array.isArray(snapshot.layers) || snapshot.layers.length !== MEMBERSHIP_TREE_DEPTH + 1) {
|
|
236
|
+
throw new Error("Invalid membership-tree layers");
|
|
237
|
+
}
|
|
238
|
+
const layers = snapshot.layers.map((layer, level) => {
|
|
239
|
+
const expectedLength = 2 ** (MEMBERSHIP_TREE_DEPTH - level);
|
|
240
|
+
if (!Array.isArray(layer) || layer.length !== expectedLength) {
|
|
241
|
+
throw new Error(`Invalid membership-tree layer ${level}`);
|
|
242
|
+
}
|
|
243
|
+
return layer.map((node, index) => normalizeField(node, `membership node ${level}:${index}`));
|
|
244
|
+
});
|
|
245
|
+
if (layers[0].some((leaf, index) => leaf !== leaves[index])) {
|
|
246
|
+
throw new Error("Membership-tree leaves do not match its layers");
|
|
247
|
+
}
|
|
248
|
+
const zeroParent = await mimcHash([0n, 0n]);
|
|
249
|
+
for (let level = 0; level < MEMBERSHIP_TREE_DEPTH; level++) {
|
|
250
|
+
for (let index = 0; index < layers[level + 1].length; index++) {
|
|
251
|
+
const left = layers[level][index * 2];
|
|
252
|
+
const right = layers[level][index * 2 + 1];
|
|
253
|
+
const parent = layers[level + 1][index];
|
|
254
|
+
if (left === "0" && right === "0") {
|
|
255
|
+
if (parent !== "0" && parent !== zeroParent) {
|
|
256
|
+
throw new Error("Invalid empty membership-tree branch");
|
|
257
|
+
}
|
|
258
|
+
} else if (parent !== await mimcHash([BigInt(left), BigInt(right)])) {
|
|
259
|
+
throw new Error("Invalid membership-tree parent");
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return layers;
|
|
264
|
+
}
|
|
265
|
+
async function deriveMembershipWitness(commitment, snapshot) {
|
|
266
|
+
const normalizedCommitment = normalizeField(commitment, "membership commitment");
|
|
267
|
+
if (normalizedCommitment === "0")
|
|
268
|
+
throw new Error("Membership commitment is not active");
|
|
269
|
+
const layers = await normalizedSnapshotLayers(snapshot);
|
|
270
|
+
const root = normalizeField(snapshot.root, "membership root");
|
|
271
|
+
if (layers[MEMBERSHIP_TREE_DEPTH][0] !== root) {
|
|
272
|
+
throw new Error("Membership-tree root does not match its layers");
|
|
273
|
+
}
|
|
274
|
+
const matchingIndices = layers[0].map((leaf, index2) => leaf === normalizedCommitment ? index2 : -1).filter((index2) => index2 >= 0);
|
|
275
|
+
if (matchingIndices.length !== 1) {
|
|
276
|
+
throw new Error("Membership commitment must appear exactly once in the public snapshot");
|
|
277
|
+
}
|
|
278
|
+
const merklePathElements = [];
|
|
279
|
+
const merklePathIndices = [];
|
|
280
|
+
let node = normalizedCommitment;
|
|
281
|
+
let index = matchingIndices[0];
|
|
282
|
+
for (let level = 0; level < MEMBERSHIP_TREE_DEPTH; level++) {
|
|
283
|
+
const sibling = layers[level][index ^ 1];
|
|
284
|
+
const isRight = index & 1;
|
|
285
|
+
merklePathElements.push(sibling);
|
|
286
|
+
merklePathIndices.push(isRight.toString());
|
|
287
|
+
node = isRight ? await mimcHash([BigInt(sibling), BigInt(node)]) : await mimcHash([BigInt(node), BigInt(sibling)]);
|
|
288
|
+
index = Math.floor(index / 2);
|
|
289
|
+
}
|
|
290
|
+
if (node !== root)
|
|
291
|
+
throw new Error("Membership witness does not reproduce the snapshot root");
|
|
292
|
+
return {
|
|
293
|
+
root,
|
|
294
|
+
leafIndex: matchingIndices[0],
|
|
295
|
+
merklePathElements,
|
|
296
|
+
merklePathIndices
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
function normalizeForCanonicalJson(value) {
|
|
300
|
+
if (value === null || typeof value === "string" || typeof value === "boolean")
|
|
301
|
+
return value;
|
|
302
|
+
if (typeof value === "number")
|
|
303
|
+
return Number.isFinite(value) ? value : null;
|
|
304
|
+
if (Array.isArray(value))
|
|
305
|
+
return value.map((item) => normalizeForCanonicalJson(item));
|
|
306
|
+
if (value !== null && typeof value === "object") {
|
|
307
|
+
const object = value;
|
|
308
|
+
return Object.fromEntries(Object.keys(object).sort().map((key) => [key, normalizeForCanonicalJson(object[key])]));
|
|
309
|
+
}
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
312
|
+
function canonicalizeRequest(request) {
|
|
313
|
+
return JSON.stringify(normalizeForCanonicalJson(request));
|
|
314
|
+
}
|
|
315
|
+
async function requestDigestToField(request) {
|
|
316
|
+
const canonical = canonicalizeRequest(request);
|
|
317
|
+
const digestBytes = new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(canonical)));
|
|
318
|
+
const digest = Array.from(digestBytes).map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
319
|
+
return { canonical, digest, field: mod(BigInt(`0x${digest}`)).toString() };
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// ../zk-credits-shared/dist/proof.js
|
|
323
|
+
import { groth16 } from "snarkjs";
|
|
324
|
+
var ProofSelfVerificationError = class extends Error {
|
|
325
|
+
constructor(message) {
|
|
326
|
+
super(message);
|
|
327
|
+
this.name = "ProofSelfVerificationError";
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
async function proveGroth16(input, wasm, zkey) {
|
|
331
|
+
const { proof, publicSignals } = await groth16.fullProve(input, wasm, zkey);
|
|
332
|
+
return { proof, publicSignals };
|
|
333
|
+
}
|
|
334
|
+
async function verifyGroth16Proof(verificationKey, publicSignals, proof) {
|
|
335
|
+
return groth16.verify(verificationKey, publicSignals, proof);
|
|
336
|
+
}
|
|
337
|
+
async function generateRlnProofSelfVerified(input, resources) {
|
|
338
|
+
const result = await proveGroth16(input, resources.rlnWasm, resources.rlnZkey);
|
|
339
|
+
let valid;
|
|
340
|
+
try {
|
|
341
|
+
valid = await verifyGroth16Proof(resources.rlnVk, result.publicSignals, result.proof);
|
|
342
|
+
} catch (err) {
|
|
343
|
+
throw new ProofSelfVerificationError(`Local Groth16 verification failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
344
|
+
}
|
|
345
|
+
if (!valid) {
|
|
346
|
+
throw new ProofSelfVerificationError("Local Groth16 verification failed; proof not returned");
|
|
347
|
+
}
|
|
348
|
+
return { ...result, nullifier: result.publicSignals[1] };
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// dist/identity.js
|
|
352
|
+
var SERVICE = "zk-credits-sidecar";
|
|
353
|
+
var ACCOUNT = "secret-k-v1";
|
|
354
|
+
function secretToHex(secretK) {
|
|
355
|
+
if (secretK.length !== 32)
|
|
356
|
+
throw new Error("Expected a 32-byte ZK Credits secret");
|
|
357
|
+
return Array.from(secretK, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
358
|
+
}
|
|
359
|
+
function hexToSecret(value) {
|
|
360
|
+
if (!/^[a-f0-9]{64}$/i.test(value)) {
|
|
361
|
+
throw new Error("Stored ZK Credits credential is malformed; re-import the mnemonic");
|
|
362
|
+
}
|
|
363
|
+
return new Uint8Array(value.match(/.{2}/g).map((pair) => Number.parseInt(pair, 16)));
|
|
364
|
+
}
|
|
365
|
+
var IdentityStore = class {
|
|
366
|
+
credentials;
|
|
367
|
+
constructor(credentials = keytar) {
|
|
368
|
+
this.credentials = credentials;
|
|
369
|
+
}
|
|
370
|
+
async hasIdentity() {
|
|
371
|
+
return await this.credentials.getPassword(SERVICE, ACCOUNT) !== null;
|
|
372
|
+
}
|
|
373
|
+
async importMnemonic(mnemonic) {
|
|
374
|
+
const secretK = recoverSecretK(mnemonic);
|
|
375
|
+
if (secretK.length !== 32)
|
|
376
|
+
throw new Error("Recovery phrase must encode a 32-byte secret");
|
|
377
|
+
await this.credentials.setPassword(SERVICE, ACCOUNT, secretToHex(secretK));
|
|
378
|
+
return secretK;
|
|
379
|
+
}
|
|
380
|
+
async loadSecretK(options = {}) {
|
|
381
|
+
const stored = await this.credentials.getPassword(SERVICE, ACCOUNT);
|
|
382
|
+
if (stored !== null)
|
|
383
|
+
return hexToSecret(stored);
|
|
384
|
+
if (options.headlessMnemonic) {
|
|
385
|
+
const secretK = recoverSecretK(options.headlessMnemonic);
|
|
386
|
+
if (secretK.length !== 32)
|
|
387
|
+
throw new Error("Recovery phrase must encode a 32-byte secret");
|
|
388
|
+
return secretK;
|
|
389
|
+
}
|
|
390
|
+
throw new Error("No ZK Credits identity found; run zk-credits import-mnemonic first");
|
|
391
|
+
}
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
// dist/local-prover.js
|
|
395
|
+
import { readFile as readFile2 } from "node:fs/promises";
|
|
396
|
+
import { resolve } from "node:path";
|
|
397
|
+
|
|
398
|
+
// dist/artifact-manifest.js
|
|
399
|
+
import { createHash } from "node:crypto";
|
|
400
|
+
import { readFile } from "node:fs/promises";
|
|
401
|
+
import { join as join2 } from "node:path";
|
|
402
|
+
var CIRCUIT_MANIFEST = [
|
|
403
|
+
{
|
|
404
|
+
file: "rln_nullifier.wasm",
|
|
405
|
+
sha256: "38370baebbafb76562d5c94e15354c04152c388bffe1bf85faeeb4dd221538c1"
|
|
406
|
+
},
|
|
407
|
+
{
|
|
408
|
+
file: "rln_nullifier_final.zkey",
|
|
409
|
+
sha256: "11cd9e881437283701dcebd75be469d57ec7428de3495d229268eb75f2413cc8"
|
|
410
|
+
},
|
|
411
|
+
{
|
|
412
|
+
file: "verification_key_rln.json",
|
|
413
|
+
sha256: "9a521e0e19dc272c281f1a41abf79571bb0094cda653c890bdd95aa5b4c5f1ee"
|
|
414
|
+
}
|
|
415
|
+
];
|
|
416
|
+
async function verifyArtifactManifest(artifactDirectory, manifest = CIRCUIT_MANIFEST) {
|
|
417
|
+
for (const artifact of manifest) {
|
|
418
|
+
const content = await readFile(join2(artifactDirectory, artifact.file));
|
|
419
|
+
const digest = createHash("sha256").update(content).digest("hex");
|
|
420
|
+
if (digest !== artifact.sha256) {
|
|
421
|
+
throw new Error(`Circuit artifact hash mismatch: ${artifact.file}`);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
return manifest;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// dist/local-prover.js
|
|
428
|
+
async function createLocalProofGenerator(options) {
|
|
429
|
+
const artifactDirectory = options.artifactDirectory ?? resolve(import.meta.dirname, "..", "circuits");
|
|
430
|
+
await verifyArtifactManifest(artifactDirectory);
|
|
431
|
+
const verificationKey = JSON.parse(await readFile2(resolve(artifactDirectory, "verification_key_rln.json"), "utf8"));
|
|
432
|
+
const prove = options.prove ?? generateRlnProofSelfVerified;
|
|
433
|
+
return async ({ ticketIndex, request }) => {
|
|
434
|
+
const [requestDigest, witness] = await Promise.all([
|
|
435
|
+
requestDigestToField(request),
|
|
436
|
+
options.membershipClient.witnessForSecret(options.secretK)
|
|
437
|
+
]);
|
|
438
|
+
const result = await prove({
|
|
439
|
+
secret_k: skToField(options.secretK),
|
|
440
|
+
ticket_index: ticketIndex.toString(),
|
|
441
|
+
request_digest: requestDigest.field,
|
|
442
|
+
merkle_path_elements: witness.merklePathElements,
|
|
443
|
+
merkle_path_indices: witness.merklePathIndices
|
|
444
|
+
}, {
|
|
445
|
+
rlnWasm: resolve(artifactDirectory, "rln_nullifier.wasm"),
|
|
446
|
+
rlnZkey: resolve(artifactDirectory, "rln_nullifier_final.zkey"),
|
|
447
|
+
rlnVk: verificationKey
|
|
448
|
+
});
|
|
449
|
+
return { proof: result.proof, pubSignals: result.publicSignals };
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// dist/membership-client.js
|
|
454
|
+
var MAX_SNAPSHOT_AGE_MS = 6e4;
|
|
455
|
+
function parseSnapshot(value) {
|
|
456
|
+
if (!value || typeof value !== "object")
|
|
457
|
+
throw new Error("Membership snapshot is malformed");
|
|
458
|
+
const snapshot = value;
|
|
459
|
+
if (typeof snapshot.root !== "string" || typeof snapshot.depth !== "number" || !Array.isArray(snapshot.leaves) || !snapshot.leaves.every((leaf) => typeof leaf === "string") || !Array.isArray(snapshot.layers) || !snapshot.layers.every((layer) => Array.isArray(layer) && layer.every((node) => typeof node === "string")) || typeof snapshot.generatedAt !== "string") {
|
|
460
|
+
throw new Error("Membership snapshot is malformed");
|
|
461
|
+
}
|
|
462
|
+
const generatedAt = Date.parse(snapshot.generatedAt);
|
|
463
|
+
if (!Number.isFinite(generatedAt) || Date.now() - generatedAt > MAX_SNAPSHOT_AGE_MS || generatedAt - Date.now() > 5e3) {
|
|
464
|
+
throw new Error("Membership snapshot is stale");
|
|
465
|
+
}
|
|
466
|
+
return {
|
|
467
|
+
root: snapshot.root,
|
|
468
|
+
depth: snapshot.depth,
|
|
469
|
+
leaves: snapshot.leaves,
|
|
470
|
+
layers: snapshot.layers,
|
|
471
|
+
generatedAt: snapshot.generatedAt
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
var MembershipClient = class {
|
|
475
|
+
fetcher;
|
|
476
|
+
baseUrl;
|
|
477
|
+
constructor(gatewayBaseUrl, fetcher = fetch) {
|
|
478
|
+
this.fetcher = fetcher;
|
|
479
|
+
this.baseUrl = gatewayBaseUrl.replace(/\/$/, "");
|
|
480
|
+
}
|
|
481
|
+
async witnessForSecret(secretK) {
|
|
482
|
+
const [chainRoot, snapshot] = await Promise.all([
|
|
483
|
+
this.getCurrentChainRoot(),
|
|
484
|
+
this.getSnapshot()
|
|
485
|
+
]);
|
|
486
|
+
if (snapshot.root !== chainRoot) {
|
|
487
|
+
throw new Error("Membership snapshot does not match the current chain root");
|
|
488
|
+
}
|
|
489
|
+
const commitment = await mimcHash([BigInt(skToField(secretK))]);
|
|
490
|
+
return deriveMembershipWitness(commitment, snapshot);
|
|
491
|
+
}
|
|
492
|
+
async getCurrentChainRoot() {
|
|
493
|
+
const response = await this.fetcher(`${this.baseUrl}/v1/contract-status`, { cache: "no-store" });
|
|
494
|
+
if (!response.ok)
|
|
495
|
+
throw new Error("Current chain root could not be loaded");
|
|
496
|
+
const payload = await response.json();
|
|
497
|
+
if (!payload || typeof payload !== "object" || typeof payload.currentRoot !== "string") {
|
|
498
|
+
throw new Error("Current chain root is unavailable");
|
|
499
|
+
}
|
|
500
|
+
return payload.currentRoot;
|
|
501
|
+
}
|
|
502
|
+
async getSnapshot() {
|
|
503
|
+
const response = await this.fetcher(`${this.baseUrl}/v1/membership-tree`, { cache: "no-store" });
|
|
504
|
+
if (!response.ok)
|
|
505
|
+
throw new Error("Membership snapshot could not be loaded");
|
|
506
|
+
return parseSnapshot(await response.json());
|
|
507
|
+
}
|
|
508
|
+
};
|
|
509
|
+
|
|
510
|
+
// dist/sidecar-config.js
|
|
511
|
+
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
512
|
+
import { join as join3 } from "node:path";
|
|
513
|
+
function createLoopbackToken() {
|
|
514
|
+
return randomBytes2(32).toString("base64url");
|
|
515
|
+
}
|
|
516
|
+
function sidecarStatePaths(stateDirectory2) {
|
|
517
|
+
return {
|
|
518
|
+
ledgerPath: join3(stateDirectory2, "tickets.json"),
|
|
519
|
+
tokenPath: join3(stateDirectory2, "loopback-token"),
|
|
520
|
+
logPath: join3(stateDirectory2, "sidecar.log")
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
function formatOpenAiEnvironment(loopbackBaseUrl2, localToken) {
|
|
524
|
+
const url = new URL(loopbackBaseUrl2);
|
|
525
|
+
if (url.protocol !== "http:" || url.hostname !== "127.0.0.1" || !/^[A-Za-z0-9_-]+$/.test(localToken)) {
|
|
526
|
+
throw new Error("Sidecar environment must use a valid 127.0.0.1 URL and local token");
|
|
527
|
+
}
|
|
528
|
+
return [
|
|
529
|
+
`export OPENAI_BASE_URL=${url.origin}/v1`,
|
|
530
|
+
`export OPENAI_API_KEY=${localToken}`
|
|
531
|
+
].join("\n");
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// dist/cli-runtime.js
|
|
535
|
+
function setupModel(args) {
|
|
536
|
+
if (args[1] !== "codex")
|
|
537
|
+
throw new Error("Usage: zk-credits setup codex [--model <model>]");
|
|
538
|
+
if (args.length === 2)
|
|
539
|
+
return void 0;
|
|
540
|
+
if (args.length === 4 && args[2] === "--model" && args[3]?.trim())
|
|
541
|
+
return args[3];
|
|
542
|
+
throw new Error("Usage: zk-credits setup codex [--model <model>]");
|
|
543
|
+
}
|
|
544
|
+
async function runCliCommand(args, dependencies) {
|
|
545
|
+
switch (args[0]) {
|
|
546
|
+
case "env": {
|
|
547
|
+
dependencies.write(formatOpenAiEnvironment(dependencies.loopbackBaseUrl, await dependencies.readToken()));
|
|
548
|
+
return 0;
|
|
549
|
+
}
|
|
550
|
+
case "import-mnemonic": {
|
|
551
|
+
await dependencies.importMnemonic(await dependencies.readMnemonic());
|
|
552
|
+
dependencies.write("ZK Credits identity imported into the system credential store.");
|
|
553
|
+
return 0;
|
|
554
|
+
}
|
|
555
|
+
case "setup": {
|
|
556
|
+
const model = setupModel(args);
|
|
557
|
+
if (!await dependencies.isIdentityConfigured()) {
|
|
558
|
+
await dependencies.importMnemonic(await dependencies.readMnemonic());
|
|
559
|
+
dependencies.write("ZK Credits identity imported into the system credential store.");
|
|
560
|
+
}
|
|
561
|
+
await dependencies.configureCodex(model);
|
|
562
|
+
await dependencies.ensureSidecar();
|
|
563
|
+
dependencies.write("ZK Credits is ready for Codex.");
|
|
564
|
+
dependencies.write("Run: zk-credits codex");
|
|
565
|
+
return 0;
|
|
566
|
+
}
|
|
567
|
+
case "token": {
|
|
568
|
+
dependencies.write(await dependencies.ensureSidecar());
|
|
569
|
+
return 0;
|
|
570
|
+
}
|
|
571
|
+
case "status": {
|
|
572
|
+
dependencies.write(`Identity: ${await dependencies.isIdentityConfigured() ? "configured" : "missing"}`);
|
|
573
|
+
dependencies.write(`Codex profile: ${await dependencies.isCodexProfileInstalled() ? "installed" : "missing"}`);
|
|
574
|
+
dependencies.write(`Sidecar: ${await dependencies.isSidecarHealthy() ? "running" : "stopped"}`);
|
|
575
|
+
return 0;
|
|
576
|
+
}
|
|
577
|
+
case "codex": {
|
|
578
|
+
if (!await dependencies.isCodexProfileInstalled()) {
|
|
579
|
+
throw new Error("Run zk-credits setup codex first");
|
|
580
|
+
}
|
|
581
|
+
await dependencies.ensureSidecar();
|
|
582
|
+
return dependencies.launchCodex(args.slice(1));
|
|
583
|
+
}
|
|
584
|
+
default:
|
|
585
|
+
throw new Error("Usage: zk-credits <setup codex|codex|status|token|import-mnemonic|serve|env>");
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// dist/node-sidecar-lifecycle.js
|
|
590
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
591
|
+
import { chmod as chmod3, mkdir as mkdir3, open } from "node:fs/promises";
|
|
592
|
+
import { dirname as dirname2 } from "node:path";
|
|
593
|
+
|
|
594
|
+
// dist/sidecar-state.js
|
|
595
|
+
import { chmod as chmod2, mkdir as mkdir2, readFile as readFile3, rename as rename2, writeFile as writeFile2 } from "node:fs/promises";
|
|
596
|
+
import { dirname } from "node:path";
|
|
597
|
+
async function writeLoopbackToken(tokenPath, token) {
|
|
598
|
+
if (!token)
|
|
599
|
+
throw new Error("Loopback token is required");
|
|
600
|
+
await mkdir2(dirname(tokenPath), { recursive: true, mode: 448 });
|
|
601
|
+
const temporaryPath = `${tokenPath}.tmp`;
|
|
602
|
+
await writeFile2(temporaryPath, `${token}
|
|
603
|
+
`, { encoding: "utf8", mode: 384 });
|
|
604
|
+
await chmod2(temporaryPath, 384);
|
|
605
|
+
await rename2(temporaryPath, tokenPath);
|
|
606
|
+
await chmod2(tokenPath, 384);
|
|
607
|
+
}
|
|
608
|
+
async function readLoopbackToken(tokenPath) {
|
|
609
|
+
const token = (await readFile3(tokenPath, "utf8")).trim();
|
|
610
|
+
if (!token)
|
|
611
|
+
throw new Error("No active ZK Credits sidecar token; start zk-credits serve first");
|
|
612
|
+
return token;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// dist/node-sidecar-lifecycle.js
|
|
616
|
+
async function startDetachedProcess(specification) {
|
|
617
|
+
await mkdir3(dirname2(specification.logPath), { recursive: true, mode: 448 });
|
|
618
|
+
const log = await open(specification.logPath, "a", 384);
|
|
619
|
+
await chmod3(specification.logPath, 384);
|
|
620
|
+
try {
|
|
621
|
+
const child = spawn2(specification.executable, specification.args, {
|
|
622
|
+
detached: true,
|
|
623
|
+
env: specification.env,
|
|
624
|
+
stdio: ["ignore", log.fd, log.fd]
|
|
625
|
+
});
|
|
626
|
+
await new Promise((resolve2, reject) => {
|
|
627
|
+
child.once("spawn", resolve2);
|
|
628
|
+
child.once("error", reject);
|
|
629
|
+
});
|
|
630
|
+
child.unref();
|
|
631
|
+
} finally {
|
|
632
|
+
await log.close();
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
var defaultRuntime = {
|
|
636
|
+
fetchHealth: async (url) => fetch(url, { signal: AbortSignal.timeout(500) }),
|
|
637
|
+
startDetachedProcess,
|
|
638
|
+
wait: async (milliseconds) => new Promise((resolve2) => setTimeout(resolve2, milliseconds))
|
|
639
|
+
};
|
|
640
|
+
function createNodeSidecarLifecycle(options, runtime = defaultRuntime) {
|
|
641
|
+
const url = new URL(options.loopbackBaseUrl);
|
|
642
|
+
if (url.protocol !== "http:" || url.hostname !== "127.0.0.1" || !url.port) {
|
|
643
|
+
throw new Error("Sidecar lifecycle requires a 127.0.0.1 HTTP URL with an explicit port");
|
|
644
|
+
}
|
|
645
|
+
return {
|
|
646
|
+
logPath: options.logPath,
|
|
647
|
+
async isHealthy() {
|
|
648
|
+
try {
|
|
649
|
+
const response = await runtime.fetchHealth(`${url.origin}/health`);
|
|
650
|
+
if (!response.ok)
|
|
651
|
+
return false;
|
|
652
|
+
const body = await response.json();
|
|
653
|
+
return body.service === "zk-credits-sidecar" && body.status === "ok";
|
|
654
|
+
} catch {
|
|
655
|
+
return false;
|
|
656
|
+
}
|
|
657
|
+
},
|
|
658
|
+
async startDetached() {
|
|
659
|
+
await runtime.startDetachedProcess({
|
|
660
|
+
executable: process.execPath,
|
|
661
|
+
args: [options.cliEntryPath, "serve", "--port", url.port],
|
|
662
|
+
env: {
|
|
663
|
+
...process.env,
|
|
664
|
+
ZK_CREDITS_HOME: options.stateDirectory,
|
|
665
|
+
ZK_CREDITS_SIDECAR_PORT: url.port
|
|
666
|
+
},
|
|
667
|
+
logPath: options.logPath
|
|
668
|
+
});
|
|
669
|
+
},
|
|
670
|
+
readToken: () => readLoopbackToken(options.tokenPath),
|
|
671
|
+
wait: runtime.wait
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// dist/server-startup.js
|
|
676
|
+
async function activateSidecarServer(activation) {
|
|
677
|
+
const address = await activation.listen();
|
|
678
|
+
await activation.publishToken();
|
|
679
|
+
return address;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// dist/sidecar-lifecycle.js
|
|
683
|
+
async function ensureSidecarReady(dependencies, options = {}) {
|
|
684
|
+
if (await dependencies.isHealthy())
|
|
685
|
+
return dependencies.readToken();
|
|
686
|
+
await dependencies.startDetached();
|
|
687
|
+
const attempts = options.attempts ?? 50;
|
|
688
|
+
const intervalMs = options.intervalMs ?? 100;
|
|
689
|
+
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
690
|
+
await dependencies.wait(intervalMs);
|
|
691
|
+
if (await dependencies.isHealthy())
|
|
692
|
+
return dependencies.readToken();
|
|
693
|
+
}
|
|
694
|
+
throw new Error(`ZK Credits sidecar did not become ready; inspect ${dependencies.logPath}`);
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
// dist/sidecar.js
|
|
698
|
+
import { createServer } from "node:http";
|
|
699
|
+
var MAX_REQUEST_BYTES = 2e6;
|
|
700
|
+
function isLoopbackAddress(address) {
|
|
701
|
+
return address === "127.0.0.1" || address === "::1" || address === "::ffff:127.0.0.1";
|
|
702
|
+
}
|
|
703
|
+
function sendJson(res, status, body) {
|
|
704
|
+
res.writeHead(status, { "Content-Type": "application/json; charset=utf-8" });
|
|
705
|
+
res.end(JSON.stringify(body));
|
|
706
|
+
}
|
|
707
|
+
async function readJsonBody(req) {
|
|
708
|
+
const chunks = [];
|
|
709
|
+
let total = 0;
|
|
710
|
+
for await (const chunk of req) {
|
|
711
|
+
const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
712
|
+
total += bytes.length;
|
|
713
|
+
if (total > MAX_REQUEST_BYTES)
|
|
714
|
+
throw new Error("OpenAI request exceeds the loopback size limit");
|
|
715
|
+
chunks.push(bytes);
|
|
716
|
+
}
|
|
717
|
+
const raw = Buffer.concat(chunks).toString("utf8");
|
|
718
|
+
if (!raw)
|
|
719
|
+
throw new Error("OpenAI request body is required");
|
|
720
|
+
try {
|
|
721
|
+
return { raw, parsed: JSON.parse(raw) };
|
|
722
|
+
} catch {
|
|
723
|
+
throw new Error("OpenAI request body must be valid JSON");
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
async function relayGatewayResponse(res, upstream) {
|
|
727
|
+
const contentType = upstream.headers.get("content-type") || "application/json; charset=utf-8";
|
|
728
|
+
res.writeHead(upstream.status, { "Content-Type": contentType });
|
|
729
|
+
if (!upstream.body) {
|
|
730
|
+
res.end();
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
const reader = upstream.body.getReader();
|
|
734
|
+
try {
|
|
735
|
+
while (true) {
|
|
736
|
+
const { done, value } = await reader.read();
|
|
737
|
+
if (done)
|
|
738
|
+
break;
|
|
739
|
+
res.write(Buffer.from(value));
|
|
740
|
+
}
|
|
741
|
+
res.end();
|
|
742
|
+
} finally {
|
|
743
|
+
reader.releaseLock();
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
function proofHeader(proof) {
|
|
747
|
+
return Buffer.from(JSON.stringify(proof)).toString("base64");
|
|
748
|
+
}
|
|
749
|
+
function createSidecarServer(options) {
|
|
750
|
+
const gatewayBaseUrl = options.gatewayBaseUrl.replace(/\/$/, "");
|
|
751
|
+
const gatewayFetch = options.gatewayFetch ?? fetch;
|
|
752
|
+
let server = null;
|
|
753
|
+
const inFlight = /* @__PURE__ */ new Set();
|
|
754
|
+
const handler = async (req, res) => {
|
|
755
|
+
const pathname = new URL(req.url || "/", "http://127.0.0.1").pathname;
|
|
756
|
+
if (!isLoopbackAddress(req.socket.remoteAddress)) {
|
|
757
|
+
sendJson(res, 403, { error: "loopback_only" });
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
if (req.method === "GET" && pathname === "/health") {
|
|
761
|
+
sendJson(res, 200, { service: "zk-credits-sidecar", status: "ok" });
|
|
762
|
+
return;
|
|
763
|
+
}
|
|
764
|
+
if (req.method === "GET" && pathname === "/v1/models") {
|
|
765
|
+
if (req.headers.authorization !== `Bearer ${options.localToken}`) {
|
|
766
|
+
sendJson(res, 401, { error: "invalid_local_token" });
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
769
|
+
sendJson(res, 200, codexModelsResponse());
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
if (req.method !== "POST" || pathname !== "/v1/chat/completions" && pathname !== "/v1/responses") {
|
|
773
|
+
sendJson(res, 404, { error: "unsupported_openai_path" });
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
776
|
+
if (req.headers.authorization !== `Bearer ${options.localToken}`) {
|
|
777
|
+
sendJson(res, 401, { error: "invalid_local_token" });
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
780
|
+
try {
|
|
781
|
+
const body = await readJsonBody(req);
|
|
782
|
+
const reservation = await options.ledger.reserve(body.parsed);
|
|
783
|
+
const proof = await options.proofGenerator({
|
|
784
|
+
ticketIndex: reservation.index,
|
|
785
|
+
request: body.parsed
|
|
786
|
+
});
|
|
787
|
+
const upstream = await gatewayFetch(`${gatewayBaseUrl}${pathname}`, {
|
|
788
|
+
method: "POST",
|
|
789
|
+
headers: {
|
|
790
|
+
Authorization: `Bearer ${options.compatibilityKey}`,
|
|
791
|
+
"Content-Type": "application/json",
|
|
792
|
+
"X-ZK-Proof": proofHeader(proof)
|
|
793
|
+
},
|
|
794
|
+
body: body.raw
|
|
795
|
+
});
|
|
796
|
+
await relayGatewayResponse(res, upstream);
|
|
797
|
+
if (upstream.status >= 200 && upstream.status < 300) {
|
|
798
|
+
await options.ledger.consume(reservation.requestDigest);
|
|
799
|
+
}
|
|
800
|
+
} catch (error) {
|
|
801
|
+
if (!res.headersSent) {
|
|
802
|
+
const message = error instanceof Error ? error.message : "Loopback request failed";
|
|
803
|
+
sendJson(res, 502, { error: "sidecar_request_failed", message });
|
|
804
|
+
} else if (!res.writableEnded) {
|
|
805
|
+
res.end();
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
};
|
|
809
|
+
return {
|
|
810
|
+
async listen(port = 0) {
|
|
811
|
+
if (server)
|
|
812
|
+
throw new Error("ZK Credits sidecar is already running");
|
|
813
|
+
server = createServer((req, res) => {
|
|
814
|
+
const request = handler(req, res).finally(() => inFlight.delete(request));
|
|
815
|
+
inFlight.add(request);
|
|
816
|
+
});
|
|
817
|
+
await new Promise((resolve2, reject) => {
|
|
818
|
+
server.once("error", reject);
|
|
819
|
+
server.listen(port, "127.0.0.1", () => {
|
|
820
|
+
server.off("error", reject);
|
|
821
|
+
resolve2();
|
|
822
|
+
});
|
|
823
|
+
});
|
|
824
|
+
const address = server.address();
|
|
825
|
+
return `http://127.0.0.1:${address.port}`;
|
|
826
|
+
},
|
|
827
|
+
async close() {
|
|
828
|
+
if (!server)
|
|
829
|
+
return;
|
|
830
|
+
const activeServer = server;
|
|
831
|
+
server = null;
|
|
832
|
+
await new Promise((resolve2, reject) => activeServer.close((error) => {
|
|
833
|
+
if (error)
|
|
834
|
+
reject(error);
|
|
835
|
+
else
|
|
836
|
+
resolve2();
|
|
837
|
+
}));
|
|
838
|
+
await Promise.all(inFlight);
|
|
839
|
+
}
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
// dist/ticket-ledger.js
|
|
844
|
+
import { mkdir as mkdir4, readFile as readFile4, rename as rename3, writeFile as writeFile3 } from "node:fs/promises";
|
|
845
|
+
import { dirname as dirname3 } from "node:path";
|
|
846
|
+
function parseLedger(raw) {
|
|
847
|
+
const parsed = JSON.parse(raw);
|
|
848
|
+
if (!parsed || typeof parsed !== "object" || !Array.isArray(parsed.entries)) {
|
|
849
|
+
throw new Error("Sidecar ticket ledger is malformed");
|
|
850
|
+
}
|
|
851
|
+
const entries = parsed.entries.map((entry) => {
|
|
852
|
+
if (!entry || typeof entry !== "object" || !Number.isInteger(entry.index) || typeof entry.requestDigest !== "string" || !["reserved", "consumed"].includes(entry.state)) {
|
|
853
|
+
throw new Error("Sidecar ticket ledger is malformed");
|
|
854
|
+
}
|
|
855
|
+
return entry;
|
|
856
|
+
});
|
|
857
|
+
return { entries };
|
|
858
|
+
}
|
|
859
|
+
var TicketLedger = class {
|
|
860
|
+
filePath;
|
|
861
|
+
capacity;
|
|
862
|
+
queue = Promise.resolve();
|
|
863
|
+
constructor(filePath, capacity = 100) {
|
|
864
|
+
this.filePath = filePath;
|
|
865
|
+
this.capacity = capacity;
|
|
866
|
+
}
|
|
867
|
+
async reserve(request) {
|
|
868
|
+
const digest = (await requestDigestToField(request)).digest;
|
|
869
|
+
return this.serialized(async () => {
|
|
870
|
+
const ledger = await this.read();
|
|
871
|
+
const existing = ledger.entries.find((entry) => entry.requestDigest === digest);
|
|
872
|
+
if (existing)
|
|
873
|
+
return { ...existing };
|
|
874
|
+
const used = new Set(ledger.entries.map((entry) => entry.index));
|
|
875
|
+
const index = Array.from({ length: this.capacity }, (_, candidate) => candidate).find((candidate) => !used.has(candidate));
|
|
876
|
+
if (index === void 0)
|
|
877
|
+
throw new Error("All ZK Credit tickets have been reserved");
|
|
878
|
+
const reservation = { index, requestDigest: digest, state: "reserved" };
|
|
879
|
+
ledger.entries.push(reservation);
|
|
880
|
+
await this.write(ledger);
|
|
881
|
+
return reservation;
|
|
882
|
+
});
|
|
883
|
+
}
|
|
884
|
+
async consume(requestDigest) {
|
|
885
|
+
await this.serialized(async () => {
|
|
886
|
+
const ledger = await this.read();
|
|
887
|
+
const entry = ledger.entries.find((candidate) => candidate.requestDigest === requestDigest);
|
|
888
|
+
if (!entry)
|
|
889
|
+
throw new Error("Unknown sidecar ticket reservation");
|
|
890
|
+
entry.state = "consumed";
|
|
891
|
+
await this.write(ledger);
|
|
892
|
+
});
|
|
893
|
+
}
|
|
894
|
+
async serialized(operation) {
|
|
895
|
+
const previous = this.queue;
|
|
896
|
+
let release;
|
|
897
|
+
this.queue = new Promise((resolve2) => {
|
|
898
|
+
release = resolve2;
|
|
899
|
+
});
|
|
900
|
+
await previous;
|
|
901
|
+
try {
|
|
902
|
+
return await operation();
|
|
903
|
+
} finally {
|
|
904
|
+
release();
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
async read() {
|
|
908
|
+
try {
|
|
909
|
+
return parseLedger(await readFile4(this.filePath, "utf8"));
|
|
910
|
+
} catch (error) {
|
|
911
|
+
if (error.code === "ENOENT")
|
|
912
|
+
return { entries: [] };
|
|
913
|
+
throw error;
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
async write(ledger) {
|
|
917
|
+
await mkdir4(dirname3(this.filePath), { recursive: true, mode: 448 });
|
|
918
|
+
const temporaryPath = `${this.filePath}.tmp`;
|
|
919
|
+
await writeFile3(temporaryPath, JSON.stringify(ledger), { encoding: "utf8", mode: 384 });
|
|
920
|
+
await rename3(temporaryPath, this.filePath);
|
|
921
|
+
}
|
|
922
|
+
};
|
|
923
|
+
|
|
924
|
+
// dist/cli.js
|
|
925
|
+
var DEFAULT_GATEWAY_URL = "https://zk-credits-gateway.onrender.com";
|
|
926
|
+
var DEFAULT_PORT = 3210;
|
|
927
|
+
function stateDirectory() {
|
|
928
|
+
return process.env.ZK_CREDITS_HOME || join4(homedir(), ".zk-credits");
|
|
929
|
+
}
|
|
930
|
+
function loopbackBaseUrl() {
|
|
931
|
+
const port = Number(process.env.ZK_CREDITS_SIDECAR_PORT || DEFAULT_PORT);
|
|
932
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
933
|
+
throw new Error("ZK_CREDITS_SIDECAR_PORT must be a port between 1 and 65535");
|
|
934
|
+
}
|
|
935
|
+
return `http://127.0.0.1:${port}`;
|
|
936
|
+
}
|
|
937
|
+
function readPort(args) {
|
|
938
|
+
const portIndex = args.indexOf("--port");
|
|
939
|
+
if (portIndex === -1)
|
|
940
|
+
return Number(new URL(loopbackBaseUrl()).port);
|
|
941
|
+
const parsed = Number(args[portIndex + 1]);
|
|
942
|
+
if (!Number.isInteger(parsed) || parsed < 1 || parsed > 65535) {
|
|
943
|
+
throw new Error("--port must be a port between 1 and 65535");
|
|
944
|
+
}
|
|
945
|
+
return parsed;
|
|
946
|
+
}
|
|
947
|
+
async function readHiddenMnemonic() {
|
|
948
|
+
if (!process.stdin.isTTY || !process.stdin.setRawMode) {
|
|
949
|
+
throw new Error("import-mnemonic requires an interactive terminal");
|
|
950
|
+
}
|
|
951
|
+
process.stdout.write("Enter your 24-word recovery phrase: ");
|
|
952
|
+
process.stdin.setRawMode(true);
|
|
953
|
+
process.stdin.resume();
|
|
954
|
+
return new Promise((resolve2, reject) => {
|
|
955
|
+
let value = "";
|
|
956
|
+
const done = (error) => {
|
|
957
|
+
process.stdin.off("data", onData);
|
|
958
|
+
process.stdin.setRawMode(false);
|
|
959
|
+
process.stdout.write("\n");
|
|
960
|
+
if (error)
|
|
961
|
+
reject(error);
|
|
962
|
+
else
|
|
963
|
+
resolve2(value.trim());
|
|
964
|
+
};
|
|
965
|
+
const onData = (chunk) => {
|
|
966
|
+
for (const byte of chunk) {
|
|
967
|
+
if (byte === 3) {
|
|
968
|
+
done(new Error("Mnemonic import cancelled"));
|
|
969
|
+
return;
|
|
970
|
+
}
|
|
971
|
+
if (byte === 13 || byte === 10) {
|
|
972
|
+
done();
|
|
973
|
+
return;
|
|
974
|
+
}
|
|
975
|
+
if (byte === 127 || byte === 8) {
|
|
976
|
+
value = value.slice(0, -1);
|
|
977
|
+
continue;
|
|
978
|
+
}
|
|
979
|
+
value += String.fromCharCode(byte);
|
|
980
|
+
}
|
|
981
|
+
};
|
|
982
|
+
process.stdin.on("data", onData);
|
|
983
|
+
});
|
|
984
|
+
}
|
|
985
|
+
function printHelp() {
|
|
986
|
+
console.log(`Usage:
|
|
987
|
+
zk-credits setup codex [--model <model>]
|
|
988
|
+
zk-credits codex [codex arguments...]
|
|
989
|
+
zk-credits status
|
|
990
|
+
zk-credits import-mnemonic
|
|
991
|
+
zk-credits serve [--port <port>]
|
|
992
|
+
eval "$(zk-credits env)"
|
|
993
|
+
|
|
994
|
+
The Codex setup starts the loopback sidecar automatically; daily use is
|
|
995
|
+
"zk-credits codex". serve/env remain available for other OpenAI-compatible
|
|
996
|
+
clients. Set ZK_CREDITS_MNEMONIC only for a headless process; it is not
|
|
997
|
+
persisted by that path.`);
|
|
998
|
+
}
|
|
999
|
+
async function serve(args) {
|
|
1000
|
+
const port = readPort(args);
|
|
1001
|
+
const statePaths = sidecarStatePaths(stateDirectory());
|
|
1002
|
+
const localToken = createLoopbackToken();
|
|
1003
|
+
const identities = new IdentityStore();
|
|
1004
|
+
const secretK = await identities.loadSecretK({ headlessMnemonic: process.env.ZK_CREDITS_MNEMONIC });
|
|
1005
|
+
const gatewayBaseUrl = process.env.ZK_CREDITS_GATEWAY_URL || DEFAULT_GATEWAY_URL;
|
|
1006
|
+
const proofGenerator = await createLocalProofGenerator({
|
|
1007
|
+
secretK,
|
|
1008
|
+
membershipClient: new MembershipClient(gatewayBaseUrl)
|
|
1009
|
+
});
|
|
1010
|
+
const sidecar = createSidecarServer({
|
|
1011
|
+
localToken,
|
|
1012
|
+
gatewayBaseUrl,
|
|
1013
|
+
compatibilityKey: process.env.ZK_CREDITS_COMPATIBILITY_KEY || "sk-zk-local-demo",
|
|
1014
|
+
ledger: new TicketLedger(statePaths.ledgerPath),
|
|
1015
|
+
proofGenerator
|
|
1016
|
+
});
|
|
1017
|
+
let address;
|
|
1018
|
+
try {
|
|
1019
|
+
address = await activateSidecarServer({
|
|
1020
|
+
listen: () => sidecar.listen(port),
|
|
1021
|
+
publishToken: () => writeLoopbackToken(statePaths.tokenPath, localToken)
|
|
1022
|
+
});
|
|
1023
|
+
} catch (error) {
|
|
1024
|
+
await sidecar.close();
|
|
1025
|
+
throw error;
|
|
1026
|
+
}
|
|
1027
|
+
console.log(`ZK Credits sidecar listening on ${address}/v1`);
|
|
1028
|
+
console.log('Run eval "$(zk-credits env)" in the client shell.');
|
|
1029
|
+
const shutdown = async () => {
|
|
1030
|
+
await sidecar.close();
|
|
1031
|
+
process.exit(0);
|
|
1032
|
+
};
|
|
1033
|
+
process.once("SIGINT", () => {
|
|
1034
|
+
void shutdown();
|
|
1035
|
+
});
|
|
1036
|
+
process.once("SIGTERM", () => {
|
|
1037
|
+
void shutdown();
|
|
1038
|
+
});
|
|
1039
|
+
}
|
|
1040
|
+
async function main() {
|
|
1041
|
+
const args = process.argv.slice(2);
|
|
1042
|
+
if (args[0] === "--help" || args[0] === "-h" || !args[0]) {
|
|
1043
|
+
printHelp();
|
|
1044
|
+
return;
|
|
1045
|
+
}
|
|
1046
|
+
if (args[0] === "serve") {
|
|
1047
|
+
await serve(args.slice(1));
|
|
1048
|
+
return;
|
|
1049
|
+
}
|
|
1050
|
+
const identities = new IdentityStore();
|
|
1051
|
+
const sidecarHome = stateDirectory();
|
|
1052
|
+
const statePaths = sidecarStatePaths(sidecarHome);
|
|
1053
|
+
const codexHome = resolveCodexHome(process.env, homedir());
|
|
1054
|
+
const cliEntryPath = process.argv[1];
|
|
1055
|
+
if (!cliEntryPath)
|
|
1056
|
+
throw new Error("Unable to resolve the zk-credits executable path");
|
|
1057
|
+
const lifecycle = createNodeSidecarLifecycle({
|
|
1058
|
+
loopbackBaseUrl: loopbackBaseUrl(),
|
|
1059
|
+
stateDirectory: sidecarHome,
|
|
1060
|
+
tokenPath: statePaths.tokenPath,
|
|
1061
|
+
logPath: statePaths.logPath,
|
|
1062
|
+
cliEntryPath
|
|
1063
|
+
});
|
|
1064
|
+
const exitCode = await runCliCommand(args, {
|
|
1065
|
+
loopbackBaseUrl: loopbackBaseUrl(),
|
|
1066
|
+
readToken: () => readLoopbackToken(statePaths.tokenPath),
|
|
1067
|
+
importMnemonic: async (mnemonic) => {
|
|
1068
|
+
await identities.importMnemonic(mnemonic);
|
|
1069
|
+
},
|
|
1070
|
+
readMnemonic: readHiddenMnemonic,
|
|
1071
|
+
write: (line) => console.log(line),
|
|
1072
|
+
isIdentityConfigured: () => identities.hasIdentity(),
|
|
1073
|
+
configureCodex: async (model) => {
|
|
1074
|
+
await writeCodexProfile({ codexHome, loopbackBaseUrl: loopbackBaseUrl(), model });
|
|
1075
|
+
},
|
|
1076
|
+
ensureSidecar: () => ensureSidecarReady(lifecycle),
|
|
1077
|
+
isCodexProfileInstalled: () => isCodexProfileInstalled(codexHome),
|
|
1078
|
+
isSidecarHealthy: () => lifecycle.isHealthy(),
|
|
1079
|
+
launchCodex: (codexArgs) => launchCodexProcess(codexArgs)
|
|
1080
|
+
});
|
|
1081
|
+
if (exitCode !== 0)
|
|
1082
|
+
process.exitCode = exitCode;
|
|
1083
|
+
}
|
|
1084
|
+
void main().catch((error) => {
|
|
1085
|
+
console.error(error instanceof Error ? error.message : "zk-credits failed");
|
|
1086
|
+
process.exitCode = 1;
|
|
1087
|
+
});
|