@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.
@@ -32150,6 +32150,8 @@ var SUPPORT = Object.freeze({
32150
32150
  discord: "https://discord.gg/EfWejgTbuU"
32151
32151
  });
32152
32152
  var SUPPORT_TEXT = `Need help? Email ${SUPPORT.email}, or reach ROZO on X ${SUPPORT.x} or Discord ${SUPPORT.discord}.`;
32153
+ var BULK_DOCS_URL = "https://docs.rozo.ai/products/checkout/bulk-and-agents";
32154
+ var BULK_HINT_TEXT = `Buying regularly or for others? ${BULK_DOCS_URL}`;
32153
32155
 
32154
32156
  // scripts/src/lib/output.mjs
32155
32157
  var PUBLIC_SUPPORT_URLS = /* @__PURE__ */ new Set([SUPPORT.x, SUPPORT.discord]);
@@ -33026,18 +33028,66 @@ function promptPassphrase(prompt = " Keystore passphrase: ") {
33026
33028
  });
33027
33029
  }
33028
33030
 
33029
- // scripts/src/lib/api.mjs
33031
+ // scripts/src/lib/version.mjs
33030
33032
  import { createRequire } from "node:module";
33033
+ var PKG_VERSION = (() => {
33034
+ const requireFrom = createRequire(import.meta.url);
33035
+ for (const candidate of ["../../package.json", "../../../package.json"]) {
33036
+ try {
33037
+ const pkg = requireFrom(candidate);
33038
+ if (pkg?.name === "@rozoai/checkout" && pkg.version) return pkg.version;
33039
+ } catch {
33040
+ }
33041
+ }
33042
+ return "0.0.0";
33043
+ })();
33031
33044
 
33032
33045
  // scripts/src/lib/http.mjs
33033
33046
  var DEFAULT_TIMEOUT_MS = 2e4;
33034
- var USER_AGENT = "rozo-checkout-skill/1.0";
33035
- async function request(method, url, { body, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
33047
+ var USER_AGENT = `rozo-checkout-skill/${PKG_VERSION}`;
33048
+ var MAX_RETRIES = 3;
33049
+ var MAX_TOTAL_WAIT_MS = 12e4;
33050
+ var FALLBACK_BACKOFF_MS = [2e3, 4e3, 8e3];
33051
+ var defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
33052
+ function parseRetryAfter(value, now = Date.now()) {
33053
+ if (value === null || value === void 0) return null;
33054
+ const s = String(value).trim();
33055
+ if (!s) return null;
33056
+ if (/^\d+(\.\d+)?$/.test(s)) return Math.round(Number(s) * 1e3);
33057
+ const at = Date.parse(s);
33058
+ if (!Number.isFinite(at)) return null;
33059
+ return Math.max(0, at - now);
33060
+ }
33061
+ function bodyRetryAfterMs(json) {
33062
+ const raw = json?.retryAfterSeconds ?? json?.error?.retryAfterSeconds;
33063
+ if (raw === null || raw === void 0 || raw === "") return null;
33064
+ const n = Number(raw);
33065
+ return Number.isFinite(n) && n >= 0 ? Math.round(n * 1e3) : null;
33066
+ }
33067
+ function rateLimitInfo(res) {
33068
+ const pick = (name) => res.headers?.get?.(name) ?? null;
33069
+ const info = {
33070
+ limit: pick("x-ratelimit-limit"),
33071
+ remaining: pick("x-ratelimit-remaining"),
33072
+ tier: pick("x-ratelimit-tier"),
33073
+ scope: pick("x-ratelimit-scope")
33074
+ };
33075
+ const present = Object.fromEntries(Object.entries(info).filter(([, v]) => v !== null));
33076
+ return Object.keys(present).length ? present : null;
33077
+ }
33078
+ function parseBody(text) {
33079
+ if (!text) return null;
33080
+ try {
33081
+ return JSON.parse(text);
33082
+ } catch {
33083
+ return null;
33084
+ }
33085
+ }
33086
+ async function fetchOnce(method, url, body, timeoutMs) {
33036
33087
  const controller = new AbortController();
33037
33088
  const timer = setTimeout(() => controller.abort(), timeoutMs);
33038
- let res;
33039
33089
  try {
33040
- res = await fetch(url, {
33090
+ const res = await fetch(url, {
33041
33091
  method,
33042
33092
  headers: {
33043
33093
  accept: "application/json",
@@ -33047,6 +33097,8 @@ async function request(method, url, { body, timeoutMs = DEFAULT_TIMEOUT_MS } = {
33047
33097
  body: body !== void 0 ? JSON.stringify(body) : void 0,
33048
33098
  signal: controller.signal
33049
33099
  });
33100
+ const text = await res.text();
33101
+ return { res, text };
33050
33102
  } catch (err) {
33051
33103
  throw new SkillError(
33052
33104
  err?.name === "AbortError" ? "HTTP_TIMEOUT" : "HTTP_UNREACHABLE",
@@ -33056,31 +33108,54 @@ async function request(method, url, { body, timeoutMs = DEFAULT_TIMEOUT_MS } = {
33056
33108
  } finally {
33057
33109
  clearTimeout(timer);
33058
33110
  }
33059
- const text = await res.text();
33060
- let json = null;
33061
- if (text) {
33062
- try {
33063
- json = JSON.parse(text);
33064
- } catch {
33065
- json = null;
33111
+ }
33112
+ async function request(method, url, {
33113
+ body,
33114
+ timeoutMs = DEFAULT_TIMEOUT_MS,
33115
+ sleep = defaultSleep,
33116
+ now = Date.now,
33117
+ maxTotalWaitMs = MAX_TOTAL_WAIT_MS
33118
+ } = {}) {
33119
+ let waitedMs = 0;
33120
+ let retries = 0;
33121
+ for (; ; ) {
33122
+ const { res, text } = await fetchOnce(method, url, body, timeoutMs);
33123
+ const json = parseBody(text);
33124
+ if (res.ok) {
33125
+ if (json === null) {
33126
+ throw new SkillError("HTTP_BAD_JSON", "Endpoint returned a non-JSON body.", {
33127
+ url: redactUrl(url),
33128
+ snippet: redact(text).slice(0, 400)
33129
+ });
33130
+ }
33131
+ return json;
33132
+ }
33133
+ const headerWaitMs = parseRetryAfter(res.headers?.get?.("retry-after"), now());
33134
+ const hintMs = headerWaitMs ?? bodyRetryAfterMs(json);
33135
+ const retryable = res.status === 429 || res.status === 503 && hintMs !== null;
33136
+ if (retryable && retries < MAX_RETRIES) {
33137
+ const waitMs = hintMs ?? FALLBACK_BACKOFF_MS[retries];
33138
+ if (waitedMs + waitMs <= Math.min(maxTotalWaitMs, MAX_TOTAL_WAIT_MS)) {
33139
+ retries += 1;
33140
+ waitedMs += waitMs;
33141
+ await sleep(waitMs);
33142
+ continue;
33143
+ }
33066
33144
  }
33067
- }
33068
- if (!res.ok) {
33069
33145
  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}`;
33070
33146
  const message = json?.message || (typeof json?.error === "string" ? json.error : json?.error?.message) || `HTTP ${res.status}`;
33147
+ const rateLimit = rateLimitInfo(res);
33071
33148
  throw new SkillError(code, redact(String(message)), {
33072
33149
  httpStatus: res.status,
33073
33150
  url: redactUrl(url),
33074
- body: json ?? redact(text).slice(0, 800)
33075
- });
33076
- }
33077
- if (json === null) {
33078
- throw new SkillError("HTTP_BAD_JSON", "Endpoint returned a non-JSON body.", {
33079
- url: redactUrl(url),
33080
- snippet: redact(text).slice(0, 400)
33151
+ body: json ?? redact(text).slice(0, 800),
33152
+ ...retryable ? {
33153
+ retries,
33154
+ retryAfterSeconds: hintMs === null ? null : Math.ceil(hintMs / 1e3)
33155
+ } : {},
33156
+ ...rateLimit ? { rateLimit } : {}
33081
33157
  });
33082
33158
  }
33083
- return json;
33084
33159
  }
33085
33160
  function redactUrl(url) {
33086
33161
  try {
@@ -33094,18 +33169,200 @@ function getJson(url, opts) {
33094
33169
  return request("GET", url, opts);
33095
33170
  }
33096
33171
 
33097
- // scripts/src/lib/api.mjs
33098
- var PKG_VERSION = (() => {
33099
- const requireFrom = createRequire(import.meta.url);
33100
- for (const candidate of ["../../package.json", "../../../package.json"]) {
33172
+ // scripts/src/lib/state.mjs
33173
+ import fs4 from "node:fs";
33174
+ import path4 from "node:path";
33175
+ import os3 from "node:os";
33176
+ import crypto4 from "node:crypto";
33177
+ var LOCK_STALE_MS = 6e4;
33178
+ var LOCK_WAIT_MS = 1e4;
33179
+ var LOCK_POLL_MS = 25;
33180
+ function lockPath() {
33181
+ return path4.join(stateRoot(), ".send.lock");
33182
+ }
33183
+ function sleepSync(ms) {
33184
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
33185
+ }
33186
+ function tryAcquire(file) {
33187
+ try {
33188
+ const fd = fs4.openSync(file, "wx", 384);
33189
+ fs4.writeFileSync(fd, JSON.stringify({ pid: process.pid, at: (/* @__PURE__ */ new Date()).toISOString() }));
33190
+ fs4.closeSync(fd);
33191
+ return true;
33192
+ } catch (err) {
33193
+ if (err.code !== "EEXIST") throw err;
33194
+ return false;
33195
+ }
33196
+ }
33197
+ function withLock(fn) {
33198
+ const file = lockPath();
33199
+ fs4.mkdirSync(path4.dirname(file), { recursive: true, mode: 448 });
33200
+ const deadline = Date.now() + LOCK_WAIT_MS;
33201
+ for (; ; ) {
33202
+ if (tryAcquire(file)) break;
33101
33203
  try {
33102
- const pkg = requireFrom(candidate);
33103
- if (pkg?.name === "@rozoai/checkout" && pkg.version) return pkg.version;
33204
+ const age = Date.now() - fs4.statSync(file).mtimeMs;
33205
+ if (age > LOCK_STALE_MS) {
33206
+ const stolen = `${file}.stale.${crypto4.randomBytes(4).toString("hex")}`;
33207
+ try {
33208
+ fs4.renameSync(file, stolen);
33209
+ fs4.unlinkSync(stolen);
33210
+ } catch {
33211
+ }
33212
+ continue;
33213
+ }
33214
+ } catch (err) {
33215
+ if (err.code !== "ENOENT") throw err;
33216
+ continue;
33217
+ }
33218
+ if (Date.now() > deadline) {
33219
+ throw new SkillError(
33220
+ "LOCK_TIMEOUT",
33221
+ "Another rozo-checkout process is holding the send lock. Refusing to proceed rather than risk a concurrent second send."
33222
+ );
33223
+ }
33224
+ sleepSync(LOCK_POLL_MS);
33225
+ }
33226
+ try {
33227
+ return fn();
33228
+ } finally {
33229
+ try {
33230
+ fs4.unlinkSync(file);
33104
33231
  } catch {
33105
33232
  }
33106
33233
  }
33107
- return "0.0.0";
33108
- })();
33234
+ }
33235
+ function stateRoot() {
33236
+ return process.env.ROZO_CHECKOUT_STATE_DIR || path4.join(os3.homedir(), ".rozo-checkout", "state");
33237
+ }
33238
+ function statePath(rozoPaymentId) {
33239
+ if (!/^[A-Za-z0-9-]{8,64}$/.test(String(rozoPaymentId || ""))) {
33240
+ throw new SkillError("BAD_ROZO_PAYMENT_ID", "Refusing to build a state path from that id.");
33241
+ }
33242
+ return path4.join(stateRoot(), `${rozoPaymentId}.json`);
33243
+ }
33244
+ function writeAtomic(file, data) {
33245
+ const dir = path4.dirname(file);
33246
+ fs4.mkdirSync(dir, { recursive: true, mode: 448 });
33247
+ const tmp = path4.join(dir, `.${path4.basename(file)}.${crypto4.randomBytes(6).toString("hex")}.tmp`);
33248
+ const fd = fs4.openSync(tmp, "wx", 384);
33249
+ try {
33250
+ fs4.writeFileSync(fd, JSON.stringify(data, null, 2) + "\n", "utf8");
33251
+ fs4.fsyncSync(fd);
33252
+ } finally {
33253
+ fs4.closeSync(fd);
33254
+ }
33255
+ fs4.renameSync(tmp, file);
33256
+ }
33257
+ function readState(rozoPaymentId) {
33258
+ const file = statePath(rozoPaymentId);
33259
+ let raw;
33260
+ try {
33261
+ raw = fs4.readFileSync(file, "utf8");
33262
+ } catch (err) {
33263
+ if (err.code === "ENOENT") return null;
33264
+ throw new SkillError("STATE_UNREADABLE", `Cannot read local state: ${err.code}`);
33265
+ }
33266
+ try {
33267
+ return JSON.parse(raw);
33268
+ } catch {
33269
+ throw new SkillError(
33270
+ "STATE_CORRUPT",
33271
+ "The local state file for this order is corrupt. Refusing to act; inspect it manually."
33272
+ );
33273
+ }
33274
+ }
33275
+ function depositDigest(source) {
33276
+ const canonical = JSON.stringify({
33277
+ chainId: String(source?.chainId ?? ""),
33278
+ tokenSymbol: String(source?.tokenSymbol ?? "").toUpperCase(),
33279
+ tokenAddress: String(source?.tokenAddress ?? ""),
33280
+ receiverAddress: String(source?.receiverAddress ?? ""),
33281
+ receiverMemo: source?.receiverMemo ?? null,
33282
+ amount: String(source?.amount ?? ""),
33283
+ amountUnit: source?.amountUnit ?? null,
33284
+ lnInvoice: source?.lnInvoice ?? null
33285
+ });
33286
+ return crypto4.createHash("sha256").update(canonical, "utf8").digest("hex");
33287
+ }
33288
+ function claimSend(rozoPaymentId, intent, { skipCaps = false } = {}) {
33289
+ return withLock(() => {
33290
+ const state = readState(rozoPaymentId);
33291
+ if (!state) {
33292
+ throw new SkillError(
33293
+ "NO_ORDER_STATE",
33294
+ "No local record for this order. Run create-order.js in this same session first."
33295
+ );
33296
+ }
33297
+ if (state.send) {
33298
+ throw new SkillError(
33299
+ "ALREADY_SENT",
33300
+ `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.`,
33301
+ { send: state.send }
33302
+ );
33303
+ }
33304
+ const caps = skipCaps ? null : assertPaymentLimit(state.invoiceAmount);
33305
+ const next = {
33306
+ ...state,
33307
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
33308
+ send: {
33309
+ status: "claimed",
33310
+ claimedAt: (/* @__PURE__ */ new Date()).toISOString(),
33311
+ chainId: intent.chainId,
33312
+ tokenSymbol: intent.tokenSymbol,
33313
+ from: intent.from,
33314
+ to: intent.to,
33315
+ amountAtomic: intent.amountAtomic,
33316
+ memo: intent.memo ?? null,
33317
+ nonceBefore: intent.nonceBefore ?? null,
33318
+ expectedTxHash: intent.expectedTxHash ?? null,
33319
+ txHash: null
33320
+ }
33321
+ };
33322
+ writeAtomic(statePath(rozoPaymentId), next);
33323
+ return { state: next, caps };
33324
+ });
33325
+ }
33326
+ function recordSendResult(rozoPaymentId, { status, txHash = null, note = null }) {
33327
+ return withLock(() => {
33328
+ const state = readState(rozoPaymentId);
33329
+ if (!state || !state.send) {
33330
+ throw new SkillError("NO_SEND_CLAIM", "No send claim to update for this order.");
33331
+ }
33332
+ const next = {
33333
+ ...state,
33334
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
33335
+ send: {
33336
+ ...state.send,
33337
+ status,
33338
+ txHash: txHash ?? state.send.txHash,
33339
+ note,
33340
+ resolvedAt: (/* @__PURE__ */ new Date()).toISOString()
33341
+ }
33342
+ };
33343
+ writeAtomic(statePath(rozoPaymentId), next);
33344
+ return next;
33345
+ });
33346
+ }
33347
+ var MAX_PAYMENT_USD = 1100;
33348
+ function assertPaymentLimit(invoiceUsd) {
33349
+ if (invoiceUsd === null || invoiceUsd === void 0 || String(invoiceUsd).trim() === "") {
33350
+ throw new SkillError("BAD_INVOICE_AMOUNT", "Cannot evaluate the payment limit without a USD amount.");
33351
+ }
33352
+ const usd = Number(invoiceUsd);
33353
+ if (!Number.isFinite(usd) || usd < 0) {
33354
+ throw new SkillError("BAD_INVOICE_AMOUNT", "Cannot evaluate the payment limit without a USD amount.");
33355
+ }
33356
+ if (usd > MAX_PAYMENT_USD) {
33357
+ throw new SkillError(
33358
+ "CAP_PER_TX",
33359
+ `$${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.`
33360
+ );
33361
+ }
33362
+ return { usd, limitUsd: MAX_PAYMENT_USD };
33363
+ }
33364
+
33365
+ // scripts/src/lib/api.mjs
33109
33366
  var MPP_BASE = process.env.ROZO_CHECKOUT_MPP_BASE || "https://apiserver.mpprouter.dev/v1/services/rozo-agent-api";
33110
33367
  var CLIENT_LABEL = `rozo-checkout-cli/${PKG_VERSION}`;
33111
33368
  var ATTRIBUTION_CLIENT = `rozo-checkout-skill/${PKG_VERSION}`;
@@ -33343,6 +33600,8 @@ var MARGINS_MS = {
33343
33600
  };
33344
33601
  var BOLT11_MIN_VALIDITY_MS = 10 * MINUTE;
33345
33602
  var DEFAULT_MARGIN_MS = 10 * MINUTE;
33603
+ var DEFAULT_WATCH_MS = 10 * MINUTE;
33604
+ var LIGHTNING_WATCH_CAP_MS = 60 * MINUTE;
33346
33605
  function marginFor(chainId) {
33347
33606
  const key = String(chainId);
33348
33607
  return MARGINS_MS[key] ?? MARGINS_MS[chainId] ?? DEFAULT_MARGIN_MS;
@@ -33443,9 +33702,9 @@ function checkExpiry({
33443
33702
  }
33444
33703
 
33445
33704
  // scripts/src/lib/blacklist.mjs
33446
- import fs4 from "node:fs";
33447
- import path4 from "node:path";
33448
- import crypto4 from "node:crypto";
33705
+ import fs5 from "node:fs";
33706
+ import path5 from "node:path";
33707
+ import crypto5 from "node:crypto";
33449
33708
  import { fileURLToPath } from "node:url";
33450
33709
  var BlacklistError = class extends Error {
33451
33710
  constructor(code, message) {
@@ -33480,7 +33739,7 @@ function parseBlacklist(doc) {
33480
33739
  }
33481
33740
  addresses.push(e.address);
33482
33741
  }
33483
- const digest = crypto4.createHash("sha256").update(JSON.stringify(addresses), "utf8").digest("hex");
33742
+ const digest = crypto5.createHash("sha256").update(JSON.stringify(addresses), "utf8").digest("hex");
33484
33743
  if (typeof provenance.addressesSha256 !== "string" || !provenance.addressesSha256) {
33485
33744
  throw new BlacklistError("BLACKLIST_UNAVAILABLE", "Blacklist provenance digest is missing.");
33486
33745
  }
@@ -33503,11 +33762,11 @@ function parseBlacklist(doc) {
33503
33762
  return { entries, index, provenance, digest };
33504
33763
  }
33505
33764
  function candidatePaths(moduleUrl) {
33506
- const here = path4.dirname(fileURLToPath(moduleUrl));
33765
+ const here = path5.dirname(fileURLToPath(moduleUrl));
33507
33766
  return [
33508
- path4.join(here, "blacklist.json"),
33509
- path4.join(here, "..", "src", "lib", "blacklist.json"),
33510
- path4.join(here, "..", "..", "src", "lib", "blacklist.json")
33767
+ path5.join(here, "blacklist.json"),
33768
+ path5.join(here, "..", "src", "lib", "blacklist.json"),
33769
+ path5.join(here, "..", "..", "src", "lib", "blacklist.json")
33511
33770
  ];
33512
33771
  }
33513
33772
  var cached = null;
@@ -33518,7 +33777,7 @@ function loadBlacklist(explicitPath) {
33518
33777
  for (const p of paths) {
33519
33778
  let raw;
33520
33779
  try {
33521
- raw = fs4.readFileSync(p, "utf8");
33780
+ raw = fs5.readFileSync(p, "utf8");
33522
33781
  } catch (err) {
33523
33782
  lastErr = err;
33524
33783
  continue;
@@ -33565,199 +33824,6 @@ function assertNotBlacklisted(targets, blacklist) {
33565
33824
  return true;
33566
33825
  }
33567
33826
 
33568
- // scripts/src/lib/state.mjs
33569
- import fs5 from "node:fs";
33570
- import path5 from "node:path";
33571
- import os3 from "node:os";
33572
- import crypto5 from "node:crypto";
33573
- var LOCK_STALE_MS = 6e4;
33574
- var LOCK_WAIT_MS = 1e4;
33575
- var LOCK_POLL_MS = 25;
33576
- function lockPath() {
33577
- return path5.join(stateRoot(), ".send.lock");
33578
- }
33579
- function sleepSync(ms) {
33580
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
33581
- }
33582
- function tryAcquire(file) {
33583
- try {
33584
- const fd = fs5.openSync(file, "wx", 384);
33585
- fs5.writeFileSync(fd, JSON.stringify({ pid: process.pid, at: (/* @__PURE__ */ new Date()).toISOString() }));
33586
- fs5.closeSync(fd);
33587
- return true;
33588
- } catch (err) {
33589
- if (err.code !== "EEXIST") throw err;
33590
- return false;
33591
- }
33592
- }
33593
- function withLock(fn) {
33594
- const file = lockPath();
33595
- fs5.mkdirSync(path5.dirname(file), { recursive: true, mode: 448 });
33596
- const deadline = Date.now() + LOCK_WAIT_MS;
33597
- for (; ; ) {
33598
- if (tryAcquire(file)) break;
33599
- try {
33600
- const age = Date.now() - fs5.statSync(file).mtimeMs;
33601
- if (age > LOCK_STALE_MS) {
33602
- const stolen = `${file}.stale.${crypto5.randomBytes(4).toString("hex")}`;
33603
- try {
33604
- fs5.renameSync(file, stolen);
33605
- fs5.unlinkSync(stolen);
33606
- } catch {
33607
- }
33608
- continue;
33609
- }
33610
- } catch (err) {
33611
- if (err.code !== "ENOENT") throw err;
33612
- continue;
33613
- }
33614
- if (Date.now() > deadline) {
33615
- throw new SkillError(
33616
- "LOCK_TIMEOUT",
33617
- "Another rozo-checkout process is holding the send lock. Refusing to proceed rather than risk a concurrent second send."
33618
- );
33619
- }
33620
- sleepSync(LOCK_POLL_MS);
33621
- }
33622
- try {
33623
- return fn();
33624
- } finally {
33625
- try {
33626
- fs5.unlinkSync(file);
33627
- } catch {
33628
- }
33629
- }
33630
- }
33631
- function stateRoot() {
33632
- return process.env.ROZO_CHECKOUT_STATE_DIR || path5.join(os3.homedir(), ".rozo-checkout", "state");
33633
- }
33634
- function statePath(rozoPaymentId) {
33635
- if (!/^[A-Za-z0-9-]{8,64}$/.test(String(rozoPaymentId || ""))) {
33636
- throw new SkillError("BAD_ROZO_PAYMENT_ID", "Refusing to build a state path from that id.");
33637
- }
33638
- return path5.join(stateRoot(), `${rozoPaymentId}.json`);
33639
- }
33640
- function writeAtomic(file, data) {
33641
- const dir = path5.dirname(file);
33642
- fs5.mkdirSync(dir, { recursive: true, mode: 448 });
33643
- const tmp = path5.join(dir, `.${path5.basename(file)}.${crypto5.randomBytes(6).toString("hex")}.tmp`);
33644
- const fd = fs5.openSync(tmp, "wx", 384);
33645
- try {
33646
- fs5.writeFileSync(fd, JSON.stringify(data, null, 2) + "\n", "utf8");
33647
- fs5.fsyncSync(fd);
33648
- } finally {
33649
- fs5.closeSync(fd);
33650
- }
33651
- fs5.renameSync(tmp, file);
33652
- }
33653
- function readState(rozoPaymentId) {
33654
- const file = statePath(rozoPaymentId);
33655
- let raw;
33656
- try {
33657
- raw = fs5.readFileSync(file, "utf8");
33658
- } catch (err) {
33659
- if (err.code === "ENOENT") return null;
33660
- throw new SkillError("STATE_UNREADABLE", `Cannot read local state: ${err.code}`);
33661
- }
33662
- try {
33663
- return JSON.parse(raw);
33664
- } catch {
33665
- throw new SkillError(
33666
- "STATE_CORRUPT",
33667
- "The local state file for this order is corrupt. Refusing to act; inspect it manually."
33668
- );
33669
- }
33670
- }
33671
- function depositDigest(source) {
33672
- const canonical = JSON.stringify({
33673
- chainId: String(source?.chainId ?? ""),
33674
- tokenSymbol: String(source?.tokenSymbol ?? "").toUpperCase(),
33675
- tokenAddress: String(source?.tokenAddress ?? ""),
33676
- receiverAddress: String(source?.receiverAddress ?? ""),
33677
- receiverMemo: source?.receiverMemo ?? null,
33678
- amount: String(source?.amount ?? ""),
33679
- amountUnit: source?.amountUnit ?? null,
33680
- lnInvoice: source?.lnInvoice ?? null
33681
- });
33682
- return crypto5.createHash("sha256").update(canonical, "utf8").digest("hex");
33683
- }
33684
- function claimSend(rozoPaymentId, intent, { skipCaps = false } = {}) {
33685
- return withLock(() => {
33686
- const state = readState(rozoPaymentId);
33687
- if (!state) {
33688
- throw new SkillError(
33689
- "NO_ORDER_STATE",
33690
- "No local record for this order. Run create-order.js in this same session first."
33691
- );
33692
- }
33693
- if (state.send) {
33694
- throw new SkillError(
33695
- "ALREADY_SENT",
33696
- `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.`,
33697
- { send: state.send }
33698
- );
33699
- }
33700
- const caps = skipCaps ? null : assertPaymentLimit(state.invoiceAmount);
33701
- const next = {
33702
- ...state,
33703
- updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
33704
- send: {
33705
- status: "claimed",
33706
- claimedAt: (/* @__PURE__ */ new Date()).toISOString(),
33707
- chainId: intent.chainId,
33708
- tokenSymbol: intent.tokenSymbol,
33709
- from: intent.from,
33710
- to: intent.to,
33711
- amountAtomic: intent.amountAtomic,
33712
- memo: intent.memo ?? null,
33713
- nonceBefore: intent.nonceBefore ?? null,
33714
- expectedTxHash: intent.expectedTxHash ?? null,
33715
- txHash: null
33716
- }
33717
- };
33718
- writeAtomic(statePath(rozoPaymentId), next);
33719
- return { state: next, caps };
33720
- });
33721
- }
33722
- function recordSendResult(rozoPaymentId, { status, txHash = null, note = null }) {
33723
- return withLock(() => {
33724
- const state = readState(rozoPaymentId);
33725
- if (!state || !state.send) {
33726
- throw new SkillError("NO_SEND_CLAIM", "No send claim to update for this order.");
33727
- }
33728
- const next = {
33729
- ...state,
33730
- updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
33731
- send: {
33732
- ...state.send,
33733
- status,
33734
- txHash: txHash ?? state.send.txHash,
33735
- note,
33736
- resolvedAt: (/* @__PURE__ */ new Date()).toISOString()
33737
- }
33738
- };
33739
- writeAtomic(statePath(rozoPaymentId), next);
33740
- return next;
33741
- });
33742
- }
33743
- var MAX_PAYMENT_USD = 1100;
33744
- function assertPaymentLimit(invoiceUsd) {
33745
- if (invoiceUsd === null || invoiceUsd === void 0 || String(invoiceUsd).trim() === "") {
33746
- throw new SkillError("BAD_INVOICE_AMOUNT", "Cannot evaluate the payment limit without a USD amount.");
33747
- }
33748
- const usd = Number(invoiceUsd);
33749
- if (!Number.isFinite(usd) || usd < 0) {
33750
- throw new SkillError("BAD_INVOICE_AMOUNT", "Cannot evaluate the payment limit without a USD amount.");
33751
- }
33752
- if (usd > MAX_PAYMENT_USD) {
33753
- throw new SkillError(
33754
- "CAP_PER_TX",
33755
- `$${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.`
33756
- );
33757
- }
33758
- return { usd, limitUsd: MAX_PAYMENT_USD };
33759
- }
33760
-
33761
33827
  // scripts/src/lib/bitrefill.mjs
33762
33828
  var BITREFILL_DESTINATION = Object.freeze({ chainId: "8453", tokenSymbol: "USDC" });
33763
33829
  var BITREFILL_MIN_EXPIRY_MS = 5 * 60 * 1e3;