@rozoai/checkout 0.1.7 → 0.1.10

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
@@ -33,7 +33,7 @@ npx @rozoai/checkout pay <coinbase-link> --with usdt-solana
33
33
  | Base | `usdc-base` | `8453` | 6 decimals |
34
34
  | Solana | `usdt-solana` `usdc-solana` | `900` | SPL; native SOL not supported |
35
35
  | Stellar | `usdc-stellar` | `1500` | `MEMO_TEXT` memo required — shown in the deposit block |
36
- | Bitcoin Lightning | `btc-lightning` | `lightning` | BOLT11; amounts in satoshis |
36
+ | Bitcoin Lightning | `btc-lightning` | `lightning` | BOLT11; amounts in satoshis. Any wallet that can pay an invoice works, including Cashu/ecash wallets — the mint melts your ecash into the Lightning payment. |
37
37
 
38
38
  Native gas coins (SOL, BNB, ETH, MATIC) and on-chain BTC are not accepted.
39
39
 
@@ -52,7 +52,10 @@ already hold and pay from wherever it already lives.
52
52
  address plus `receiverMemo`, so whatever you send from — exchange or wallet —
53
53
  must let you set a memo. Omit it and the payment is lost.
54
54
  - **Lightning pays an invoice, not an address.** Scan or paste
55
- `deposit.lnInvoice`; there is no address to send to.
55
+ `deposit.lnInvoice`; there is no address to send to. A Cashu/ecash wallet
56
+ works here too — paying the invoice melts the ecash on your behalf, and this
57
+ route settles against ROZO's own Lightning node rather than a third-party
58
+ swap, so no special handling is needed on our side.
56
59
  - **Only `--send` needs a private key**, read from `ROZO_CHECKOUT_EVM_KEY` or
57
60
  `ROZO_CHECKOUT_SOL_KEY`, and it covers EVM chains and Solana only. Everything
58
61
  else is keyless.
@@ -70,6 +73,8 @@ environment (or a gitignored `.env`) still works for unattended automation.
70
73
  <details>
71
74
  <summary><b>Set up a local wallet for <code>--send</code></b> — .env template and per-wallet export steps</summary>
72
75
 
76
+ You very likely do not need any of this: the default path needs no key, and for Stellar the `stellar-agent-wallet` skill sends with its own key handling — this `.env` setup is only for unattended EVM/Solana automation with a dedicated low-balance hot wallet.
77
+
73
78
  **None of this is needed for the default path.** Paying from your own wallet
74
79
  needs no key and no configuration, and works with wallets that can never be
75
80
  used here — including hardware wallets and exchange accounts.
@@ -224,6 +224,8 @@ Un archivo de claves o un `.env` no debe ser legible por otros usuarios
224
224
  <details>
225
225
  <summary><b>Configurar una billetera local para <code>--send</code></b> — plantilla .env y pasos de exportación por billetera</summary>
226
226
 
227
+ Muy probablemente no necesites nada de esto: la ruta por defecto no requiere ninguna clave, y para Stellar la skill `stellar-agent-wallet` envía con su propio manejo de claves — esta configuración `.env` es solo para automatización desatendida en EVM/Solana con una billetera caliente dedicada de saldo bajo.
228
+
227
229
  **Nada de esto hace falta para la ruta por defecto.** Pagar desde la propia
228
230
  billetera no necesita clave ni configuración, y funciona con billeteras que aquí
229
231
  nunca se pueden usar, incluidas las de hardware y las cuentas de exchange.
@@ -202,6 +202,8 @@ ROZO_CHECKOUT_EVM_KEYSTORE=~/wallets/hot.json \
202
202
  <details>
203
203
  <summary><b><code>--send</code> 用のローカルウォレット設定</b> — .env テンプレートとウォレット別の書き出し手順</summary>
204
204
 
205
+ おそらくこれらは一切不要です。既定の経路に鍵は不要で、Stellar なら `stellar-agent-wallet` スキルが独自の鍵管理で送金します — この `.env` 設定は、残高を低く抑えた専用ホットウォレットによる EVM/Solana の無人自動化専用です。
206
+
205
207
  **既定の経路ではここに書かれたものは一切不要です。** 自分のウォレットから支払う場合、
206
208
  鍵も設定も要りません。しかもここでは決して使えないウォレット — ハードウェアウォレットや
207
209
  取引所アカウントを含む — でも支払えます。
@@ -217,6 +217,8 @@ not be tracked by git. Both are refused rather than warned about.
217
217
  <details>
218
218
  <summary><b>Set up a local wallet for <code>--send</code></b> — .env template and per-wallet export steps</summary>
219
219
 
220
+ You very likely do not need any of this: the default path needs no key, and for Stellar the `stellar-agent-wallet` skill sends with its own key handling — this `.env` setup is only for unattended EVM/Solana automation with a dedicated low-balance hot wallet.
221
+
220
222
  **None of this is needed for the default path.** Paying from your own wallet
221
223
  needs no key and no configuration, and works with wallets that can never be
222
224
  used here — including hardware wallets and exchange accounts.
@@ -185,6 +185,8 @@ ROZO_CHECKOUT_EVM_KEYSTORE=~/wallets/hot.json \
185
185
  <details>
186
186
  <summary><b>为 <code>--send</code> 配置本地钱包</b> —— .env 模板与各钱包导出步骤</summary>
187
187
 
188
+ 你很可能完全不需要这些:默认路径不需要任何私钥;Stellar 来源可用 `stellar-agent-wallet` skill,它有自己的密钥管理来完成发送 —— 这份 `.env` 配置只用于无人值守的 EVM/Solana 自动化,且应使用余额很低的专用热钱包。
189
+
188
190
  **默认路径不需要这里的任何东西。** 用你自己的钱包付款既不需要私钥也不需要任何配置,
189
191
  而且支持那些在这里根本用不了的钱包 —— 包括硬件钱包和交易所账户。
190
192
 
package/docs/README.es.md CHANGED
@@ -239,6 +239,8 @@ Billetera: cualquier billetera, **sin clave**. Solo `--send` la necesita: firma
239
239
  <details>
240
240
  <summary><b>Configurar una billetera local para <code>--send</code></b> — plantilla .env y pasos de exportación por billetera</summary>
241
241
 
242
+ Muy probablemente no necesites nada de esto: la ruta por defecto no requiere ninguna clave, y para Stellar la skill `stellar-agent-wallet` envía con su propio manejo de claves — esta configuración `.env` es solo para automatización desatendida en EVM/Solana con una billetera caliente dedicada de saldo bajo.
243
+
242
244
  **Nada de esto hace falta para la ruta por defecto.** Pagar desde la propia
243
245
  billetera no necesita clave ni configuración, y funciona con billeteras que aquí
244
246
  nunca se pueden usar, incluidas las de hardware y las cuentas de exchange.
package/docs/README.ja.md CHANGED
@@ -232,6 +232,8 @@ https://checkout.rozo.ai/llms.txt を自分のコンテキストに読み込み
232
232
  <details>
233
233
  <summary><b><code>--send</code> 用のローカルウォレット設定</b> — .env テンプレートとウォレット別の書き出し手順</summary>
234
234
 
235
+ おそらくこれらは一切不要です。既定の経路に鍵は不要で、Stellar なら `stellar-agent-wallet` スキルが独自の鍵管理で送金します — この `.env` 設定は、残高を低く抑えた専用ホットウォレットによる EVM/Solana の無人自動化専用です。
236
+
235
237
  **既定の経路ではここに書かれたものは一切不要です。** 自分のウォレットから支払う場合、
236
238
  鍵も設定も要りません。しかもここでは決して使えないウォレット — ハードウェアウォレットや
237
239
  取引所アカウントを含む — でも支払えます。
package/docs/README.zh.md CHANGED
@@ -214,6 +214,8 @@ node scripts/dist/status.js --rozo-payment-id <uuid> --watch
214
214
  <details>
215
215
  <summary><b>为 <code>--send</code> 配置本地钱包</b> —— .env 模板与各钱包导出步骤</summary>
216
216
 
217
+ 你很可能完全不需要这些:默认路径不需要任何私钥;Stellar 来源可用 `stellar-agent-wallet` skill,它有自己的密钥管理来完成发送 —— 这份 `.env` 配置只用于无人值守的 EVM/Solana 自动化,且应使用余额很低的专用热钱包。
218
+
217
219
  **默认路径不需要这里的任何东西。** 用你自己的钱包付款既不需要私钥也不需要任何配置,
218
220
  而且支持那些在这里根本用不了的钱包 —— 包括硬件钱包和交易所账户。
219
221
 
@@ -0,0 +1,30 @@
1
+ # Security-audit notes for scanners and reviewers
2
+
3
+ What automated scans flag in this skill, and why each pattern is present.
4
+
5
+ ## `env_credential_access` — environment variable read + network send
6
+
7
+ Real and by design, in exactly two places: `send-evm.js` reads
8
+ `ROZO_CHECKOUT_EVM_KEY` (or an encrypted keystore) and `send-sol.js` reads
9
+ `ROZO_CHECKOUT_SOL_KEY`, to sign a transfer the user has already confirmed
10
+ twice (`--confirm` on the order, `--send` on the send). That is the entire
11
+ optional hot-wallet mode ("Mode B" in SKILL.md); the default mode never
12
+ touches a key. Mitigations, all enforced in code, not prose:
13
+
14
+ - keys come from the environment or a named file only — never argv, never
15
+ printed, scrubbed from every output object (`forget()` in `lib/keys.mjs`);
16
+ - a `.env` is only read from the working directory, this tool's own
17
+ `~/.rozo-checkout/.env`, or a path the user names with `--env-file`; a
18
+ generic `~/.env` is never read;
19
+ - sends refuse if any `.env` here is git-tracked, refuse over the $1,100
20
+ per-payment cap, re-verify chain id and token decimals against the RPC, and
21
+ record the send locally before broadcast so the same order cannot pay twice.
22
+
23
+ ## `dangerous_exec` — subprocess execution
24
+
25
+ Removed in 0.1.9. Earlier versions shelled out to `git rev-parse` /
26
+ `git ls-files` for one purpose: refusing to use hot-wallet keys from a
27
+ directory whose `.env` is git-tracked. The check now reads `.git/index`
28
+ directly (`lib/keys.mjs`), so the published bundle contains no
29
+ `child_process` usage at all. The fail-closed contract is unchanged — an
30
+ unparseable index refuses rather than assumes.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rozoai/checkout",
3
- "version": "0.1.7",
3
+ "version": "0.1.10",
4
4
  "type": "module",
5
5
  "description": "Pay an OpenRouter Coinbase Payment Link with BTC Lightning, or USDT/USDC on Solana, BNB Chain, Ethereum, Polygon, Base or Stellar.",
6
6
  "license": "MIT",
@@ -21270,10 +21270,10 @@ var require_dist = __commonJS({
21270
21270
  });
21271
21271
 
21272
21272
  // node_modules/uuid/dist/esm-node/rng.js
21273
- import crypto6 from "crypto";
21273
+ import crypto7 from "crypto";
21274
21274
  function rng() {
21275
21275
  if (poolPtr > rnds8Pool.length - 16) {
21276
- crypto6.randomFillSync(rnds8Pool);
21276
+ crypto7.randomFillSync(rnds8Pool);
21277
21277
  poolPtr = 0;
21278
21278
  }
21279
21279
  return rnds8Pool.slice(poolPtr, poolPtr += 16);
@@ -21473,14 +21473,14 @@ var init_v35 = __esm({
21473
21473
  });
21474
21474
 
21475
21475
  // node_modules/uuid/dist/esm-node/md5.js
21476
- import crypto7 from "crypto";
21476
+ import crypto8 from "crypto";
21477
21477
  function md5(bytes) {
21478
21478
  if (Array.isArray(bytes)) {
21479
21479
  bytes = Buffer.from(bytes);
21480
21480
  } else if (typeof bytes === "string") {
21481
21481
  bytes = Buffer.from(bytes, "utf8");
21482
21482
  }
21483
- return crypto7.createHash("md5").update(bytes).digest();
21483
+ return crypto8.createHash("md5").update(bytes).digest();
21484
21484
  }
21485
21485
  var md5_default;
21486
21486
  var init_md5 = __esm({
@@ -21525,14 +21525,14 @@ var init_v4 = __esm({
21525
21525
  });
21526
21526
 
21527
21527
  // node_modules/uuid/dist/esm-node/sha1.js
21528
- import crypto8 from "crypto";
21528
+ import crypto9 from "crypto";
21529
21529
  function sha1(bytes) {
21530
21530
  if (Array.isArray(bytes)) {
21531
21531
  bytes = Buffer.from(bytes);
21532
21532
  } else if (typeof bytes === "string") {
21533
21533
  bytes = Buffer.from(bytes, "utf8");
21534
21534
  }
21535
- return crypto8.createHash("sha1").update(bytes).digest();
21535
+ return crypto9.createHash("sha1").update(bytes).digest();
21536
21536
  }
21537
21537
  var sha1_default;
21538
21538
  var init_sha1 = __esm({
@@ -42729,7 +42729,7 @@ function checkExpiry({
42729
42729
  import fs6 from "node:fs";
42730
42730
  import path6 from "node:path";
42731
42731
  import os4 from "node:os";
42732
- import crypto5 from "node:crypto";
42732
+ import crypto6 from "node:crypto";
42733
42733
 
42734
42734
  // node_modules/viem/_esm/utils/getAction.js
42735
42735
  function getAction(client, actionFn, name) {
@@ -53697,43 +53697,148 @@ init_keccak256();
53697
53697
  // scripts/src/lib/keys.mjs
53698
53698
  import fs4 from "node:fs";
53699
53699
  import path4 from "node:path";
53700
- import { execFileSync } from "node:child_process";
53701
- var ENV_FILE_RE = /^\.env(\..+)?$/;
53702
- var PUBLIC_ENV_RE = /^\.env\.(example|sample|template)$/;
53703
- function insideGitWorkTree(dir, whatFor) {
53700
+ import crypto5 from "node:crypto";
53701
+ function findGitRepo(dir) {
53702
+ for (const v of ["GIT_DIR", "GIT_WORK_TREE", "GIT_INDEX_FILE"]) {
53703
+ if (process.env[v]) {
53704
+ throw new SkillError(
53705
+ "TRACKED_DOTENV_UNVERIFIABLE",
53706
+ `${v} is set, so the repository layout cannot be resolved by reading the filesystem alone. Unset it (or run from a plain checkout) before using hot-wallet keys.`
53707
+ );
53708
+ }
53709
+ }
53710
+ let cur = path4.resolve(dir);
53711
+ for (; ; ) {
53712
+ const dotGit = path4.join(cur, ".git");
53713
+ let st = null;
53714
+ try {
53715
+ st = fs4.statSync(dotGit);
53716
+ } catch (err) {
53717
+ if (err?.code !== "ENOENT" && err?.code !== "ENOTDIR") {
53718
+ throw new SkillError(
53719
+ "TRACKED_DOTENV_UNVERIFIABLE",
53720
+ `A .git entry here could not be inspected (${err?.code || "error"}), so tracked status cannot be proved. Refusing.`
53721
+ );
53722
+ }
53723
+ st = null;
53724
+ }
53725
+ if (st) {
53726
+ if (st.isDirectory()) return { root: cur, gitDir: dotGit };
53727
+ let text;
53728
+ try {
53729
+ text = fs4.readFileSync(dotGit, "utf8");
53730
+ } catch {
53731
+ throw new SkillError(
53732
+ "TRACKED_DOTENV_UNVERIFIABLE",
53733
+ "A .git entry exists here but could not be read, so tracked status cannot be proved. Refusing."
53734
+ );
53735
+ }
53736
+ const m = /^gitdir:\s*(.+)\s*$/m.exec(text);
53737
+ if (!m) {
53738
+ throw new SkillError(
53739
+ "TRACKED_DOTENV_UNVERIFIABLE",
53740
+ "A .git file exists here but is not a recognised gitdir pointer. Refusing."
53741
+ );
53742
+ }
53743
+ return { root: cur, gitDir: path4.resolve(cur, m[1]) };
53744
+ }
53745
+ const parent = path4.dirname(cur);
53746
+ if (parent === cur) return null;
53747
+ cur = parent;
53748
+ }
53749
+ }
53750
+ function trackedPathsFromIndex(buf, { hashLen = 20 } = {}) {
53751
+ const fail2 = (why) => new SkillError(
53752
+ "TRACKED_DOTENV_UNVERIFIABLE",
53753
+ `The git index could not be interpreted (${why}), so tracked status cannot be proved. Refusing.`
53754
+ );
53755
+ if (buf.length < 12 + hashLen || buf.toString("latin1", 0, 4) !== "DIRC") throw fail2("bad header");
53756
+ const algo = hashLen === 32 ? "sha256" : "sha1";
53757
+ const expected = buf.subarray(buf.length - hashLen);
53758
+ const actual = crypto5.createHash(algo).update(buf.subarray(0, buf.length - hashLen)).digest();
53759
+ if (!actual.equals(expected)) throw fail2(`${algo} checksum mismatch`);
53760
+ const version6 = buf.readUInt32BE(4);
53761
+ if (version6 !== 2 && version6 !== 3) throw fail2(`unsupported index version ${version6}`);
53762
+ const count = buf.readUInt32BE(8);
53763
+ const paths = /* @__PURE__ */ new Set();
53764
+ const fixed = 40 + hashLen + 2;
53765
+ let off = 12;
53766
+ for (let i = 0; i < count; i++) {
53767
+ const entryStart = off;
53768
+ if (off + fixed > buf.length - hashLen) throw fail2("truncated entry");
53769
+ const flags = buf.readUInt16BE(off + fixed - 2);
53770
+ let nameOff = off + fixed;
53771
+ if (flags & 16384) {
53772
+ if (version6 < 3) throw fail2("extended flags in v2 index");
53773
+ nameOff += 2;
53774
+ }
53775
+ const nameLen = flags & 4095;
53776
+ let end;
53777
+ if (nameLen < 4095) {
53778
+ end = nameOff + nameLen;
53779
+ if (end > buf.length - hashLen) throw fail2("truncated path");
53780
+ } else {
53781
+ end = buf.indexOf(0, nameOff);
53782
+ if (end === -1 || end > buf.length - hashLen) throw fail2("unterminated path");
53783
+ }
53784
+ paths.add(buf.toString("utf8", nameOff, end));
53785
+ const entryLen = end - entryStart;
53786
+ off = entryStart + (Math.floor(entryLen / 8) + 1) * 8;
53787
+ }
53788
+ while (off < buf.length - hashLen) {
53789
+ if (off + 8 > buf.length - hashLen) throw fail2("truncated extension header");
53790
+ const extName = buf.toString("latin1", off, off + 4);
53791
+ const extSize = buf.readUInt32BE(off + 4);
53792
+ const first = extName.charCodeAt(0);
53793
+ if (first >= 97 && first <= 122) {
53794
+ throw fail2(`mandatory index extension "${extName}" (e.g. core.splitIndex) is not supported`);
53795
+ }
53796
+ off += 8 + extSize;
53797
+ if (off > buf.length - hashLen) throw fail2("truncated extension");
53798
+ }
53799
+ return paths;
53800
+ }
53801
+ function repoHashLen(gitDir) {
53802
+ let configDir = gitDir;
53803
+ try {
53804
+ const common = fs4.readFileSync(path4.join(gitDir, "commondir"), "utf8").trim();
53805
+ if (common) configDir = path4.resolve(gitDir, common);
53806
+ } catch {
53807
+ }
53808
+ try {
53809
+ const cfg = fs4.readFileSync(path4.join(configDir, "config"), "utf8");
53810
+ if (/^\s*objectformat\s*=\s*sha256\s*$/im.test(cfg)) return 32;
53811
+ } catch {
53812
+ }
53813
+ return 20;
53814
+ }
53815
+ function trackedAmong(repo, files) {
53816
+ const indexPath = path4.join(repo.gitDir, "index");
53817
+ let buf;
53704
53818
  try {
53705
- return execFileSync("git", ["rev-parse", "--is-inside-work-tree"], {
53706
- cwd: dir,
53707
- stdio: ["ignore", "pipe", "pipe"],
53708
- encoding: "utf8"
53709
- }).trim() === "true";
53819
+ buf = fs4.readFileSync(indexPath);
53710
53820
  } catch (err) {
53711
- const stderr = String(err?.stderr || "");
53712
- if (/not a git repository|does not appear to be a git repository/i.test(stderr)) return false;
53821
+ if (err?.code === "ENOENT") return [];
53713
53822
  throw new SkillError(
53714
53823
  "TRACKED_DOTENV_UNVERIFIABLE",
53715
- `git could not be consulted (${err?.code || "unknown error"}), so it cannot be proved that ${whatFor} is untracked. Refusing rather than assuming it is safe.`
53824
+ "The git index exists but could not be read, so tracked status cannot be proved. Refusing."
53716
53825
  );
53717
53826
  }
53827
+ const tracked = trackedPathsFromIndex(buf, { hashLen: repoHashLen(repo.gitDir) });
53828
+ return files.filter((f) => {
53829
+ const rel = path4.relative(repo.root, path4.resolve(f)).split(path4.sep).join("/");
53830
+ return tracked.has(rel);
53831
+ });
53718
53832
  }
53833
+ var ENV_FILE_RE = /^\.env(\..+)?$/;
53834
+ var PUBLIC_ENV_RE = /^\.env\.(example|sample|template)$/;
53719
53835
  function assertNotTrackedByGit(file) {
53720
53836
  const dir = path4.dirname(path4.resolve(file));
53721
- const base = path4.basename(file);
53722
- if (!insideGitWorkTree(dir, "this key file")) return { checked: true, tracked: false };
53723
- let out2;
53724
- try {
53725
- out2 = execFileSync("git", ["ls-files", "-z", "--", base], {
53726
- cwd: dir,
53727
- stdio: ["ignore", "pipe", "ignore"],
53728
- encoding: "utf8"
53729
- });
53730
- } catch {
53731
- throw new SkillError(
53732
- "TRACKED_DOTENV_UNVERIFIABLE",
53733
- "git could not report whether this key file is tracked. Refusing to use it."
53734
- );
53735
- }
53736
- if (out2.split("\0").filter(Boolean).length) {
53837
+ const repo = findGitRepo(dir);
53838
+ if (!repo) return { checked: true, tracked: false };
53839
+ const hits = trackedAmong(repo, [file]);
53840
+ if (hits.length) {
53841
+ const base = path4.basename(file);
53737
53842
  throw new SkillError(
53738
53843
  "TRACKED_KEYFILE",
53739
53844
  `${base} is tracked by git. A committed key is one push from being public \u2014 untrack it (git rm --cached ${base}) and gitignore it before using it to sign.`
@@ -53752,37 +53857,11 @@ function assertNoTrackedDotEnv(cwd = process.cwd()) {
53752
53857
  );
53753
53858
  }
53754
53859
  if (candidates.length === 0) return { checked: true, tracked: false, candidates: [] };
53755
- let insideRepo;
53756
- try {
53757
- insideRepo = execFileSync("git", ["rev-parse", "--is-inside-work-tree"], {
53758
- cwd,
53759
- stdio: ["ignore", "pipe", "pipe"],
53760
- encoding: "utf8"
53761
- }).trim() === "true";
53762
- } catch (err) {
53763
- const stderr = String(err?.stderr || "");
53764
- const notARepo = /not a git repository|does not appear to be a git repository/i.test(stderr);
53765
- if (notARepo) return { checked: true, tracked: false, candidates };
53766
- throw new SkillError(
53767
- "TRACKED_DOTENV_UNVERIFIABLE",
53768
- `Found ${candidates.length} .env file(s) here, but git could not be consulted (${err?.code || "unknown error"}), so it cannot be proved they are untracked. Refusing to use hot-wallet keys in this directory.`
53769
- );
53770
- }
53771
- if (!insideRepo) return { checked: true, tracked: false, candidates };
53772
- let out2;
53773
- try {
53774
- out2 = execFileSync("git", ["ls-files", "-z", "--", ...candidates], {
53775
- cwd,
53776
- stdio: ["ignore", "pipe", "ignore"],
53777
- encoding: "utf8"
53778
- });
53779
- } catch {
53780
- throw new SkillError(
53781
- "TRACKED_DOTENV_UNVERIFIABLE",
53782
- "git could not report whether the .env file(s) in this directory are tracked. Refusing to use hot-wallet keys rather than assuming they are safe."
53783
- );
53784
- }
53785
- const tracked = out2.split("\0").filter(Boolean);
53860
+ const repo = findGitRepo(cwd);
53861
+ if (!repo) return { checked: true, tracked: false, candidates };
53862
+ const tracked = trackedAmong(repo, candidates.map((f) => path4.join(cwd, f))).map(
53863
+ (f) => path4.basename(f)
53864
+ );
53786
53865
  if (tracked.length) {
53787
53866
  throw new SkillError(
53788
53867
  "TRACKED_DOTENV",
@@ -54018,7 +54097,7 @@ function decryptKeystoreV3(keystore, passphrase) {
54018
54097
  if (!n || !r || !p || !dklen || !salt) {
54019
54098
  throw new SkillError("BAD_KEYSTORE", "The keystore scrypt parameters are incomplete.");
54020
54099
  }
54021
- derived = crypto5.scryptSync(pass, Buffer.from(salt, "hex"), dklen, {
54100
+ derived = crypto6.scryptSync(pass, Buffer.from(salt, "hex"), dklen, {
54022
54101
  N: n,
54023
54102
  r,
54024
54103
  p,
@@ -54034,7 +54113,7 @@ function decryptKeystoreV3(keystore, passphrase) {
54034
54113
  if (!iterations || !dklen || !salt) {
54035
54114
  throw new SkillError("BAD_KEYSTORE", "The keystore pbkdf2 parameters are incomplete.");
54036
54115
  }
54037
- derived = crypto5.pbkdf2Sync(pass, Buffer.from(salt, "hex"), iterations, dklen, "sha256");
54116
+ derived = crypto6.pbkdf2Sync(pass, Buffer.from(salt, "hex"), iterations, dklen, "sha256");
54038
54117
  } else {
54039
54118
  throw new SkillError("BAD_KEYSTORE", `Unsupported keystore KDF "${c2.kdf}".`);
54040
54119
  }
@@ -54049,7 +54128,7 @@ function decryptKeystoreV3(keystore, passphrase) {
54049
54128
  if (c2.cipher !== "aes-128-ctr") {
54050
54129
  throw new SkillError("BAD_KEYSTORE", `Unsupported keystore cipher "${c2.cipher}".`);
54051
54130
  }
54052
- const decipher = crypto5.createDecipheriv(
54131
+ const decipher = crypto6.createDecipheriv(
54053
54132
  "aes-128-ctr",
54054
54133
  derived.subarray(0, 16),
54055
54134
  Buffer.from(c2.cipherparams.iv, "hex")
@@ -54071,7 +54150,7 @@ function timingSafeEqualHex(a, b) {
54071
54150
  const ab = Buffer.from(String(a).toLowerCase(), "hex");
54072
54151
  const bb = Buffer.from(String(b).toLowerCase(), "hex");
54073
54152
  if (ab.length === 0 || ab.length !== bb.length) return false;
54074
- return crypto5.timingSafeEqual(ab, bb);
54153
+ return crypto6.timingSafeEqual(ab, bb);
54075
54154
  }
54076
54155
  function looksLikeKeystore(text) {
54077
54156
  const t = String(text).trimStart();
@@ -1759,10 +1759,10 @@ var init_u64 = __esm({
1759
1759
 
1760
1760
  // node_modules/@noble/hashes/esm/cryptoNode.js
1761
1761
  import * as nc from "node:crypto";
1762
- var crypto2;
1762
+ var crypto3;
1763
1763
  var init_cryptoNode = __esm({
1764
1764
  "node_modules/@noble/hashes/esm/cryptoNode.js"() {
1765
- crypto2 = nc && typeof nc === "object" && "webcrypto" in nc ? nc.webcrypto : nc && typeof nc === "object" && "randomBytes" in nc ? nc : void 0;
1765
+ crypto3 = nc && typeof nc === "object" && "webcrypto" in nc ? nc.webcrypto : nc && typeof nc === "object" && "randomBytes" in nc ? nc : void 0;
1766
1766
  }
1767
1767
  });
1768
1768
 
@@ -1857,11 +1857,11 @@ function createHasher(hashCons) {
1857
1857
  return hashC;
1858
1858
  }
1859
1859
  function randomBytes(bytesLength = 32) {
1860
- if (crypto2 && typeof crypto2.getRandomValues === "function") {
1861
- return crypto2.getRandomValues(new Uint8Array(bytesLength));
1860
+ if (crypto3 && typeof crypto3.getRandomValues === "function") {
1861
+ return crypto3.getRandomValues(new Uint8Array(bytesLength));
1862
1862
  }
1863
- if (crypto2 && typeof crypto2.randomBytes === "function") {
1864
- return Uint8Array.from(crypto2.randomBytes(bytesLength));
1863
+ if (crypto3 && typeof crypto3.randomBytes === "function") {
1864
+ return Uint8Array.from(crypto3.randomBytes(bytesLength));
1865
1865
  }
1866
1866
  throw new Error("crypto.getRandomValues must be defined");
1867
1867
  }
@@ -10214,43 +10214,148 @@ function chainFamily(chainId) {
10214
10214
  // scripts/src/lib/keys.mjs
10215
10215
  import fs from "node:fs";
10216
10216
  import path from "node:path";
10217
- import { execFileSync } from "node:child_process";
10218
- var ENV_FILE_RE = /^\.env(\..+)?$/;
10219
- var PUBLIC_ENV_RE = /^\.env\.(example|sample|template)$/;
10220
- function insideGitWorkTree(dir, whatFor) {
10217
+ import crypto2 from "node:crypto";
10218
+ function findGitRepo(dir) {
10219
+ for (const v of ["GIT_DIR", "GIT_WORK_TREE", "GIT_INDEX_FILE"]) {
10220
+ if (process.env[v]) {
10221
+ throw new SkillError(
10222
+ "TRACKED_DOTENV_UNVERIFIABLE",
10223
+ `${v} is set, so the repository layout cannot be resolved by reading the filesystem alone. Unset it (or run from a plain checkout) before using hot-wallet keys.`
10224
+ );
10225
+ }
10226
+ }
10227
+ let cur = path.resolve(dir);
10228
+ for (; ; ) {
10229
+ const dotGit = path.join(cur, ".git");
10230
+ let st = null;
10231
+ try {
10232
+ st = fs.statSync(dotGit);
10233
+ } catch (err) {
10234
+ if (err?.code !== "ENOENT" && err?.code !== "ENOTDIR") {
10235
+ throw new SkillError(
10236
+ "TRACKED_DOTENV_UNVERIFIABLE",
10237
+ `A .git entry here could not be inspected (${err?.code || "error"}), so tracked status cannot be proved. Refusing.`
10238
+ );
10239
+ }
10240
+ st = null;
10241
+ }
10242
+ if (st) {
10243
+ if (st.isDirectory()) return { root: cur, gitDir: dotGit };
10244
+ let text;
10245
+ try {
10246
+ text = fs.readFileSync(dotGit, "utf8");
10247
+ } catch {
10248
+ throw new SkillError(
10249
+ "TRACKED_DOTENV_UNVERIFIABLE",
10250
+ "A .git entry exists here but could not be read, so tracked status cannot be proved. Refusing."
10251
+ );
10252
+ }
10253
+ const m = /^gitdir:\s*(.+)\s*$/m.exec(text);
10254
+ if (!m) {
10255
+ throw new SkillError(
10256
+ "TRACKED_DOTENV_UNVERIFIABLE",
10257
+ "A .git file exists here but is not a recognised gitdir pointer. Refusing."
10258
+ );
10259
+ }
10260
+ return { root: cur, gitDir: path.resolve(cur, m[1]) };
10261
+ }
10262
+ const parent = path.dirname(cur);
10263
+ if (parent === cur) return null;
10264
+ cur = parent;
10265
+ }
10266
+ }
10267
+ function trackedPathsFromIndex(buf, { hashLen = 20 } = {}) {
10268
+ const fail2 = (why) => new SkillError(
10269
+ "TRACKED_DOTENV_UNVERIFIABLE",
10270
+ `The git index could not be interpreted (${why}), so tracked status cannot be proved. Refusing.`
10271
+ );
10272
+ if (buf.length < 12 + hashLen || buf.toString("latin1", 0, 4) !== "DIRC") throw fail2("bad header");
10273
+ const algo = hashLen === 32 ? "sha256" : "sha1";
10274
+ const expected = buf.subarray(buf.length - hashLen);
10275
+ const actual = crypto2.createHash(algo).update(buf.subarray(0, buf.length - hashLen)).digest();
10276
+ if (!actual.equals(expected)) throw fail2(`${algo} checksum mismatch`);
10277
+ const version4 = buf.readUInt32BE(4);
10278
+ if (version4 !== 2 && version4 !== 3) throw fail2(`unsupported index version ${version4}`);
10279
+ const count = buf.readUInt32BE(8);
10280
+ const paths = /* @__PURE__ */ new Set();
10281
+ const fixed = 40 + hashLen + 2;
10282
+ let off = 12;
10283
+ for (let i = 0; i < count; i++) {
10284
+ const entryStart = off;
10285
+ if (off + fixed > buf.length - hashLen) throw fail2("truncated entry");
10286
+ const flags = buf.readUInt16BE(off + fixed - 2);
10287
+ let nameOff = off + fixed;
10288
+ if (flags & 16384) {
10289
+ if (version4 < 3) throw fail2("extended flags in v2 index");
10290
+ nameOff += 2;
10291
+ }
10292
+ const nameLen = flags & 4095;
10293
+ let end;
10294
+ if (nameLen < 4095) {
10295
+ end = nameOff + nameLen;
10296
+ if (end > buf.length - hashLen) throw fail2("truncated path");
10297
+ } else {
10298
+ end = buf.indexOf(0, nameOff);
10299
+ if (end === -1 || end > buf.length - hashLen) throw fail2("unterminated path");
10300
+ }
10301
+ paths.add(buf.toString("utf8", nameOff, end));
10302
+ const entryLen = end - entryStart;
10303
+ off = entryStart + (Math.floor(entryLen / 8) + 1) * 8;
10304
+ }
10305
+ while (off < buf.length - hashLen) {
10306
+ if (off + 8 > buf.length - hashLen) throw fail2("truncated extension header");
10307
+ const extName = buf.toString("latin1", off, off + 4);
10308
+ const extSize = buf.readUInt32BE(off + 4);
10309
+ const first = extName.charCodeAt(0);
10310
+ if (first >= 97 && first <= 122) {
10311
+ throw fail2(`mandatory index extension "${extName}" (e.g. core.splitIndex) is not supported`);
10312
+ }
10313
+ off += 8 + extSize;
10314
+ if (off > buf.length - hashLen) throw fail2("truncated extension");
10315
+ }
10316
+ return paths;
10317
+ }
10318
+ function repoHashLen(gitDir) {
10319
+ let configDir = gitDir;
10221
10320
  try {
10222
- return execFileSync("git", ["rev-parse", "--is-inside-work-tree"], {
10223
- cwd: dir,
10224
- stdio: ["ignore", "pipe", "pipe"],
10225
- encoding: "utf8"
10226
- }).trim() === "true";
10321
+ const common = fs.readFileSync(path.join(gitDir, "commondir"), "utf8").trim();
10322
+ if (common) configDir = path.resolve(gitDir, common);
10323
+ } catch {
10324
+ }
10325
+ try {
10326
+ const cfg = fs.readFileSync(path.join(configDir, "config"), "utf8");
10327
+ if (/^\s*objectformat\s*=\s*sha256\s*$/im.test(cfg)) return 32;
10328
+ } catch {
10329
+ }
10330
+ return 20;
10331
+ }
10332
+ function trackedAmong(repo, files) {
10333
+ const indexPath = path.join(repo.gitDir, "index");
10334
+ let buf;
10335
+ try {
10336
+ buf = fs.readFileSync(indexPath);
10227
10337
  } catch (err) {
10228
- const stderr = String(err?.stderr || "");
10229
- if (/not a git repository|does not appear to be a git repository/i.test(stderr)) return false;
10338
+ if (err?.code === "ENOENT") return [];
10230
10339
  throw new SkillError(
10231
10340
  "TRACKED_DOTENV_UNVERIFIABLE",
10232
- `git could not be consulted (${err?.code || "unknown error"}), so it cannot be proved that ${whatFor} is untracked. Refusing rather than assuming it is safe.`
10341
+ "The git index exists but could not be read, so tracked status cannot be proved. Refusing."
10233
10342
  );
10234
10343
  }
10344
+ const tracked = trackedPathsFromIndex(buf, { hashLen: repoHashLen(repo.gitDir) });
10345
+ return files.filter((f) => {
10346
+ const rel = path.relative(repo.root, path.resolve(f)).split(path.sep).join("/");
10347
+ return tracked.has(rel);
10348
+ });
10235
10349
  }
10350
+ var ENV_FILE_RE = /^\.env(\..+)?$/;
10351
+ var PUBLIC_ENV_RE = /^\.env\.(example|sample|template)$/;
10236
10352
  function assertNotTrackedByGit(file) {
10237
10353
  const dir = path.dirname(path.resolve(file));
10238
- const base = path.basename(file);
10239
- if (!insideGitWorkTree(dir, "this key file")) return { checked: true, tracked: false };
10240
- let out;
10241
- try {
10242
- out = execFileSync("git", ["ls-files", "-z", "--", base], {
10243
- cwd: dir,
10244
- stdio: ["ignore", "pipe", "ignore"],
10245
- encoding: "utf8"
10246
- });
10247
- } catch {
10248
- throw new SkillError(
10249
- "TRACKED_DOTENV_UNVERIFIABLE",
10250
- "git could not report whether this key file is tracked. Refusing to use it."
10251
- );
10252
- }
10253
- if (out.split("\0").filter(Boolean).length) {
10354
+ const repo = findGitRepo(dir);
10355
+ if (!repo) return { checked: true, tracked: false };
10356
+ const hits = trackedAmong(repo, [file]);
10357
+ if (hits.length) {
10358
+ const base = path.basename(file);
10254
10359
  throw new SkillError(
10255
10360
  "TRACKED_KEYFILE",
10256
10361
  `${base} is tracked by git. A committed key is one push from being public \u2014 untrack it (git rm --cached ${base}) and gitignore it before using it to sign.`
@@ -10269,37 +10374,11 @@ function assertNoTrackedDotEnv(cwd = process.cwd()) {
10269
10374
  );
10270
10375
  }
10271
10376
  if (candidates.length === 0) return { checked: true, tracked: false, candidates: [] };
10272
- let insideRepo;
10273
- try {
10274
- insideRepo = execFileSync("git", ["rev-parse", "--is-inside-work-tree"], {
10275
- cwd,
10276
- stdio: ["ignore", "pipe", "pipe"],
10277
- encoding: "utf8"
10278
- }).trim() === "true";
10279
- } catch (err) {
10280
- const stderr = String(err?.stderr || "");
10281
- const notARepo = /not a git repository|does not appear to be a git repository/i.test(stderr);
10282
- if (notARepo) return { checked: true, tracked: false, candidates };
10283
- throw new SkillError(
10284
- "TRACKED_DOTENV_UNVERIFIABLE",
10285
- `Found ${candidates.length} .env file(s) here, but git could not be consulted (${err?.code || "unknown error"}), so it cannot be proved they are untracked. Refusing to use hot-wallet keys in this directory.`
10286
- );
10287
- }
10288
- if (!insideRepo) return { checked: true, tracked: false, candidates };
10289
- let out;
10290
- try {
10291
- out = execFileSync("git", ["ls-files", "-z", "--", ...candidates], {
10292
- cwd,
10293
- stdio: ["ignore", "pipe", "ignore"],
10294
- encoding: "utf8"
10295
- });
10296
- } catch {
10297
- throw new SkillError(
10298
- "TRACKED_DOTENV_UNVERIFIABLE",
10299
- "git could not report whether the .env file(s) in this directory are tracked. Refusing to use hot-wallet keys rather than assuming they are safe."
10300
- );
10301
- }
10302
- const tracked = out.split("\0").filter(Boolean);
10377
+ const repo = findGitRepo(cwd);
10378
+ if (!repo) return { checked: true, tracked: false, candidates };
10379
+ const tracked = trackedAmong(repo, candidates.map((f) => path.join(cwd, f))).map(
10380
+ (f) => path.basename(f)
10381
+ );
10303
10382
  if (tracked.length) {
10304
10383
  throw new SkillError(
10305
10384
  "TRACKED_DOTENV",
@@ -10313,7 +10392,7 @@ function assertNoTrackedDotEnv(cwd = process.cwd()) {
10313
10392
  import fs3 from "node:fs";
10314
10393
  import path3 from "node:path";
10315
10394
  import os2 from "node:os";
10316
- import crypto3 from "node:crypto";
10395
+ import crypto4 from "node:crypto";
10317
10396
 
10318
10397
  // node_modules/viem/_esm/utils/getAction.js
10319
10398
  function getAction(client, actionFn, name) {
@@ -21504,7 +21583,7 @@ function decryptKeystoreV3(keystore, passphrase) {
21504
21583
  if (!n || !r || !p || !dklen || !salt) {
21505
21584
  throw new SkillError("BAD_KEYSTORE", "The keystore scrypt parameters are incomplete.");
21506
21585
  }
21507
- derived = crypto3.scryptSync(pass, Buffer.from(salt, "hex"), dklen, {
21586
+ derived = crypto4.scryptSync(pass, Buffer.from(salt, "hex"), dklen, {
21508
21587
  N: n,
21509
21588
  r,
21510
21589
  p,
@@ -21520,7 +21599,7 @@ function decryptKeystoreV3(keystore, passphrase) {
21520
21599
  if (!iterations || !dklen || !salt) {
21521
21600
  throw new SkillError("BAD_KEYSTORE", "The keystore pbkdf2 parameters are incomplete.");
21522
21601
  }
21523
- derived = crypto3.pbkdf2Sync(pass, Buffer.from(salt, "hex"), iterations, dklen, "sha256");
21602
+ derived = crypto4.pbkdf2Sync(pass, Buffer.from(salt, "hex"), iterations, dklen, "sha256");
21524
21603
  } else {
21525
21604
  throw new SkillError("BAD_KEYSTORE", `Unsupported keystore KDF "${c.kdf}".`);
21526
21605
  }
@@ -21535,7 +21614,7 @@ function decryptKeystoreV3(keystore, passphrase) {
21535
21614
  if (c.cipher !== "aes-128-ctr") {
21536
21615
  throw new SkillError("BAD_KEYSTORE", `Unsupported keystore cipher "${c.cipher}".`);
21537
21616
  }
21538
- const decipher = crypto3.createDecipheriv(
21617
+ const decipher = crypto4.createDecipheriv(
21539
21618
  "aes-128-ctr",
21540
21619
  derived.subarray(0, 16),
21541
21620
  Buffer.from(c.cipherparams.iv, "hex")
@@ -21557,7 +21636,7 @@ function timingSafeEqualHex(a, b) {
21557
21636
  const ab = Buffer.from(String(a).toLowerCase(), "hex");
21558
21637
  const bb = Buffer.from(String(b).toLowerCase(), "hex");
21559
21638
  if (ab.length === 0 || ab.length !== bb.length) return false;
21560
- return crypto3.timingSafeEqual(ab, bb);
21639
+ return crypto4.timingSafeEqual(ab, bb);
21561
21640
  }
21562
21641
  function looksLikeKeystore(text) {
21563
21642
  const t = String(text).trimStart();
@@ -22107,7 +22186,7 @@ function checkExpiry({
22107
22186
  // scripts/src/lib/blacklist.mjs
22108
22187
  import fs4 from "node:fs";
22109
22188
  import path4 from "node:path";
22110
- import crypto4 from "node:crypto";
22189
+ import crypto5 from "node:crypto";
22111
22190
  import { fileURLToPath } from "node:url";
22112
22191
  var BlacklistError = class extends Error {
22113
22192
  constructor(code, message) {
@@ -22142,7 +22221,7 @@ function parseBlacklist(doc) {
22142
22221
  }
22143
22222
  addresses.push(e.address);
22144
22223
  }
22145
- const digest = crypto4.createHash("sha256").update(JSON.stringify(addresses), "utf8").digest("hex");
22224
+ const digest = crypto5.createHash("sha256").update(JSON.stringify(addresses), "utf8").digest("hex");
22146
22225
  if (typeof provenance.addressesSha256 !== "string" || !provenance.addressesSha256) {
22147
22226
  throw new BlacklistError("BLACKLIST_UNAVAILABLE", "Blacklist provenance digest is missing.");
22148
22227
  }
@@ -22231,7 +22310,7 @@ function assertNotBlacklisted(targets, blacklist) {
22231
22310
  import fs5 from "node:fs";
22232
22311
  import path5 from "node:path";
22233
22312
  import os3 from "node:os";
22234
- import crypto5 from "node:crypto";
22313
+ import crypto6 from "node:crypto";
22235
22314
  var LOCK_STALE_MS = 6e4;
22236
22315
  var LOCK_WAIT_MS = 1e4;
22237
22316
  var LOCK_POLL_MS = 25;
@@ -22261,7 +22340,7 @@ function withLock(fn) {
22261
22340
  try {
22262
22341
  const age = Date.now() - fs5.statSync(file).mtimeMs;
22263
22342
  if (age > LOCK_STALE_MS) {
22264
- const stolen = `${file}.stale.${crypto5.randomBytes(4).toString("hex")}`;
22343
+ const stolen = `${file}.stale.${crypto6.randomBytes(4).toString("hex")}`;
22265
22344
  try {
22266
22345
  fs5.renameSync(file, stolen);
22267
22346
  fs5.unlinkSync(stolen);
@@ -22302,7 +22381,7 @@ function statePath(rozoPaymentId) {
22302
22381
  function writeAtomic(file, data) {
22303
22382
  const dir = path5.dirname(file);
22304
22383
  fs5.mkdirSync(dir, { recursive: true, mode: 448 });
22305
- const tmp = path5.join(dir, `.${path5.basename(file)}.${crypto5.randomBytes(6).toString("hex")}.tmp`);
22384
+ const tmp = path5.join(dir, `.${path5.basename(file)}.${crypto6.randomBytes(6).toString("hex")}.tmp`);
22306
22385
  const fd = fs5.openSync(tmp, "wx", 384);
22307
22386
  try {
22308
22387
  fs5.writeFileSync(fd, JSON.stringify(data, null, 2) + "\n", "utf8");
@@ -22341,7 +22420,7 @@ function depositDigest(source) {
22341
22420
  amountUnit: source?.amountUnit ?? null,
22342
22421
  lnInvoice: source?.lnInvoice ?? null
22343
22422
  });
22344
- return crypto5.createHash("sha256").update(canonical, "utf8").digest("hex");
22423
+ return crypto6.createHash("sha256").update(canonical, "utf8").digest("hex");
22345
22424
  }
22346
22425
  function claimSend(rozoPaymentId, intent, { skipCaps = false } = {}) {
22347
22426
  return withLock(() => {
@@ -11988,10 +11988,10 @@ var require_dist = __commonJS({
11988
11988
  });
11989
11989
 
11990
11990
  // node_modules/uuid/dist/esm-node/rng.js
11991
- import crypto5 from "crypto";
11991
+ import crypto6 from "crypto";
11992
11992
  function rng() {
11993
11993
  if (poolPtr > rnds8Pool.length - 16) {
11994
- crypto5.randomFillSync(rnds8Pool);
11994
+ crypto6.randomFillSync(rnds8Pool);
11995
11995
  poolPtr = 0;
11996
11996
  }
11997
11997
  return rnds8Pool.slice(poolPtr, poolPtr += 16);
@@ -12191,14 +12191,14 @@ var init_v35 = __esm({
12191
12191
  });
12192
12192
 
12193
12193
  // node_modules/uuid/dist/esm-node/md5.js
12194
- import crypto6 from "crypto";
12194
+ import crypto7 from "crypto";
12195
12195
  function md5(bytes) {
12196
12196
  if (Array.isArray(bytes)) {
12197
12197
  bytes = Buffer.from(bytes);
12198
12198
  } else if (typeof bytes === "string") {
12199
12199
  bytes = Buffer.from(bytes, "utf8");
12200
12200
  }
12201
- return crypto6.createHash("md5").update(bytes).digest();
12201
+ return crypto7.createHash("md5").update(bytes).digest();
12202
12202
  }
12203
12203
  var md5_default;
12204
12204
  var init_md5 = __esm({
@@ -12243,14 +12243,14 @@ var init_v4 = __esm({
12243
12243
  });
12244
12244
 
12245
12245
  // node_modules/uuid/dist/esm-node/sha1.js
12246
- import crypto7 from "crypto";
12246
+ import crypto8 from "crypto";
12247
12247
  function sha1(bytes) {
12248
12248
  if (Array.isArray(bytes)) {
12249
12249
  bytes = Buffer.from(bytes);
12250
12250
  } else if (typeof bytes === "string") {
12251
12251
  bytes = Buffer.from(bytes, "utf8");
12252
12252
  }
12253
- return crypto7.createHash("sha1").update(bytes).digest();
12253
+ return crypto8.createHash("sha1").update(bytes).digest();
12254
12254
  }
12255
12255
  var sha1_default;
12256
12256
  var init_sha1 = __esm({
@@ -32359,43 +32359,148 @@ function chainFamily(chainId) {
32359
32359
  // scripts/src/lib/keys.mjs
32360
32360
  import fs from "node:fs";
32361
32361
  import path from "node:path";
32362
- import { execFileSync } from "node:child_process";
32363
- var ENV_FILE_RE = /^\.env(\..+)?$/;
32364
- var PUBLIC_ENV_RE = /^\.env\.(example|sample|template)$/;
32365
- function insideGitWorkTree(dir, whatFor) {
32362
+ import crypto2 from "node:crypto";
32363
+ function findGitRepo(dir) {
32364
+ for (const v of ["GIT_DIR", "GIT_WORK_TREE", "GIT_INDEX_FILE"]) {
32365
+ if (process.env[v]) {
32366
+ throw new SkillError(
32367
+ "TRACKED_DOTENV_UNVERIFIABLE",
32368
+ `${v} is set, so the repository layout cannot be resolved by reading the filesystem alone. Unset it (or run from a plain checkout) before using hot-wallet keys.`
32369
+ );
32370
+ }
32371
+ }
32372
+ let cur = path.resolve(dir);
32373
+ for (; ; ) {
32374
+ const dotGit = path.join(cur, ".git");
32375
+ let st = null;
32376
+ try {
32377
+ st = fs.statSync(dotGit);
32378
+ } catch (err) {
32379
+ if (err?.code !== "ENOENT" && err?.code !== "ENOTDIR") {
32380
+ throw new SkillError(
32381
+ "TRACKED_DOTENV_UNVERIFIABLE",
32382
+ `A .git entry here could not be inspected (${err?.code || "error"}), so tracked status cannot be proved. Refusing.`
32383
+ );
32384
+ }
32385
+ st = null;
32386
+ }
32387
+ if (st) {
32388
+ if (st.isDirectory()) return { root: cur, gitDir: dotGit };
32389
+ let text;
32390
+ try {
32391
+ text = fs.readFileSync(dotGit, "utf8");
32392
+ } catch {
32393
+ throw new SkillError(
32394
+ "TRACKED_DOTENV_UNVERIFIABLE",
32395
+ "A .git entry exists here but could not be read, so tracked status cannot be proved. Refusing."
32396
+ );
32397
+ }
32398
+ const m = /^gitdir:\s*(.+)\s*$/m.exec(text);
32399
+ if (!m) {
32400
+ throw new SkillError(
32401
+ "TRACKED_DOTENV_UNVERIFIABLE",
32402
+ "A .git file exists here but is not a recognised gitdir pointer. Refusing."
32403
+ );
32404
+ }
32405
+ return { root: cur, gitDir: path.resolve(cur, m[1]) };
32406
+ }
32407
+ const parent = path.dirname(cur);
32408
+ if (parent === cur) return null;
32409
+ cur = parent;
32410
+ }
32411
+ }
32412
+ function trackedPathsFromIndex(buf, { hashLen = 20 } = {}) {
32413
+ const fail2 = (why) => new SkillError(
32414
+ "TRACKED_DOTENV_UNVERIFIABLE",
32415
+ `The git index could not be interpreted (${why}), so tracked status cannot be proved. Refusing.`
32416
+ );
32417
+ if (buf.length < 12 + hashLen || buf.toString("latin1", 0, 4) !== "DIRC") throw fail2("bad header");
32418
+ const algo = hashLen === 32 ? "sha256" : "sha1";
32419
+ const expected = buf.subarray(buf.length - hashLen);
32420
+ const actual = crypto2.createHash(algo).update(buf.subarray(0, buf.length - hashLen)).digest();
32421
+ if (!actual.equals(expected)) throw fail2(`${algo} checksum mismatch`);
32422
+ const version4 = buf.readUInt32BE(4);
32423
+ if (version4 !== 2 && version4 !== 3) throw fail2(`unsupported index version ${version4}`);
32424
+ const count = buf.readUInt32BE(8);
32425
+ const paths = /* @__PURE__ */ new Set();
32426
+ const fixed = 40 + hashLen + 2;
32427
+ let off = 12;
32428
+ for (let i = 0; i < count; i++) {
32429
+ const entryStart = off;
32430
+ if (off + fixed > buf.length - hashLen) throw fail2("truncated entry");
32431
+ const flags = buf.readUInt16BE(off + fixed - 2);
32432
+ let nameOff = off + fixed;
32433
+ if (flags & 16384) {
32434
+ if (version4 < 3) throw fail2("extended flags in v2 index");
32435
+ nameOff += 2;
32436
+ }
32437
+ const nameLen = flags & 4095;
32438
+ let end;
32439
+ if (nameLen < 4095) {
32440
+ end = nameOff + nameLen;
32441
+ if (end > buf.length - hashLen) throw fail2("truncated path");
32442
+ } else {
32443
+ end = buf.indexOf(0, nameOff);
32444
+ if (end === -1 || end > buf.length - hashLen) throw fail2("unterminated path");
32445
+ }
32446
+ paths.add(buf.toString("utf8", nameOff, end));
32447
+ const entryLen = end - entryStart;
32448
+ off = entryStart + (Math.floor(entryLen / 8) + 1) * 8;
32449
+ }
32450
+ while (off < buf.length - hashLen) {
32451
+ if (off + 8 > buf.length - hashLen) throw fail2("truncated extension header");
32452
+ const extName = buf.toString("latin1", off, off + 4);
32453
+ const extSize = buf.readUInt32BE(off + 4);
32454
+ const first = extName.charCodeAt(0);
32455
+ if (first >= 97 && first <= 122) {
32456
+ throw fail2(`mandatory index extension "${extName}" (e.g. core.splitIndex) is not supported`);
32457
+ }
32458
+ off += 8 + extSize;
32459
+ if (off > buf.length - hashLen) throw fail2("truncated extension");
32460
+ }
32461
+ return paths;
32462
+ }
32463
+ function repoHashLen(gitDir) {
32464
+ let configDir = gitDir;
32366
32465
  try {
32367
- return execFileSync("git", ["rev-parse", "--is-inside-work-tree"], {
32368
- cwd: dir,
32369
- stdio: ["ignore", "pipe", "pipe"],
32370
- encoding: "utf8"
32371
- }).trim() === "true";
32466
+ const common = fs.readFileSync(path.join(gitDir, "commondir"), "utf8").trim();
32467
+ if (common) configDir = path.resolve(gitDir, common);
32468
+ } catch {
32469
+ }
32470
+ try {
32471
+ const cfg = fs.readFileSync(path.join(configDir, "config"), "utf8");
32472
+ if (/^\s*objectformat\s*=\s*sha256\s*$/im.test(cfg)) return 32;
32473
+ } catch {
32474
+ }
32475
+ return 20;
32476
+ }
32477
+ function trackedAmong(repo, files) {
32478
+ const indexPath = path.join(repo.gitDir, "index");
32479
+ let buf;
32480
+ try {
32481
+ buf = fs.readFileSync(indexPath);
32372
32482
  } catch (err) {
32373
- const stderr = String(err?.stderr || "");
32374
- if (/not a git repository|does not appear to be a git repository/i.test(stderr)) return false;
32483
+ if (err?.code === "ENOENT") return [];
32375
32484
  throw new SkillError(
32376
32485
  "TRACKED_DOTENV_UNVERIFIABLE",
32377
- `git could not be consulted (${err?.code || "unknown error"}), so it cannot be proved that ${whatFor} is untracked. Refusing rather than assuming it is safe.`
32486
+ "The git index exists but could not be read, so tracked status cannot be proved. Refusing."
32378
32487
  );
32379
32488
  }
32489
+ const tracked = trackedPathsFromIndex(buf, { hashLen: repoHashLen(repo.gitDir) });
32490
+ return files.filter((f) => {
32491
+ const rel = path.relative(repo.root, path.resolve(f)).split(path.sep).join("/");
32492
+ return tracked.has(rel);
32493
+ });
32380
32494
  }
32495
+ var ENV_FILE_RE = /^\.env(\..+)?$/;
32496
+ var PUBLIC_ENV_RE = /^\.env\.(example|sample|template)$/;
32381
32497
  function assertNotTrackedByGit(file) {
32382
32498
  const dir = path.dirname(path.resolve(file));
32383
- const base = path.basename(file);
32384
- if (!insideGitWorkTree(dir, "this key file")) return { checked: true, tracked: false };
32385
- let out;
32386
- try {
32387
- out = execFileSync("git", ["ls-files", "-z", "--", base], {
32388
- cwd: dir,
32389
- stdio: ["ignore", "pipe", "ignore"],
32390
- encoding: "utf8"
32391
- });
32392
- } catch {
32393
- throw new SkillError(
32394
- "TRACKED_DOTENV_UNVERIFIABLE",
32395
- "git could not report whether this key file is tracked. Refusing to use it."
32396
- );
32397
- }
32398
- if (out.split("\0").filter(Boolean).length) {
32499
+ const repo = findGitRepo(dir);
32500
+ if (!repo) return { checked: true, tracked: false };
32501
+ const hits = trackedAmong(repo, [file]);
32502
+ if (hits.length) {
32503
+ const base = path.basename(file);
32399
32504
  throw new SkillError(
32400
32505
  "TRACKED_KEYFILE",
32401
32506
  `${base} is tracked by git. A committed key is one push from being public \u2014 untrack it (git rm --cached ${base}) and gitignore it before using it to sign.`
@@ -32414,37 +32519,11 @@ function assertNoTrackedDotEnv(cwd = process.cwd()) {
32414
32519
  );
32415
32520
  }
32416
32521
  if (candidates.length === 0) return { checked: true, tracked: false, candidates: [] };
32417
- let insideRepo;
32418
- try {
32419
- insideRepo = execFileSync("git", ["rev-parse", "--is-inside-work-tree"], {
32420
- cwd,
32421
- stdio: ["ignore", "pipe", "pipe"],
32422
- encoding: "utf8"
32423
- }).trim() === "true";
32424
- } catch (err) {
32425
- const stderr = String(err?.stderr || "");
32426
- const notARepo = /not a git repository|does not appear to be a git repository/i.test(stderr);
32427
- if (notARepo) return { checked: true, tracked: false, candidates };
32428
- throw new SkillError(
32429
- "TRACKED_DOTENV_UNVERIFIABLE",
32430
- `Found ${candidates.length} .env file(s) here, but git could not be consulted (${err?.code || "unknown error"}), so it cannot be proved they are untracked. Refusing to use hot-wallet keys in this directory.`
32431
- );
32432
- }
32433
- if (!insideRepo) return { checked: true, tracked: false, candidates };
32434
- let out;
32435
- try {
32436
- out = execFileSync("git", ["ls-files", "-z", "--", ...candidates], {
32437
- cwd,
32438
- stdio: ["ignore", "pipe", "ignore"],
32439
- encoding: "utf8"
32440
- });
32441
- } catch {
32442
- throw new SkillError(
32443
- "TRACKED_DOTENV_UNVERIFIABLE",
32444
- "git could not report whether the .env file(s) in this directory are tracked. Refusing to use hot-wallet keys rather than assuming they are safe."
32445
- );
32446
- }
32447
- const tracked = out.split("\0").filter(Boolean);
32522
+ const repo = findGitRepo(cwd);
32523
+ if (!repo) return { checked: true, tracked: false, candidates };
32524
+ const tracked = trackedAmong(repo, candidates.map((f) => path.join(cwd, f))).map(
32525
+ (f) => path.basename(f)
32526
+ );
32448
32527
  if (tracked.length) {
32449
32528
  throw new SkillError(
32450
32529
  "TRACKED_DOTENV",
@@ -32458,7 +32537,7 @@ function assertNoTrackedDotEnv(cwd = process.cwd()) {
32458
32537
  import fs3 from "node:fs";
32459
32538
  import path3 from "node:path";
32460
32539
  import os2 from "node:os";
32461
- import crypto2 from "node:crypto";
32540
+ import crypto3 from "node:crypto";
32462
32541
 
32463
32542
  // node_modules/viem/_esm/index.js
32464
32543
  init_keccak256();
@@ -32689,7 +32768,7 @@ function decryptKeystoreV3(keystore, passphrase) {
32689
32768
  if (!n || !r || !p || !dklen || !salt) {
32690
32769
  throw new SkillError("BAD_KEYSTORE", "The keystore scrypt parameters are incomplete.");
32691
32770
  }
32692
- derived = crypto2.scryptSync(pass, Buffer.from(salt, "hex"), dklen, {
32771
+ derived = crypto3.scryptSync(pass, Buffer.from(salt, "hex"), dklen, {
32693
32772
  N: n,
32694
32773
  r,
32695
32774
  p,
@@ -32705,7 +32784,7 @@ function decryptKeystoreV3(keystore, passphrase) {
32705
32784
  if (!iterations || !dklen || !salt) {
32706
32785
  throw new SkillError("BAD_KEYSTORE", "The keystore pbkdf2 parameters are incomplete.");
32707
32786
  }
32708
- derived = crypto2.pbkdf2Sync(pass, Buffer.from(salt, "hex"), iterations, dklen, "sha256");
32787
+ derived = crypto3.pbkdf2Sync(pass, Buffer.from(salt, "hex"), iterations, dklen, "sha256");
32709
32788
  } else {
32710
32789
  throw new SkillError("BAD_KEYSTORE", `Unsupported keystore KDF "${c.kdf}".`);
32711
32790
  }
@@ -32720,7 +32799,7 @@ function decryptKeystoreV3(keystore, passphrase) {
32720
32799
  if (c.cipher !== "aes-128-ctr") {
32721
32800
  throw new SkillError("BAD_KEYSTORE", `Unsupported keystore cipher "${c.cipher}".`);
32722
32801
  }
32723
- const decipher = crypto2.createDecipheriv(
32802
+ const decipher = crypto3.createDecipheriv(
32724
32803
  "aes-128-ctr",
32725
32804
  derived.subarray(0, 16),
32726
32805
  Buffer.from(c.cipherparams.iv, "hex")
@@ -32742,7 +32821,7 @@ function timingSafeEqualHex(a, b) {
32742
32821
  const ab = Buffer.from(String(a).toLowerCase(), "hex");
32743
32822
  const bb = Buffer.from(String(b).toLowerCase(), "hex");
32744
32823
  if (ab.length === 0 || ab.length !== bb.length) return false;
32745
- return crypto2.timingSafeEqual(ab, bb);
32824
+ return crypto3.timingSafeEqual(ab, bb);
32746
32825
  }
32747
32826
  function looksLikeKeystore(text) {
32748
32827
  const t = String(text).trimStart();
@@ -33292,7 +33371,7 @@ function checkExpiry({
33292
33371
  // scripts/src/lib/blacklist.mjs
33293
33372
  import fs4 from "node:fs";
33294
33373
  import path4 from "node:path";
33295
- import crypto3 from "node:crypto";
33374
+ import crypto4 from "node:crypto";
33296
33375
  import { fileURLToPath } from "node:url";
33297
33376
  var BlacklistError = class extends Error {
33298
33377
  constructor(code, message) {
@@ -33327,7 +33406,7 @@ function parseBlacklist(doc) {
33327
33406
  }
33328
33407
  addresses.push(e.address);
33329
33408
  }
33330
- const digest = crypto3.createHash("sha256").update(JSON.stringify(addresses), "utf8").digest("hex");
33409
+ const digest = crypto4.createHash("sha256").update(JSON.stringify(addresses), "utf8").digest("hex");
33331
33410
  if (typeof provenance.addressesSha256 !== "string" || !provenance.addressesSha256) {
33332
33411
  throw new BlacklistError("BLACKLIST_UNAVAILABLE", "Blacklist provenance digest is missing.");
33333
33412
  }
@@ -33416,7 +33495,7 @@ function assertNotBlacklisted(targets, blacklist) {
33416
33495
  import fs5 from "node:fs";
33417
33496
  import path5 from "node:path";
33418
33497
  import os3 from "node:os";
33419
- import crypto4 from "node:crypto";
33498
+ import crypto5 from "node:crypto";
33420
33499
  var LOCK_STALE_MS = 6e4;
33421
33500
  var LOCK_WAIT_MS = 1e4;
33422
33501
  var LOCK_POLL_MS = 25;
@@ -33446,7 +33525,7 @@ function withLock(fn) {
33446
33525
  try {
33447
33526
  const age = Date.now() - fs5.statSync(file).mtimeMs;
33448
33527
  if (age > LOCK_STALE_MS) {
33449
- const stolen = `${file}.stale.${crypto4.randomBytes(4).toString("hex")}`;
33528
+ const stolen = `${file}.stale.${crypto5.randomBytes(4).toString("hex")}`;
33450
33529
  try {
33451
33530
  fs5.renameSync(file, stolen);
33452
33531
  fs5.unlinkSync(stolen);
@@ -33487,7 +33566,7 @@ function statePath(rozoPaymentId) {
33487
33566
  function writeAtomic(file, data) {
33488
33567
  const dir = path5.dirname(file);
33489
33568
  fs5.mkdirSync(dir, { recursive: true, mode: 448 });
33490
- const tmp = path5.join(dir, `.${path5.basename(file)}.${crypto4.randomBytes(6).toString("hex")}.tmp`);
33569
+ const tmp = path5.join(dir, `.${path5.basename(file)}.${crypto5.randomBytes(6).toString("hex")}.tmp`);
33491
33570
  const fd = fs5.openSync(tmp, "wx", 384);
33492
33571
  try {
33493
33572
  fs5.writeFileSync(fd, JSON.stringify(data, null, 2) + "\n", "utf8");
@@ -33526,7 +33605,7 @@ function depositDigest(source) {
33526
33605
  amountUnit: source?.amountUnit ?? null,
33527
33606
  lnInvoice: source?.lnInvoice ?? null
33528
33607
  });
33529
- return crypto4.createHash("sha256").update(canonical, "utf8").digest("hex");
33608
+ return crypto5.createHash("sha256").update(canonical, "utf8").digest("hex");
33530
33609
  }
33531
33610
  function claimSend(rozoPaymentId, intent, { skipCaps = false } = {}) {
33532
33611
  return withLock(() => {