@tokenzero/cli 0.1.7 → 0.1.8

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.
Files changed (2) hide show
  1. package/bin/tokenzero.mjs +54 -3
  2. package/package.json +1 -1
package/bin/tokenzero.mjs CHANGED
@@ -710,7 +710,7 @@ function canonical(obj) {
710
710
  }
711
711
 
712
712
  // cli/src/main.ts
713
- var VERSION = "0.1.7";
713
+ var VERSION = "0.1.8";
714
714
  var BAKED_URL = true ? "https://tokenzero.sh" : "__TOKENZERO_DEFAULT_URL__";
715
715
  function parseArgs(argv) {
716
716
  const a = { _: [], flags: {} };
@@ -730,7 +730,7 @@ function parseArgs(argv) {
730
730
  }
731
731
  return a;
732
732
  }
733
- var VALUE_FLAGS = /* @__PURE__ */ new Set(["url", "name", "key", "format", "diff", "fail-over", "env-file"]);
733
+ var VALUE_FLAGS = /* @__PURE__ */ new Set(["url", "name", "key", "format", "diff", "fail-over", "env-file", "limit"]);
734
734
  var tty = process.stdout.isTTY && !process.env.NO_COLOR;
735
735
  var c = (code2) => (s) => tty ? `\x1B[${code2}m${s}\x1B[0m` : s;
736
736
  var bold = c("1");
@@ -1183,8 +1183,9 @@ async function cmdRotate(a) {
1183
1183
  async function cmdVerify(a) {
1184
1184
  const base = baseUrl(a);
1185
1185
  const id = a._[1];
1186
+ if (base && a.flags.chain) return cmdVerifyChain(a, base);
1186
1187
  if (!base || !id) {
1187
- err(red("usage: tokenzero verify <request_id> [--url \u2026]"));
1188
+ err(red("usage: tokenzero verify <request_id> [--url \u2026] | tokenzero verify --chain [--limit N]"));
1188
1189
  process.exitCode = 2;
1189
1190
  return;
1190
1191
  }
@@ -1208,6 +1209,55 @@ async function cmdVerify(a) {
1208
1209
  out(` baseline $${rc.baseline_usd} \xB7 actual $${rc.actual_usd} \xB7 saved $${rc.savings_usd} \xB7 engines ${rc.engines.join(",") || "none"} \xB7 pricing ${rc.pricing_version}`);
1209
1210
  if (!ok) process.exitCode = 1;
1210
1211
  }
1212
+ async function cmdVerifyChain(a, base) {
1213
+ const saved = loadCreds();
1214
+ const key = a.flags.key || process.env.TOKENZERO_KEY || saved?.api_key || "";
1215
+ if (!key) {
1216
+ err(red("--chain needs the workspace key (TOKENZERO_KEY, --key, or saved credentials)"));
1217
+ process.exitCode = 2;
1218
+ return;
1219
+ }
1220
+ const limit = Number(a.flags.limit) || 50;
1221
+ const [c2, k] = await Promise.all([api(base, `/v1/ledger/chain?limit=${limit}`, { headers: { "x-tokenzero-key": key } }), api(base, "/.well-known/tokenzero.json")]);
1222
+ if (c2.status !== 200) {
1223
+ err(red(`chain: ${c2.status} ${JSON.stringify(c2.body).slice(0, 200)}`));
1224
+ process.exitCode = 1;
1225
+ return;
1226
+ }
1227
+ const items = c2.body.items ?? [];
1228
+ const keys = /* @__PURE__ */ new Map();
1229
+ for (const jwk of k.body?.keys ?? []) keys.set(jwk.kid, crypto2.createPublicKey({ key: { kty: jwk.kty, crv: jwk.crv, x: jwk.x }, format: "jwk" }));
1230
+ const sha = (s) => crypto2.createHash("sha256").update(s).digest("hex");
1231
+ let ok = true, reason = "";
1232
+ for (let i = 0; i < items.length && ok; i++) {
1233
+ const { receipt, signature } = items[i];
1234
+ const pub = keys.get(receipt.key_id);
1235
+ if (!pub || !crypto2.verify(null, Buffer.from(canonical(receipt)), pub, Buffer.from(signature, "base64url"))) {
1236
+ ok = false;
1237
+ reason = `bad signature at seq ${receipt.seq}`;
1238
+ break;
1239
+ }
1240
+ const prev = items[i - 1];
1241
+ if (prev && receipt.seq !== prev.receipt.seq + 1) {
1242
+ ok = false;
1243
+ reason = `seq ${receipt.seq} does not follow ${prev.receipt.seq}`;
1244
+ } else if (prev && receipt.prev_receipt_hash !== sha(prev.signature)) {
1245
+ ok = false;
1246
+ reason = `prev_receipt_hash mismatch at seq ${receipt.seq}`;
1247
+ } else if (!prev && receipt.seq === 1 && receipt.prev_receipt_hash !== "0".repeat(64)) {
1248
+ ok = false;
1249
+ reason = "genesis receipt does not point at the zero hash";
1250
+ }
1251
+ }
1252
+ if (a.flags.json) {
1253
+ out(JSON.stringify({ valid: ok, reason: reason || null, checked: items.length, head_seq: c2.body.head_seq, server_verification: c2.body.verification }, null, 2));
1254
+ if (!ok) process.exitCode = 1;
1255
+ return;
1256
+ }
1257
+ out(`${ok ? green("\u2713 chain intact") : red("\u2717 CHAIN BROKEN")}: ${items.length} receipt(s) checked locally${c2.body.head_seq != null ? `, head seq ${c2.body.head_seq}` : ""}${reason ? " \u2014 " + reason : ""}`);
1258
+ if (items.length) out(dim(` ${items[0].receipt.seq ?? "?"} \u2026 ${items.at(-1).receipt.seq ?? "?"} \xB7 savings in this run $${items.reduce((s, x) => s + (x.receipt.savings_usd ?? 0), 0).toFixed(4)} \xB7 key ${items[0].receipt.key_id}`));
1259
+ if (!ok) process.exitCode = 1;
1260
+ }
1211
1261
  function help() {
1212
1262
  out(`${bold("tokenzero")} v${VERSION} \u2014 cut your LLM bill with a BYOK gateway
1213
1263
 
@@ -1220,6 +1270,7 @@ function help() {
1220
1270
  Static token-waste audit. Runs locally; nothing is uploaded. --fail-over makes it a CI gate.
1221
1271
  ${bold("tokenzero usage")} [--json] Routed spend vs the free tier, savings, fee preview.
1222
1272
  ${bold("tokenzero verify")} <request_id> Verify a savings receipt's signature offline.
1273
+ ${bold("tokenzero verify --chain")} [--limit N] Verify the workspace's append-only receipt chain (signatures, seq, prev hashes).
1223
1274
  ${bold("tokenzero rotate-key")} [--yes] Issue a new workspace key; rewrites it in .env. Old key dies immediately.
1224
1275
 
1225
1276
  Env: TOKENZERO_URL, TOKENZERO_KEY, NO_COLOR`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tokenzero/cli",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "description": "TokenZero CLI: connect a project to the TokenZero BYOK gateway, audit LLM token waste locally, check usage and verify signed savings receipts.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",