@rozoai/checkout 0.1.16 → 0.1.18

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.
@@ -9982,6 +9982,8 @@ var SUPPORT = Object.freeze({
9982
9982
  discord: "https://discord.gg/EfWejgTbuU"
9983
9983
  });
9984
9984
  var SUPPORT_TEXT = `Need help? Email ${SUPPORT.email}, or reach ROZO on X ${SUPPORT.x} or Discord ${SUPPORT.discord}.`;
9985
+ var BULK_DOCS_URL = "https://docs.rozo.ai/products/checkout/bulk-and-agents";
9986
+ var BULK_HINT_TEXT = `Buying regularly or for others? ${BULK_DOCS_URL}`;
9985
9987
 
9986
9988
  // scripts/src/lib/output.mjs
9987
9989
  var PUBLIC_SUPPORT_URLS = /* @__PURE__ */ new Set([SUPPORT.x, SUPPORT.discord]);
@@ -21849,18 +21851,66 @@ function promptPassphrase(prompt = " Keystore passphrase: ") {
21849
21851
  });
21850
21852
  }
21851
21853
 
21852
- // scripts/src/lib/api.mjs
21854
+ // scripts/src/lib/version.mjs
21853
21855
  import { createRequire } from "node:module";
21856
+ var PKG_VERSION = (() => {
21857
+ const requireFrom = createRequire(import.meta.url);
21858
+ for (const candidate of ["../../package.json", "../../../package.json"]) {
21859
+ try {
21860
+ const pkg = requireFrom(candidate);
21861
+ if (pkg?.name === "@rozoai/checkout" && pkg.version) return pkg.version;
21862
+ } catch {
21863
+ }
21864
+ }
21865
+ return "0.0.0";
21866
+ })();
21854
21867
 
21855
21868
  // scripts/src/lib/http.mjs
21856
21869
  var DEFAULT_TIMEOUT_MS = 2e4;
21857
- var USER_AGENT = "rozo-checkout-skill/1.0";
21858
- async function request(method, url, { body, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
21870
+ var USER_AGENT = `rozo-checkout-skill/${PKG_VERSION}`;
21871
+ var MAX_RETRIES = 3;
21872
+ var MAX_TOTAL_WAIT_MS = 12e4;
21873
+ var FALLBACK_BACKOFF_MS = [2e3, 4e3, 8e3];
21874
+ var defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
21875
+ function parseRetryAfter(value, now = Date.now()) {
21876
+ if (value === null || value === void 0) return null;
21877
+ const s = String(value).trim();
21878
+ if (!s) return null;
21879
+ if (/^\d+(\.\d+)?$/.test(s)) return Math.round(Number(s) * 1e3);
21880
+ const at = Date.parse(s);
21881
+ if (!Number.isFinite(at)) return null;
21882
+ return Math.max(0, at - now);
21883
+ }
21884
+ function bodyRetryAfterMs(json) {
21885
+ const raw = json?.retryAfterSeconds ?? json?.error?.retryAfterSeconds;
21886
+ if (raw === null || raw === void 0 || raw === "") return null;
21887
+ const n = Number(raw);
21888
+ return Number.isFinite(n) && n >= 0 ? Math.round(n * 1e3) : null;
21889
+ }
21890
+ function rateLimitInfo(res) {
21891
+ const pick = (name) => res.headers?.get?.(name) ?? null;
21892
+ const info = {
21893
+ limit: pick("x-ratelimit-limit"),
21894
+ remaining: pick("x-ratelimit-remaining"),
21895
+ tier: pick("x-ratelimit-tier"),
21896
+ scope: pick("x-ratelimit-scope")
21897
+ };
21898
+ const present = Object.fromEntries(Object.entries(info).filter(([, v]) => v !== null));
21899
+ return Object.keys(present).length ? present : null;
21900
+ }
21901
+ function parseBody(text) {
21902
+ if (!text) return null;
21903
+ try {
21904
+ return JSON.parse(text);
21905
+ } catch {
21906
+ return null;
21907
+ }
21908
+ }
21909
+ async function fetchOnce(method, url, body, timeoutMs) {
21859
21910
  const controller = new AbortController();
21860
21911
  const timer = setTimeout(() => controller.abort(), timeoutMs);
21861
- let res;
21862
21912
  try {
21863
- res = await fetch(url, {
21913
+ const res = await fetch(url, {
21864
21914
  method,
21865
21915
  headers: {
21866
21916
  accept: "application/json",
@@ -21870,6 +21920,8 @@ async function request(method, url, { body, timeoutMs = DEFAULT_TIMEOUT_MS } = {
21870
21920
  body: body !== void 0 ? JSON.stringify(body) : void 0,
21871
21921
  signal: controller.signal
21872
21922
  });
21923
+ const text = await res.text();
21924
+ return { res, text };
21873
21925
  } catch (err) {
21874
21926
  throw new SkillError(
21875
21927
  err?.name === "AbortError" ? "HTTP_TIMEOUT" : "HTTP_UNREACHABLE",
@@ -21879,31 +21931,54 @@ async function request(method, url, { body, timeoutMs = DEFAULT_TIMEOUT_MS } = {
21879
21931
  } finally {
21880
21932
  clearTimeout(timer);
21881
21933
  }
21882
- const text = await res.text();
21883
- let json = null;
21884
- if (text) {
21885
- try {
21886
- json = JSON.parse(text);
21887
- } catch {
21888
- json = null;
21934
+ }
21935
+ async function request(method, url, {
21936
+ body,
21937
+ timeoutMs = DEFAULT_TIMEOUT_MS,
21938
+ sleep = defaultSleep,
21939
+ now = Date.now,
21940
+ maxTotalWaitMs = MAX_TOTAL_WAIT_MS
21941
+ } = {}) {
21942
+ let waitedMs = 0;
21943
+ let retries = 0;
21944
+ for (; ; ) {
21945
+ const { res, text } = await fetchOnce(method, url, body, timeoutMs);
21946
+ const json = parseBody(text);
21947
+ if (res.ok) {
21948
+ if (json === null) {
21949
+ throw new SkillError("HTTP_BAD_JSON", "Endpoint returned a non-JSON body.", {
21950
+ url: redactUrl(url),
21951
+ snippet: redact(text).slice(0, 400)
21952
+ });
21953
+ }
21954
+ return json;
21955
+ }
21956
+ const headerWaitMs = parseRetryAfter(res.headers?.get?.("retry-after"), now());
21957
+ const hintMs = headerWaitMs ?? bodyRetryAfterMs(json);
21958
+ const retryable = res.status === 429 || res.status === 503 && hintMs !== null;
21959
+ if (retryable && retries < MAX_RETRIES) {
21960
+ const waitMs = hintMs ?? FALLBACK_BACKOFF_MS[retries];
21961
+ if (waitedMs + waitMs <= Math.min(maxTotalWaitMs, MAX_TOTAL_WAIT_MS)) {
21962
+ retries += 1;
21963
+ waitedMs += waitMs;
21964
+ await sleep(waitMs);
21965
+ continue;
21966
+ }
21889
21967
  }
21890
- }
21891
- if (!res.ok) {
21892
21968
  const code = json?.code || json?.error?.code || (typeof json?.error === "string" && /^[A-Z][A-Z0-9_]+$/.test(json.error) ? json.error : null) || `HTTP_${res.status}`;
21893
21969
  const message = json?.message || (typeof json?.error === "string" ? json.error : json?.error?.message) || `HTTP ${res.status}`;
21970
+ const rateLimit = rateLimitInfo(res);
21894
21971
  throw new SkillError(code, redact(String(message)), {
21895
21972
  httpStatus: res.status,
21896
21973
  url: redactUrl(url),
21897
- body: json ?? redact(text).slice(0, 800)
21898
- });
21899
- }
21900
- if (json === null) {
21901
- throw new SkillError("HTTP_BAD_JSON", "Endpoint returned a non-JSON body.", {
21902
- url: redactUrl(url),
21903
- snippet: redact(text).slice(0, 400)
21974
+ body: json ?? redact(text).slice(0, 800),
21975
+ ...retryable ? {
21976
+ retries,
21977
+ retryAfterSeconds: hintMs === null ? null : Math.ceil(hintMs / 1e3)
21978
+ } : {},
21979
+ ...rateLimit ? { rateLimit } : {}
21904
21980
  });
21905
21981
  }
21906
- return json;
21907
21982
  }
21908
21983
  function redactUrl(url) {
21909
21984
  try {
@@ -21917,18 +21992,200 @@ function getJson(url, opts) {
21917
21992
  return request("GET", url, opts);
21918
21993
  }
21919
21994
 
21920
- // scripts/src/lib/api.mjs
21921
- var PKG_VERSION = (() => {
21922
- const requireFrom = createRequire(import.meta.url);
21923
- for (const candidate of ["../../package.json", "../../../package.json"]) {
21995
+ // scripts/src/lib/state.mjs
21996
+ import fs4 from "node:fs";
21997
+ import path4 from "node:path";
21998
+ import os3 from "node:os";
21999
+ import crypto5 from "node:crypto";
22000
+ var LOCK_STALE_MS = 6e4;
22001
+ var LOCK_WAIT_MS = 1e4;
22002
+ var LOCK_POLL_MS = 25;
22003
+ function lockPath() {
22004
+ return path4.join(stateRoot(), ".send.lock");
22005
+ }
22006
+ function sleepSync(ms) {
22007
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
22008
+ }
22009
+ function tryAcquire(file) {
22010
+ try {
22011
+ const fd = fs4.openSync(file, "wx", 384);
22012
+ fs4.writeFileSync(fd, JSON.stringify({ pid: process.pid, at: (/* @__PURE__ */ new Date()).toISOString() }));
22013
+ fs4.closeSync(fd);
22014
+ return true;
22015
+ } catch (err) {
22016
+ if (err.code !== "EEXIST") throw err;
22017
+ return false;
22018
+ }
22019
+ }
22020
+ function withLock(fn) {
22021
+ const file = lockPath();
22022
+ fs4.mkdirSync(path4.dirname(file), { recursive: true, mode: 448 });
22023
+ const deadline = Date.now() + LOCK_WAIT_MS;
22024
+ for (; ; ) {
22025
+ if (tryAcquire(file)) break;
21924
22026
  try {
21925
- const pkg = requireFrom(candidate);
21926
- if (pkg?.name === "@rozoai/checkout" && pkg.version) return pkg.version;
22027
+ const age = Date.now() - fs4.statSync(file).mtimeMs;
22028
+ if (age > LOCK_STALE_MS) {
22029
+ const stolen = `${file}.stale.${crypto5.randomBytes(4).toString("hex")}`;
22030
+ try {
22031
+ fs4.renameSync(file, stolen);
22032
+ fs4.unlinkSync(stolen);
22033
+ } catch {
22034
+ }
22035
+ continue;
22036
+ }
22037
+ } catch (err) {
22038
+ if (err.code !== "ENOENT") throw err;
22039
+ continue;
22040
+ }
22041
+ if (Date.now() > deadline) {
22042
+ throw new SkillError(
22043
+ "LOCK_TIMEOUT",
22044
+ "Another rozo-checkout process is holding the send lock. Refusing to proceed rather than risk a concurrent second send."
22045
+ );
22046
+ }
22047
+ sleepSync(LOCK_POLL_MS);
22048
+ }
22049
+ try {
22050
+ return fn();
22051
+ } finally {
22052
+ try {
22053
+ fs4.unlinkSync(file);
21927
22054
  } catch {
21928
22055
  }
21929
22056
  }
21930
- return "0.0.0";
21931
- })();
22057
+ }
22058
+ function stateRoot() {
22059
+ return process.env.ROZO_CHECKOUT_STATE_DIR || path4.join(os3.homedir(), ".rozo-checkout", "state");
22060
+ }
22061
+ function statePath(rozoPaymentId) {
22062
+ if (!/^[A-Za-z0-9-]{8,64}$/.test(String(rozoPaymentId || ""))) {
22063
+ throw new SkillError("BAD_ROZO_PAYMENT_ID", "Refusing to build a state path from that id.");
22064
+ }
22065
+ return path4.join(stateRoot(), `${rozoPaymentId}.json`);
22066
+ }
22067
+ function writeAtomic(file, data) {
22068
+ const dir = path4.dirname(file);
22069
+ fs4.mkdirSync(dir, { recursive: true, mode: 448 });
22070
+ const tmp = path4.join(dir, `.${path4.basename(file)}.${crypto5.randomBytes(6).toString("hex")}.tmp`);
22071
+ const fd = fs4.openSync(tmp, "wx", 384);
22072
+ try {
22073
+ fs4.writeFileSync(fd, JSON.stringify(data, null, 2) + "\n", "utf8");
22074
+ fs4.fsyncSync(fd);
22075
+ } finally {
22076
+ fs4.closeSync(fd);
22077
+ }
22078
+ fs4.renameSync(tmp, file);
22079
+ }
22080
+ function readState(rozoPaymentId) {
22081
+ const file = statePath(rozoPaymentId);
22082
+ let raw;
22083
+ try {
22084
+ raw = fs4.readFileSync(file, "utf8");
22085
+ } catch (err) {
22086
+ if (err.code === "ENOENT") return null;
22087
+ throw new SkillError("STATE_UNREADABLE", `Cannot read local state: ${err.code}`);
22088
+ }
22089
+ try {
22090
+ return JSON.parse(raw);
22091
+ } catch {
22092
+ throw new SkillError(
22093
+ "STATE_CORRUPT",
22094
+ "The local state file for this order is corrupt. Refusing to act; inspect it manually."
22095
+ );
22096
+ }
22097
+ }
22098
+ function depositDigest(source) {
22099
+ const canonical = JSON.stringify({
22100
+ chainId: String(source?.chainId ?? ""),
22101
+ tokenSymbol: String(source?.tokenSymbol ?? "").toUpperCase(),
22102
+ tokenAddress: String(source?.tokenAddress ?? ""),
22103
+ receiverAddress: String(source?.receiverAddress ?? ""),
22104
+ receiverMemo: source?.receiverMemo ?? null,
22105
+ amount: String(source?.amount ?? ""),
22106
+ amountUnit: source?.amountUnit ?? null,
22107
+ lnInvoice: source?.lnInvoice ?? null
22108
+ });
22109
+ return crypto5.createHash("sha256").update(canonical, "utf8").digest("hex");
22110
+ }
22111
+ function claimSend(rozoPaymentId, intent, { skipCaps = false } = {}) {
22112
+ return withLock(() => {
22113
+ const state = readState(rozoPaymentId);
22114
+ if (!state) {
22115
+ throw new SkillError(
22116
+ "NO_ORDER_STATE",
22117
+ "No local record for this order. Run create-order.js in this same session first."
22118
+ );
22119
+ }
22120
+ if (state.send) {
22121
+ throw new SkillError(
22122
+ "ALREADY_SENT",
22123
+ `A send was already recorded for this order at ${state.send.claimedAt} (status: ${state.send.status}). Refusing to send twice. Check the backend for the pay-in before doing anything else.`,
22124
+ { send: state.send }
22125
+ );
22126
+ }
22127
+ const caps = skipCaps ? null : assertPaymentLimit(state.invoiceAmount);
22128
+ const next = {
22129
+ ...state,
22130
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
22131
+ send: {
22132
+ status: "claimed",
22133
+ claimedAt: (/* @__PURE__ */ new Date()).toISOString(),
22134
+ chainId: intent.chainId,
22135
+ tokenSymbol: intent.tokenSymbol,
22136
+ from: intent.from,
22137
+ to: intent.to,
22138
+ amountAtomic: intent.amountAtomic,
22139
+ memo: intent.memo ?? null,
22140
+ nonceBefore: intent.nonceBefore ?? null,
22141
+ expectedTxHash: intent.expectedTxHash ?? null,
22142
+ txHash: null
22143
+ }
22144
+ };
22145
+ writeAtomic(statePath(rozoPaymentId), next);
22146
+ return { state: next, caps };
22147
+ });
22148
+ }
22149
+ function recordSendResult(rozoPaymentId, { status, txHash = null, note = null }) {
22150
+ return withLock(() => {
22151
+ const state = readState(rozoPaymentId);
22152
+ if (!state || !state.send) {
22153
+ throw new SkillError("NO_SEND_CLAIM", "No send claim to update for this order.");
22154
+ }
22155
+ const next = {
22156
+ ...state,
22157
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
22158
+ send: {
22159
+ ...state.send,
22160
+ status,
22161
+ txHash: txHash ?? state.send.txHash,
22162
+ note,
22163
+ resolvedAt: (/* @__PURE__ */ new Date()).toISOString()
22164
+ }
22165
+ };
22166
+ writeAtomic(statePath(rozoPaymentId), next);
22167
+ return next;
22168
+ });
22169
+ }
22170
+ var MAX_PAYMENT_USD = 1100;
22171
+ function assertPaymentLimit(invoiceUsd) {
22172
+ if (invoiceUsd === null || invoiceUsd === void 0 || String(invoiceUsd).trim() === "") {
22173
+ throw new SkillError("BAD_INVOICE_AMOUNT", "Cannot evaluate the payment limit without a USD amount.");
22174
+ }
22175
+ const usd = Number(invoiceUsd);
22176
+ if (!Number.isFinite(usd) || usd < 0) {
22177
+ throw new SkillError("BAD_INVOICE_AMOUNT", "Cannot evaluate the payment limit without a USD amount.");
22178
+ }
22179
+ if (usd > MAX_PAYMENT_USD) {
22180
+ throw new SkillError(
22181
+ "CAP_PER_TX",
22182
+ `$${usd} is above the $${MAX_PAYMENT_USD} per-payment limit for automated sending. This limit has no override. Pay this invoice from a wallet you control instead \u2014 that path needs no key and has no limit.`
22183
+ );
22184
+ }
22185
+ return { usd, limitUsd: MAX_PAYMENT_USD };
22186
+ }
22187
+
22188
+ // scripts/src/lib/api.mjs
21932
22189
  var MPP_BASE = process.env.ROZO_CHECKOUT_MPP_BASE || "https://apiserver.mpprouter.dev/v1/services/rozo-agent-api";
21933
22190
  var CLIENT_LABEL = `rozo-checkout-cli/${PKG_VERSION}`;
21934
22191
  var ATTRIBUTION_CLIENT = `rozo-checkout-skill/${PKG_VERSION}`;
@@ -22166,6 +22423,8 @@ var MARGINS_MS = {
22166
22423
  };
22167
22424
  var BOLT11_MIN_VALIDITY_MS = 10 * MINUTE;
22168
22425
  var DEFAULT_MARGIN_MS = 10 * MINUTE;
22426
+ var DEFAULT_WATCH_MS = 10 * MINUTE;
22427
+ var LIGHTNING_WATCH_CAP_MS = 60 * MINUTE;
22169
22428
  function marginFor(chainId) {
22170
22429
  const key = String(chainId);
22171
22430
  return MARGINS_MS[key] ?? MARGINS_MS[chainId] ?? DEFAULT_MARGIN_MS;
@@ -22266,9 +22525,9 @@ function checkExpiry({
22266
22525
  }
22267
22526
 
22268
22527
  // scripts/src/lib/blacklist.mjs
22269
- import fs4 from "node:fs";
22270
- import path4 from "node:path";
22271
- import crypto5 from "node:crypto";
22528
+ import fs5 from "node:fs";
22529
+ import path5 from "node:path";
22530
+ import crypto6 from "node:crypto";
22272
22531
  import { fileURLToPath } from "node:url";
22273
22532
  var BlacklistError = class extends Error {
22274
22533
  constructor(code, message) {
@@ -22303,7 +22562,7 @@ function parseBlacklist(doc) {
22303
22562
  }
22304
22563
  addresses.push(e.address);
22305
22564
  }
22306
- const digest = crypto5.createHash("sha256").update(JSON.stringify(addresses), "utf8").digest("hex");
22565
+ const digest = crypto6.createHash("sha256").update(JSON.stringify(addresses), "utf8").digest("hex");
22307
22566
  if (typeof provenance.addressesSha256 !== "string" || !provenance.addressesSha256) {
22308
22567
  throw new BlacklistError("BLACKLIST_UNAVAILABLE", "Blacklist provenance digest is missing.");
22309
22568
  }
@@ -22326,11 +22585,11 @@ function parseBlacklist(doc) {
22326
22585
  return { entries, index: index2, provenance, digest };
22327
22586
  }
22328
22587
  function candidatePaths(moduleUrl) {
22329
- const here = path4.dirname(fileURLToPath(moduleUrl));
22588
+ const here = path5.dirname(fileURLToPath(moduleUrl));
22330
22589
  return [
22331
- path4.join(here, "blacklist.json"),
22332
- path4.join(here, "..", "src", "lib", "blacklist.json"),
22333
- path4.join(here, "..", "..", "src", "lib", "blacklist.json")
22590
+ path5.join(here, "blacklist.json"),
22591
+ path5.join(here, "..", "src", "lib", "blacklist.json"),
22592
+ path5.join(here, "..", "..", "src", "lib", "blacklist.json")
22334
22593
  ];
22335
22594
  }
22336
22595
  var cached = null;
@@ -22341,7 +22600,7 @@ function loadBlacklist(explicitPath) {
22341
22600
  for (const p of paths) {
22342
22601
  let raw;
22343
22602
  try {
22344
- raw = fs4.readFileSync(p, "utf8");
22603
+ raw = fs5.readFileSync(p, "utf8");
22345
22604
  } catch (err) {
22346
22605
  lastErr = err;
22347
22606
  continue;
@@ -22388,199 +22647,6 @@ function assertNotBlacklisted(targets, blacklist) {
22388
22647
  return true;
22389
22648
  }
22390
22649
 
22391
- // scripts/src/lib/state.mjs
22392
- import fs5 from "node:fs";
22393
- import path5 from "node:path";
22394
- import os3 from "node:os";
22395
- import crypto6 from "node:crypto";
22396
- var LOCK_STALE_MS = 6e4;
22397
- var LOCK_WAIT_MS = 1e4;
22398
- var LOCK_POLL_MS = 25;
22399
- function lockPath() {
22400
- return path5.join(stateRoot(), ".send.lock");
22401
- }
22402
- function sleepSync(ms) {
22403
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
22404
- }
22405
- function tryAcquire(file) {
22406
- try {
22407
- const fd = fs5.openSync(file, "wx", 384);
22408
- fs5.writeFileSync(fd, JSON.stringify({ pid: process.pid, at: (/* @__PURE__ */ new Date()).toISOString() }));
22409
- fs5.closeSync(fd);
22410
- return true;
22411
- } catch (err) {
22412
- if (err.code !== "EEXIST") throw err;
22413
- return false;
22414
- }
22415
- }
22416
- function withLock(fn) {
22417
- const file = lockPath();
22418
- fs5.mkdirSync(path5.dirname(file), { recursive: true, mode: 448 });
22419
- const deadline = Date.now() + LOCK_WAIT_MS;
22420
- for (; ; ) {
22421
- if (tryAcquire(file)) break;
22422
- try {
22423
- const age = Date.now() - fs5.statSync(file).mtimeMs;
22424
- if (age > LOCK_STALE_MS) {
22425
- const stolen = `${file}.stale.${crypto6.randomBytes(4).toString("hex")}`;
22426
- try {
22427
- fs5.renameSync(file, stolen);
22428
- fs5.unlinkSync(stolen);
22429
- } catch {
22430
- }
22431
- continue;
22432
- }
22433
- } catch (err) {
22434
- if (err.code !== "ENOENT") throw err;
22435
- continue;
22436
- }
22437
- if (Date.now() > deadline) {
22438
- throw new SkillError(
22439
- "LOCK_TIMEOUT",
22440
- "Another rozo-checkout process is holding the send lock. Refusing to proceed rather than risk a concurrent second send."
22441
- );
22442
- }
22443
- sleepSync(LOCK_POLL_MS);
22444
- }
22445
- try {
22446
- return fn();
22447
- } finally {
22448
- try {
22449
- fs5.unlinkSync(file);
22450
- } catch {
22451
- }
22452
- }
22453
- }
22454
- function stateRoot() {
22455
- return process.env.ROZO_CHECKOUT_STATE_DIR || path5.join(os3.homedir(), ".rozo-checkout", "state");
22456
- }
22457
- function statePath(rozoPaymentId) {
22458
- if (!/^[A-Za-z0-9-]{8,64}$/.test(String(rozoPaymentId || ""))) {
22459
- throw new SkillError("BAD_ROZO_PAYMENT_ID", "Refusing to build a state path from that id.");
22460
- }
22461
- return path5.join(stateRoot(), `${rozoPaymentId}.json`);
22462
- }
22463
- function writeAtomic(file, data) {
22464
- const dir = path5.dirname(file);
22465
- fs5.mkdirSync(dir, { recursive: true, mode: 448 });
22466
- const tmp = path5.join(dir, `.${path5.basename(file)}.${crypto6.randomBytes(6).toString("hex")}.tmp`);
22467
- const fd = fs5.openSync(tmp, "wx", 384);
22468
- try {
22469
- fs5.writeFileSync(fd, JSON.stringify(data, null, 2) + "\n", "utf8");
22470
- fs5.fsyncSync(fd);
22471
- } finally {
22472
- fs5.closeSync(fd);
22473
- }
22474
- fs5.renameSync(tmp, file);
22475
- }
22476
- function readState(rozoPaymentId) {
22477
- const file = statePath(rozoPaymentId);
22478
- let raw;
22479
- try {
22480
- raw = fs5.readFileSync(file, "utf8");
22481
- } catch (err) {
22482
- if (err.code === "ENOENT") return null;
22483
- throw new SkillError("STATE_UNREADABLE", `Cannot read local state: ${err.code}`);
22484
- }
22485
- try {
22486
- return JSON.parse(raw);
22487
- } catch {
22488
- throw new SkillError(
22489
- "STATE_CORRUPT",
22490
- "The local state file for this order is corrupt. Refusing to act; inspect it manually."
22491
- );
22492
- }
22493
- }
22494
- function depositDigest(source) {
22495
- const canonical = JSON.stringify({
22496
- chainId: String(source?.chainId ?? ""),
22497
- tokenSymbol: String(source?.tokenSymbol ?? "").toUpperCase(),
22498
- tokenAddress: String(source?.tokenAddress ?? ""),
22499
- receiverAddress: String(source?.receiverAddress ?? ""),
22500
- receiverMemo: source?.receiverMemo ?? null,
22501
- amount: String(source?.amount ?? ""),
22502
- amountUnit: source?.amountUnit ?? null,
22503
- lnInvoice: source?.lnInvoice ?? null
22504
- });
22505
- return crypto6.createHash("sha256").update(canonical, "utf8").digest("hex");
22506
- }
22507
- function claimSend(rozoPaymentId, intent, { skipCaps = false } = {}) {
22508
- return withLock(() => {
22509
- const state = readState(rozoPaymentId);
22510
- if (!state) {
22511
- throw new SkillError(
22512
- "NO_ORDER_STATE",
22513
- "No local record for this order. Run create-order.js in this same session first."
22514
- );
22515
- }
22516
- if (state.send) {
22517
- throw new SkillError(
22518
- "ALREADY_SENT",
22519
- `A send was already recorded for this order at ${state.send.claimedAt} (status: ${state.send.status}). Refusing to send twice. Check the backend for the pay-in before doing anything else.`,
22520
- { send: state.send }
22521
- );
22522
- }
22523
- const caps = skipCaps ? null : assertPaymentLimit(state.invoiceAmount);
22524
- const next = {
22525
- ...state,
22526
- updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
22527
- send: {
22528
- status: "claimed",
22529
- claimedAt: (/* @__PURE__ */ new Date()).toISOString(),
22530
- chainId: intent.chainId,
22531
- tokenSymbol: intent.tokenSymbol,
22532
- from: intent.from,
22533
- to: intent.to,
22534
- amountAtomic: intent.amountAtomic,
22535
- memo: intent.memo ?? null,
22536
- nonceBefore: intent.nonceBefore ?? null,
22537
- expectedTxHash: intent.expectedTxHash ?? null,
22538
- txHash: null
22539
- }
22540
- };
22541
- writeAtomic(statePath(rozoPaymentId), next);
22542
- return { state: next, caps };
22543
- });
22544
- }
22545
- function recordSendResult(rozoPaymentId, { status, txHash = null, note = null }) {
22546
- return withLock(() => {
22547
- const state = readState(rozoPaymentId);
22548
- if (!state || !state.send) {
22549
- throw new SkillError("NO_SEND_CLAIM", "No send claim to update for this order.");
22550
- }
22551
- const next = {
22552
- ...state,
22553
- updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
22554
- send: {
22555
- ...state.send,
22556
- status,
22557
- txHash: txHash ?? state.send.txHash,
22558
- note,
22559
- resolvedAt: (/* @__PURE__ */ new Date()).toISOString()
22560
- }
22561
- };
22562
- writeAtomic(statePath(rozoPaymentId), next);
22563
- return next;
22564
- });
22565
- }
22566
- var MAX_PAYMENT_USD = 1100;
22567
- function assertPaymentLimit(invoiceUsd) {
22568
- if (invoiceUsd === null || invoiceUsd === void 0 || String(invoiceUsd).trim() === "") {
22569
- throw new SkillError("BAD_INVOICE_AMOUNT", "Cannot evaluate the payment limit without a USD amount.");
22570
- }
22571
- const usd = Number(invoiceUsd);
22572
- if (!Number.isFinite(usd) || usd < 0) {
22573
- throw new SkillError("BAD_INVOICE_AMOUNT", "Cannot evaluate the payment limit without a USD amount.");
22574
- }
22575
- if (usd > MAX_PAYMENT_USD) {
22576
- throw new SkillError(
22577
- "CAP_PER_TX",
22578
- `$${usd} is above the $${MAX_PAYMENT_USD} per-payment limit for automated sending. This limit has no override. Pay this invoice from a wallet you control instead \u2014 that path needs no key and has no limit.`
22579
- );
22580
- }
22581
- return { usd, limitUsd: MAX_PAYMENT_USD };
22582
- }
22583
-
22584
22650
  // scripts/src/lib/bitrefill.mjs
22585
22651
  var BITREFILL_DESTINATION = Object.freeze({ chainId: "8453", tokenSymbol: "USDC" });
22586
22652
  var BITREFILL_MIN_EXPIRY_MS = 5 * 60 * 1e3;