@rozoai/checkout 0.1.6 → 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 +7 -2
- package/docs/QUICKSTART.es.md +2 -0
- package/docs/QUICKSTART.ja.md +2 -0
- package/docs/QUICKSTART.md +2 -0
- package/docs/QUICKSTART.zh.md +2 -0
- package/docs/README.es.md +2 -0
- package/docs/README.ja.md +2 -0
- package/docs/README.zh.md +2 -0
- package/docs/security-audit-notes.md +30 -0
- package/package.json +1 -1
- package/scripts/dist/cli.js +285 -182
- package/scripts/dist/send-evm.js +284 -186
- package/scripts/dist/send-sol.js +287 -189
- package/docs/review-history.md +0 -77
package/scripts/dist/send-evm.js
CHANGED
|
@@ -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
|
|
1762
|
+
var crypto3;
|
|
1763
1763
|
var init_cryptoNode = __esm({
|
|
1764
1764
|
"node_modules/@noble/hashes/esm/cryptoNode.js"() {
|
|
1765
|
-
|
|
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 (
|
|
1861
|
-
return
|
|
1860
|
+
if (crypto3 && typeof crypto3.getRandomValues === "function") {
|
|
1861
|
+
return crypto3.getRandomValues(new Uint8Array(bytesLength));
|
|
1862
1862
|
}
|
|
1863
|
-
if (
|
|
1864
|
-
return Uint8Array.from(
|
|
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
|
|
10218
|
-
|
|
10219
|
-
|
|
10220
|
-
|
|
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;
|
|
10320
|
+
try {
|
|
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;
|
|
10221
10335
|
try {
|
|
10222
|
-
|
|
10223
|
-
cwd: dir,
|
|
10224
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
10225
|
-
encoding: "utf8"
|
|
10226
|
-
}).trim() === "true";
|
|
10336
|
+
buf = fs.readFileSync(indexPath);
|
|
10227
10337
|
} catch (err) {
|
|
10228
|
-
|
|
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
|
-
|
|
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
|
|
10239
|
-
if (!
|
|
10240
|
-
|
|
10241
|
-
|
|
10242
|
-
|
|
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
|
-
|
|
10273
|
-
|
|
10274
|
-
|
|
10275
|
-
|
|
10276
|
-
|
|
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",
|
|
@@ -10310,10 +10389,10 @@ function assertNoTrackedDotEnv(cwd = process.cwd()) {
|
|
|
10310
10389
|
}
|
|
10311
10390
|
|
|
10312
10391
|
// scripts/src/lib/key-source.mjs
|
|
10313
|
-
import
|
|
10314
|
-
import
|
|
10315
|
-
import
|
|
10316
|
-
import
|
|
10392
|
+
import fs3 from "node:fs";
|
|
10393
|
+
import path3 from "node:path";
|
|
10394
|
+
import os2 from "node:os";
|
|
10395
|
+
import crypto4 from "node:crypto";
|
|
10317
10396
|
|
|
10318
10397
|
// node_modules/viem/_esm/utils/getAction.js
|
|
10319
10398
|
function getAction(client, actionFn, name) {
|
|
@@ -21278,18 +21357,121 @@ function http(url, config = {}) {
|
|
|
21278
21357
|
init_encodeFunctionData();
|
|
21279
21358
|
init_keccak256();
|
|
21280
21359
|
|
|
21360
|
+
// scripts/src/lib/dotenv.mjs
|
|
21361
|
+
import fs2 from "node:fs";
|
|
21362
|
+
import os from "node:os";
|
|
21363
|
+
import path2 from "node:path";
|
|
21364
|
+
var ALLOWED_KEYS = [
|
|
21365
|
+
"ROZO_CHECKOUT_EVM_KEY",
|
|
21366
|
+
"ROZO_CHECKOUT_SOL_KEY",
|
|
21367
|
+
"ROZO_CHECKOUT_EVM_KEYSTORE",
|
|
21368
|
+
"ROZO_CHECKOUT_KEYSTORE_PASSPHRASE"
|
|
21369
|
+
];
|
|
21370
|
+
var ALLOWED_PATTERN = /^ROZO_CHECKOUT_RPC_[A-Za-z0-9_]+$/;
|
|
21371
|
+
function isAllowedKey(key) {
|
|
21372
|
+
return ALLOWED_KEYS.includes(key) || ALLOWED_PATTERN.test(key);
|
|
21373
|
+
}
|
|
21374
|
+
function parseDotenv(text) {
|
|
21375
|
+
const vars = {};
|
|
21376
|
+
const lines = String(text).split(/\r?\n/);
|
|
21377
|
+
lines.forEach((rawLine, i) => {
|
|
21378
|
+
const lineNo = i + 1;
|
|
21379
|
+
const line = rawLine.trim();
|
|
21380
|
+
if (!line || line.startsWith("#")) return;
|
|
21381
|
+
const withoutExport = line.startsWith("export ") ? line.slice(7).trim() : line;
|
|
21382
|
+
const eq = withoutExport.indexOf("=");
|
|
21383
|
+
if (eq === -1) {
|
|
21384
|
+
throw new SkillError(
|
|
21385
|
+
"BAD_ENV_FILE",
|
|
21386
|
+
`Malformed .env: line ${lineNo} is not KEY=VALUE. (Content withheld \u2014 it may be a secret.)`
|
|
21387
|
+
);
|
|
21388
|
+
}
|
|
21389
|
+
const key = withoutExport.slice(0, eq).trim();
|
|
21390
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
|
|
21391
|
+
throw new SkillError(
|
|
21392
|
+
"BAD_ENV_FILE",
|
|
21393
|
+
`Malformed .env: line ${lineNo} has an invalid key name. (Content withheld.)`
|
|
21394
|
+
);
|
|
21395
|
+
}
|
|
21396
|
+
let value = withoutExport.slice(eq + 1).trim();
|
|
21397
|
+
if (value.length >= 2 && (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'"))) {
|
|
21398
|
+
value = value.slice(1, -1);
|
|
21399
|
+
}
|
|
21400
|
+
vars[key] = value;
|
|
21401
|
+
});
|
|
21402
|
+
return vars;
|
|
21403
|
+
}
|
|
21404
|
+
function filterAllowed(vars) {
|
|
21405
|
+
const out = {};
|
|
21406
|
+
for (const [k, v] of Object.entries(vars)) if (isAllowedKey(k)) out[k] = v;
|
|
21407
|
+
return out;
|
|
21408
|
+
}
|
|
21409
|
+
function envFileCandidates({ cwd = process.cwd(), home = os.homedir() } = {}) {
|
|
21410
|
+
const candidates = [path2.join(cwd, ".env")];
|
|
21411
|
+
const owned = path2.join(home, ".rozo-checkout", ".env");
|
|
21412
|
+
if (owned !== candidates[0]) candidates.push(owned);
|
|
21413
|
+
return candidates;
|
|
21414
|
+
}
|
|
21415
|
+
function resolveEnvFile({ file, cwd = process.cwd(), home = os.homedir() } = {}) {
|
|
21416
|
+
if (file) {
|
|
21417
|
+
const p = path2.resolve(file);
|
|
21418
|
+
if (!fs2.existsSync(p)) {
|
|
21419
|
+
throw new SkillError("ENV_FILE_MISSING", `No such env file: ${file}`);
|
|
21420
|
+
}
|
|
21421
|
+
return p;
|
|
21422
|
+
}
|
|
21423
|
+
return envFileCandidates({ cwd, home }).find((p) => fs2.existsSync(p)) ?? null;
|
|
21424
|
+
}
|
|
21425
|
+
function applyDotenv({
|
|
21426
|
+
file,
|
|
21427
|
+
cwd = process.cwd(),
|
|
21428
|
+
home = os.homedir(),
|
|
21429
|
+
env = process.env
|
|
21430
|
+
} = {}) {
|
|
21431
|
+
const target = resolveEnvFile({ file, cwd, home });
|
|
21432
|
+
if (!target) return null;
|
|
21433
|
+
let stat;
|
|
21434
|
+
try {
|
|
21435
|
+
stat = fs2.statSync(target);
|
|
21436
|
+
} catch (err) {
|
|
21437
|
+
throw new SkillError("ENV_FILE_MISSING", `Cannot read env file (${err.code || "error"}).`);
|
|
21438
|
+
}
|
|
21439
|
+
if (process.platform !== "win32" && stat.mode & 63) {
|
|
21440
|
+
const mode = (stat.mode & 511).toString(8).padStart(3, "0");
|
|
21441
|
+
throw new SkillError(
|
|
21442
|
+
"ENV_FILE_PERMISSIONS",
|
|
21443
|
+
`That .env is readable by other users (mode ${mode}). Run: chmod 600 ${target}`
|
|
21444
|
+
);
|
|
21445
|
+
}
|
|
21446
|
+
assertNotTrackedByGit(target);
|
|
21447
|
+
const parsed = parseDotenv(fs2.readFileSync(target, "utf8"));
|
|
21448
|
+
const allowed = filterAllowed(parsed);
|
|
21449
|
+
const applied = [];
|
|
21450
|
+
for (const [k, v] of Object.entries(allowed)) {
|
|
21451
|
+
const existing = env[k];
|
|
21452
|
+
if (existing !== void 0 && String(existing).trim() !== "") continue;
|
|
21453
|
+
env[k] = v;
|
|
21454
|
+
applied.push(k);
|
|
21455
|
+
}
|
|
21456
|
+
return {
|
|
21457
|
+
path: target,
|
|
21458
|
+
applied,
|
|
21459
|
+
ignored: Object.keys(parsed).length - Object.keys(allowed).length
|
|
21460
|
+
};
|
|
21461
|
+
}
|
|
21462
|
+
|
|
21281
21463
|
// scripts/src/lib/key-source.mjs
|
|
21282
21464
|
var EVM_KEY_ENV = "ROZO_CHECKOUT_EVM_KEY";
|
|
21283
21465
|
var SOL_KEY_ENV = "ROZO_CHECKOUT_SOL_KEY";
|
|
21284
21466
|
var EVM_KEYSTORE_ENV = "ROZO_CHECKOUT_EVM_KEYSTORE";
|
|
21285
21467
|
var KEYSTORE_PASSPHRASE_ENV = "ROZO_CHECKOUT_KEYSTORE_PASSPHRASE";
|
|
21286
21468
|
function defaultSolanaKeypairPath() {
|
|
21287
|
-
return
|
|
21469
|
+
return path3.join(os2.homedir(), ".config", "solana", "id.json");
|
|
21288
21470
|
}
|
|
21289
21471
|
function assertKeyfileSafe(file) {
|
|
21290
21472
|
let stat;
|
|
21291
21473
|
try {
|
|
21292
|
-
stat =
|
|
21474
|
+
stat = fs3.statSync(file);
|
|
21293
21475
|
} catch (err) {
|
|
21294
21476
|
throw new SkillError(
|
|
21295
21477
|
"KEYFILE_UNREADABLE",
|
|
@@ -21401,7 +21583,7 @@ function decryptKeystoreV3(keystore, passphrase) {
|
|
|
21401
21583
|
if (!n || !r || !p || !dklen || !salt) {
|
|
21402
21584
|
throw new SkillError("BAD_KEYSTORE", "The keystore scrypt parameters are incomplete.");
|
|
21403
21585
|
}
|
|
21404
|
-
derived =
|
|
21586
|
+
derived = crypto4.scryptSync(pass, Buffer.from(salt, "hex"), dklen, {
|
|
21405
21587
|
N: n,
|
|
21406
21588
|
r,
|
|
21407
21589
|
p,
|
|
@@ -21417,7 +21599,7 @@ function decryptKeystoreV3(keystore, passphrase) {
|
|
|
21417
21599
|
if (!iterations || !dklen || !salt) {
|
|
21418
21600
|
throw new SkillError("BAD_KEYSTORE", "The keystore pbkdf2 parameters are incomplete.");
|
|
21419
21601
|
}
|
|
21420
|
-
derived =
|
|
21602
|
+
derived = crypto4.pbkdf2Sync(pass, Buffer.from(salt, "hex"), iterations, dklen, "sha256");
|
|
21421
21603
|
} else {
|
|
21422
21604
|
throw new SkillError("BAD_KEYSTORE", `Unsupported keystore KDF "${c.kdf}".`);
|
|
21423
21605
|
}
|
|
@@ -21432,7 +21614,7 @@ function decryptKeystoreV3(keystore, passphrase) {
|
|
|
21432
21614
|
if (c.cipher !== "aes-128-ctr") {
|
|
21433
21615
|
throw new SkillError("BAD_KEYSTORE", `Unsupported keystore cipher "${c.cipher}".`);
|
|
21434
21616
|
}
|
|
21435
|
-
const decipher =
|
|
21617
|
+
const decipher = crypto4.createDecipheriv(
|
|
21436
21618
|
"aes-128-ctr",
|
|
21437
21619
|
derived.subarray(0, 16),
|
|
21438
21620
|
Buffer.from(c.cipherparams.iv, "hex")
|
|
@@ -21454,7 +21636,7 @@ function timingSafeEqualHex(a, b) {
|
|
|
21454
21636
|
const ab = Buffer.from(String(a).toLowerCase(), "hex");
|
|
21455
21637
|
const bb = Buffer.from(String(b).toLowerCase(), "hex");
|
|
21456
21638
|
if (ab.length === 0 || ab.length !== bb.length) return false;
|
|
21457
|
-
return
|
|
21639
|
+
return crypto4.timingSafeEqual(ab, bb);
|
|
21458
21640
|
}
|
|
21459
21641
|
function looksLikeKeystore(text) {
|
|
21460
21642
|
const t = String(text).trimStart();
|
|
@@ -21466,12 +21648,16 @@ function looksLikeKeystore(text) {
|
|
|
21466
21648
|
return false;
|
|
21467
21649
|
}
|
|
21468
21650
|
}
|
|
21469
|
-
function
|
|
21651
|
+
function searchedSuffix(searched) {
|
|
21652
|
+
if (!searched || !searched.length) return "";
|
|
21653
|
+
return ` Looked for a .env in: ${searched.map(displayPath).join(", ")}.`;
|
|
21654
|
+
}
|
|
21655
|
+
function planKeySource({ family, keyfile, env = process.env, searched }) {
|
|
21470
21656
|
if (keyfile) {
|
|
21471
21657
|
const resolved = expandHome(keyfile);
|
|
21472
21658
|
let text;
|
|
21473
21659
|
try {
|
|
21474
|
-
text =
|
|
21660
|
+
text = fs3.readFileSync(resolved, "utf8");
|
|
21475
21661
|
} catch (err) {
|
|
21476
21662
|
throw new SkillError(
|
|
21477
21663
|
"KEYFILE_UNREADABLE",
|
|
@@ -21495,13 +21681,13 @@ function planKeySource({ family, keyfile, env = process.env }) {
|
|
|
21495
21681
|
}
|
|
21496
21682
|
if (family === "solana") {
|
|
21497
21683
|
const standard = defaultSolanaKeypairPath();
|
|
21498
|
-
if (
|
|
21684
|
+
if (fs3.existsSync(standard)) {
|
|
21499
21685
|
return { kind: "keypair-file", path: standard, label: displayPath(standard) };
|
|
21500
21686
|
}
|
|
21501
21687
|
if (env[SOL_KEY_ENV]) return { kind: "env", label: SOL_KEY_ENV };
|
|
21502
21688
|
throw new SkillError(
|
|
21503
21689
|
"NO_KEY_SOURCE",
|
|
21504
|
-
`No signing key found. Either create one with solana-keygen (writes ${displayPath(standard)}), pass --keyfile <path>, or set ${SOL_KEY_ENV}.`
|
|
21690
|
+
`No signing key found. Either create one with solana-keygen (writes ${displayPath(standard)}), pass --keyfile <path>, or set ${SOL_KEY_ENV}.` + searchedSuffix(searched)
|
|
21505
21691
|
);
|
|
21506
21692
|
}
|
|
21507
21693
|
if (family === "evm") {
|
|
@@ -21513,7 +21699,7 @@ function planKeySource({ family, keyfile, env = process.env }) {
|
|
|
21513
21699
|
if (env[EVM_KEY_ENV]) return { kind: "env", label: EVM_KEY_ENV };
|
|
21514
21700
|
throw new SkillError(
|
|
21515
21701
|
"NO_KEY_SOURCE",
|
|
21516
|
-
`No signing key found. Either point ${EVM_KEYSTORE_ENV} at an encrypted JSON keystore, pass --keyfile <path>, or set ${EVM_KEY_ENV}
|
|
21702
|
+
`No signing key found. Either point ${EVM_KEYSTORE_ENV} at an encrypted JSON keystore, pass --keyfile <path>, or set ${EVM_KEY_ENV}.${searchedSuffix(searched)}`
|
|
21517
21703
|
);
|
|
21518
21704
|
}
|
|
21519
21705
|
throw new SkillError("NO_KEY_SOURCE", `No key source for family "${family}".`);
|
|
@@ -21531,7 +21717,7 @@ async function loadKeySource(plan, { family, env = process.env, askPassphrase }
|
|
|
21531
21717
|
return { secretKey: decodeSolanaEnvKey(raw), label: plan.label, kind: plan.kind };
|
|
21532
21718
|
}
|
|
21533
21719
|
assertKeyfileSafe(plan.path);
|
|
21534
|
-
const text =
|
|
21720
|
+
const text = fs3.readFileSync(plan.path, "utf8");
|
|
21535
21721
|
if (plan.kind === "keypair-file") {
|
|
21536
21722
|
return { secretKey: parseSolanaKeypairJson(text), label: plan.label, kind: plan.kind };
|
|
21537
21723
|
}
|
|
@@ -21550,106 +21736,14 @@ async function loadKeySource(plan, { family, env = process.env, askPassphrase }
|
|
|
21550
21736
|
}
|
|
21551
21737
|
function expandHome(p) {
|
|
21552
21738
|
const s = String(p);
|
|
21553
|
-
if (s === "~") return
|
|
21554
|
-
if (s.startsWith("~/")) return
|
|
21739
|
+
if (s === "~") return os2.homedir();
|
|
21740
|
+
if (s.startsWith("~/")) return path3.join(os2.homedir(), s.slice(2));
|
|
21555
21741
|
return s;
|
|
21556
21742
|
}
|
|
21557
21743
|
function displayPath(p) {
|
|
21558
|
-
const home =
|
|
21744
|
+
const home = os2.homedir();
|
|
21559
21745
|
const s = String(p);
|
|
21560
|
-
return s.startsWith(home +
|
|
21561
|
-
}
|
|
21562
|
-
|
|
21563
|
-
// scripts/src/lib/dotenv.mjs
|
|
21564
|
-
import fs3 from "node:fs";
|
|
21565
|
-
import path3 from "node:path";
|
|
21566
|
-
var ALLOWED_KEYS = [
|
|
21567
|
-
"ROZO_CHECKOUT_EVM_KEY",
|
|
21568
|
-
"ROZO_CHECKOUT_SOL_KEY",
|
|
21569
|
-
"ROZO_CHECKOUT_EVM_KEYSTORE",
|
|
21570
|
-
"ROZO_CHECKOUT_KEYSTORE_PASSPHRASE"
|
|
21571
|
-
];
|
|
21572
|
-
var ALLOWED_PATTERN = /^ROZO_CHECKOUT_RPC_[A-Za-z0-9_]+$/;
|
|
21573
|
-
function isAllowedKey(key) {
|
|
21574
|
-
return ALLOWED_KEYS.includes(key) || ALLOWED_PATTERN.test(key);
|
|
21575
|
-
}
|
|
21576
|
-
function parseDotenv(text) {
|
|
21577
|
-
const vars = {};
|
|
21578
|
-
const lines = String(text).split(/\r?\n/);
|
|
21579
|
-
lines.forEach((rawLine, i) => {
|
|
21580
|
-
const lineNo = i + 1;
|
|
21581
|
-
const line = rawLine.trim();
|
|
21582
|
-
if (!line || line.startsWith("#")) return;
|
|
21583
|
-
const withoutExport = line.startsWith("export ") ? line.slice(7).trim() : line;
|
|
21584
|
-
const eq = withoutExport.indexOf("=");
|
|
21585
|
-
if (eq === -1) {
|
|
21586
|
-
throw new SkillError(
|
|
21587
|
-
"BAD_ENV_FILE",
|
|
21588
|
-
`Malformed .env: line ${lineNo} is not KEY=VALUE. (Content withheld \u2014 it may be a secret.)`
|
|
21589
|
-
);
|
|
21590
|
-
}
|
|
21591
|
-
const key = withoutExport.slice(0, eq).trim();
|
|
21592
|
-
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
|
|
21593
|
-
throw new SkillError(
|
|
21594
|
-
"BAD_ENV_FILE",
|
|
21595
|
-
`Malformed .env: line ${lineNo} has an invalid key name. (Content withheld.)`
|
|
21596
|
-
);
|
|
21597
|
-
}
|
|
21598
|
-
let value = withoutExport.slice(eq + 1).trim();
|
|
21599
|
-
if (value.length >= 2 && (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'"))) {
|
|
21600
|
-
value = value.slice(1, -1);
|
|
21601
|
-
}
|
|
21602
|
-
vars[key] = value;
|
|
21603
|
-
});
|
|
21604
|
-
return vars;
|
|
21605
|
-
}
|
|
21606
|
-
function filterAllowed(vars) {
|
|
21607
|
-
const out = {};
|
|
21608
|
-
for (const [k, v] of Object.entries(vars)) if (isAllowedKey(k)) out[k] = v;
|
|
21609
|
-
return out;
|
|
21610
|
-
}
|
|
21611
|
-
function resolveEnvFile({ file, cwd = process.cwd() } = {}) {
|
|
21612
|
-
if (file) {
|
|
21613
|
-
const p = path3.resolve(file);
|
|
21614
|
-
if (!fs3.existsSync(p)) {
|
|
21615
|
-
throw new SkillError("ENV_FILE_MISSING", `No such env file: ${file}`);
|
|
21616
|
-
}
|
|
21617
|
-
return p;
|
|
21618
|
-
}
|
|
21619
|
-
const fallback = path3.join(cwd, ".env");
|
|
21620
|
-
return fs3.existsSync(fallback) ? fallback : null;
|
|
21621
|
-
}
|
|
21622
|
-
function applyDotenv({ file, cwd = process.cwd(), env = process.env } = {}) {
|
|
21623
|
-
const target = resolveEnvFile({ file, cwd });
|
|
21624
|
-
if (!target) return null;
|
|
21625
|
-
let stat;
|
|
21626
|
-
try {
|
|
21627
|
-
stat = fs3.statSync(target);
|
|
21628
|
-
} catch (err) {
|
|
21629
|
-
throw new SkillError("ENV_FILE_MISSING", `Cannot read env file (${err.code || "error"}).`);
|
|
21630
|
-
}
|
|
21631
|
-
if (process.platform !== "win32" && stat.mode & 63) {
|
|
21632
|
-
const mode = (stat.mode & 511).toString(8).padStart(3, "0");
|
|
21633
|
-
throw new SkillError(
|
|
21634
|
-
"ENV_FILE_PERMISSIONS",
|
|
21635
|
-
`That .env is readable by other users (mode ${mode}). Run: chmod 600 ${target}`
|
|
21636
|
-
);
|
|
21637
|
-
}
|
|
21638
|
-
assertNotTrackedByGit(target);
|
|
21639
|
-
const parsed = parseDotenv(fs3.readFileSync(target, "utf8"));
|
|
21640
|
-
const allowed = filterAllowed(parsed);
|
|
21641
|
-
const applied = [];
|
|
21642
|
-
for (const [k, v] of Object.entries(allowed)) {
|
|
21643
|
-
const existing = env[k];
|
|
21644
|
-
if (existing !== void 0 && String(existing).trim() !== "") continue;
|
|
21645
|
-
env[k] = v;
|
|
21646
|
-
applied.push(k);
|
|
21647
|
-
}
|
|
21648
|
-
return {
|
|
21649
|
-
path: target,
|
|
21650
|
-
applied,
|
|
21651
|
-
ignored: Object.keys(parsed).length - Object.keys(allowed).length
|
|
21652
|
-
};
|
|
21746
|
+
return s.startsWith(home + path3.sep) ? `~${s.slice(home.length)}` : s;
|
|
21653
21747
|
}
|
|
21654
21748
|
|
|
21655
21749
|
// scripts/src/lib/passphrase.mjs
|
|
@@ -22092,7 +22186,7 @@ function checkExpiry({
|
|
|
22092
22186
|
// scripts/src/lib/blacklist.mjs
|
|
22093
22187
|
import fs4 from "node:fs";
|
|
22094
22188
|
import path4 from "node:path";
|
|
22095
|
-
import
|
|
22189
|
+
import crypto5 from "node:crypto";
|
|
22096
22190
|
import { fileURLToPath } from "node:url";
|
|
22097
22191
|
var BlacklistError = class extends Error {
|
|
22098
22192
|
constructor(code, message) {
|
|
@@ -22127,7 +22221,7 @@ function parseBlacklist(doc) {
|
|
|
22127
22221
|
}
|
|
22128
22222
|
addresses.push(e.address);
|
|
22129
22223
|
}
|
|
22130
|
-
const digest =
|
|
22224
|
+
const digest = crypto5.createHash("sha256").update(JSON.stringify(addresses), "utf8").digest("hex");
|
|
22131
22225
|
if (typeof provenance.addressesSha256 !== "string" || !provenance.addressesSha256) {
|
|
22132
22226
|
throw new BlacklistError("BLACKLIST_UNAVAILABLE", "Blacklist provenance digest is missing.");
|
|
22133
22227
|
}
|
|
@@ -22215,8 +22309,8 @@ function assertNotBlacklisted(targets, blacklist) {
|
|
|
22215
22309
|
// scripts/src/lib/state.mjs
|
|
22216
22310
|
import fs5 from "node:fs";
|
|
22217
22311
|
import path5 from "node:path";
|
|
22218
|
-
import
|
|
22219
|
-
import
|
|
22312
|
+
import os3 from "node:os";
|
|
22313
|
+
import crypto6 from "node:crypto";
|
|
22220
22314
|
var LOCK_STALE_MS = 6e4;
|
|
22221
22315
|
var LOCK_WAIT_MS = 1e4;
|
|
22222
22316
|
var LOCK_POLL_MS = 25;
|
|
@@ -22246,7 +22340,7 @@ function withLock(fn) {
|
|
|
22246
22340
|
try {
|
|
22247
22341
|
const age = Date.now() - fs5.statSync(file).mtimeMs;
|
|
22248
22342
|
if (age > LOCK_STALE_MS) {
|
|
22249
|
-
const stolen = `${file}.stale.${
|
|
22343
|
+
const stolen = `${file}.stale.${crypto6.randomBytes(4).toString("hex")}`;
|
|
22250
22344
|
try {
|
|
22251
22345
|
fs5.renameSync(file, stolen);
|
|
22252
22346
|
fs5.unlinkSync(stolen);
|
|
@@ -22276,7 +22370,7 @@ function withLock(fn) {
|
|
|
22276
22370
|
}
|
|
22277
22371
|
}
|
|
22278
22372
|
function stateRoot() {
|
|
22279
|
-
return process.env.ROZO_CHECKOUT_STATE_DIR || path5.join(
|
|
22373
|
+
return process.env.ROZO_CHECKOUT_STATE_DIR || path5.join(os3.homedir(), ".rozo-checkout", "state");
|
|
22280
22374
|
}
|
|
22281
22375
|
function statePath(rozoPaymentId) {
|
|
22282
22376
|
if (!/^[A-Za-z0-9-]{8,64}$/.test(String(rozoPaymentId || ""))) {
|
|
@@ -22287,7 +22381,7 @@ function statePath(rozoPaymentId) {
|
|
|
22287
22381
|
function writeAtomic(file, data) {
|
|
22288
22382
|
const dir = path5.dirname(file);
|
|
22289
22383
|
fs5.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
22290
|
-
const tmp = path5.join(dir, `.${path5.basename(file)}.${
|
|
22384
|
+
const tmp = path5.join(dir, `.${path5.basename(file)}.${crypto6.randomBytes(6).toString("hex")}.tmp`);
|
|
22291
22385
|
const fd = fs5.openSync(tmp, "wx", 384);
|
|
22292
22386
|
try {
|
|
22293
22387
|
fs5.writeFileSync(fd, JSON.stringify(data, null, 2) + "\n", "utf8");
|
|
@@ -22326,7 +22420,7 @@ function depositDigest(source) {
|
|
|
22326
22420
|
amountUnit: source?.amountUnit ?? null,
|
|
22327
22421
|
lnInvoice: source?.lnInvoice ?? null
|
|
22328
22422
|
});
|
|
22329
|
-
return
|
|
22423
|
+
return crypto6.createHash("sha256").update(canonical, "utf8").digest("hex");
|
|
22330
22424
|
}
|
|
22331
22425
|
function claimSend(rozoPaymentId, intent, { skipCaps = false } = {}) {
|
|
22332
22426
|
return withLock(() => {
|
|
@@ -22770,7 +22864,11 @@ async function main(argv) {
|
|
|
22770
22864
|
const rozoPaymentId = assertRozoPaymentId(args["rozo-payment-id"] || args._[0]);
|
|
22771
22865
|
assertNoTrackedDotEnv();
|
|
22772
22866
|
const dotenv = applyDotenv({ file: args["env-file"] });
|
|
22773
|
-
const plan = planKeySource({
|
|
22867
|
+
const plan = planKeySource({
|
|
22868
|
+
family: "evm",
|
|
22869
|
+
keyfile: args.keyfile,
|
|
22870
|
+
searched: args["env-file"] ? null : envFileCandidates()
|
|
22871
|
+
});
|
|
22774
22872
|
const loaded = await loadKeySource(plan, { family: "evm", askPassphrase: promptPassphrase });
|
|
22775
22873
|
const account = privateKeyToAccount(loaded.privateKey);
|
|
22776
22874
|
const sender = account.address;
|