@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.
@@ -13,6 +13,8 @@ var SUPPORT = Object.freeze({
13
13
  discord: "https://discord.gg/EfWejgTbuU"
14
14
  });
15
15
  var SUPPORT_TEXT = `Need help? Email ${SUPPORT.email}, or reach ROZO on X ${SUPPORT.x} or Discord ${SUPPORT.discord}.`;
16
+ var BULK_DOCS_URL = "https://docs.rozo.ai/products/checkout/bulk-and-agents";
17
+ var BULK_HINT_TEXT = `Buying regularly or for others? ${BULK_DOCS_URL}`;
16
18
  var CONTACT_EMAIL_MAX_LENGTH = 254;
17
19
  var EMAIL_RE = /^[a-z0-9][^\s@]*@[^\s@.]+(\.[^\s@.]+)+$/;
18
20
  function normalizeContactEmail(raw) {
@@ -198,18 +200,66 @@ function maskMemo(memo) {
198
200
  return `${s.slice(0, 4)}...${s.slice(-3)}`;
199
201
  }
200
202
 
201
- // scripts/src/lib/api.mjs
203
+ // scripts/src/lib/version.mjs
202
204
  import { createRequire } from "node:module";
205
+ var PKG_VERSION = (() => {
206
+ const requireFrom = createRequire(import.meta.url);
207
+ for (const candidate of ["../../package.json", "../../../package.json"]) {
208
+ try {
209
+ const pkg = requireFrom(candidate);
210
+ if (pkg?.name === "@rozoai/checkout" && pkg.version) return pkg.version;
211
+ } catch {
212
+ }
213
+ }
214
+ return "0.0.0";
215
+ })();
203
216
 
204
217
  // scripts/src/lib/http.mjs
205
218
  var DEFAULT_TIMEOUT_MS = 2e4;
206
- var USER_AGENT = "rozo-checkout-skill/1.0";
207
- async function request(method, url, { body, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
219
+ var USER_AGENT = `rozo-checkout-skill/${PKG_VERSION}`;
220
+ var MAX_RETRIES = 3;
221
+ var MAX_TOTAL_WAIT_MS = 12e4;
222
+ var FALLBACK_BACKOFF_MS = [2e3, 4e3, 8e3];
223
+ var defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
224
+ function parseRetryAfter(value, now = Date.now()) {
225
+ if (value === null || value === void 0) return null;
226
+ const s = String(value).trim();
227
+ if (!s) return null;
228
+ if (/^\d+(\.\d+)?$/.test(s)) return Math.round(Number(s) * 1e3);
229
+ const at = Date.parse(s);
230
+ if (!Number.isFinite(at)) return null;
231
+ return Math.max(0, at - now);
232
+ }
233
+ function bodyRetryAfterMs(json) {
234
+ const raw = json?.retryAfterSeconds ?? json?.error?.retryAfterSeconds;
235
+ if (raw === null || raw === void 0 || raw === "") return null;
236
+ const n = Number(raw);
237
+ return Number.isFinite(n) && n >= 0 ? Math.round(n * 1e3) : null;
238
+ }
239
+ function rateLimitInfo(res) {
240
+ const pick = (name) => res.headers?.get?.(name) ?? null;
241
+ const info = {
242
+ limit: pick("x-ratelimit-limit"),
243
+ remaining: pick("x-ratelimit-remaining"),
244
+ tier: pick("x-ratelimit-tier"),
245
+ scope: pick("x-ratelimit-scope")
246
+ };
247
+ const present = Object.fromEntries(Object.entries(info).filter(([, v]) => v !== null));
248
+ return Object.keys(present).length ? present : null;
249
+ }
250
+ function parseBody(text) {
251
+ if (!text) return null;
252
+ try {
253
+ return JSON.parse(text);
254
+ } catch {
255
+ return null;
256
+ }
257
+ }
258
+ async function fetchOnce(method, url, body, timeoutMs) {
208
259
  const controller = new AbortController();
209
260
  const timer = setTimeout(() => controller.abort(), timeoutMs);
210
- let res;
211
261
  try {
212
- res = await fetch(url, {
262
+ const res = await fetch(url, {
213
263
  method,
214
264
  headers: {
215
265
  accept: "application/json",
@@ -219,6 +269,8 @@ async function request(method, url, { body, timeoutMs = DEFAULT_TIMEOUT_MS } = {
219
269
  body: body !== void 0 ? JSON.stringify(body) : void 0,
220
270
  signal: controller.signal
221
271
  });
272
+ const text = await res.text();
273
+ return { res, text };
222
274
  } catch (err) {
223
275
  throw new SkillError(
224
276
  err?.name === "AbortError" ? "HTTP_TIMEOUT" : "HTTP_UNREACHABLE",
@@ -228,31 +280,54 @@ async function request(method, url, { body, timeoutMs = DEFAULT_TIMEOUT_MS } = {
228
280
  } finally {
229
281
  clearTimeout(timer);
230
282
  }
231
- const text = await res.text();
232
- let json = null;
233
- if (text) {
234
- try {
235
- json = JSON.parse(text);
236
- } catch {
237
- json = null;
283
+ }
284
+ async function request(method, url, {
285
+ body,
286
+ timeoutMs = DEFAULT_TIMEOUT_MS,
287
+ sleep = defaultSleep,
288
+ now = Date.now,
289
+ maxTotalWaitMs = MAX_TOTAL_WAIT_MS
290
+ } = {}) {
291
+ let waitedMs = 0;
292
+ let retries = 0;
293
+ for (; ; ) {
294
+ const { res, text } = await fetchOnce(method, url, body, timeoutMs);
295
+ const json = parseBody(text);
296
+ if (res.ok) {
297
+ if (json === null) {
298
+ throw new SkillError("HTTP_BAD_JSON", "Endpoint returned a non-JSON body.", {
299
+ url: redactUrl(url),
300
+ snippet: redact(text).slice(0, 400)
301
+ });
302
+ }
303
+ return json;
304
+ }
305
+ const headerWaitMs = parseRetryAfter(res.headers?.get?.("retry-after"), now());
306
+ const hintMs = headerWaitMs ?? bodyRetryAfterMs(json);
307
+ const retryable = res.status === 429 || res.status === 503 && hintMs !== null;
308
+ if (retryable && retries < MAX_RETRIES) {
309
+ const waitMs = hintMs ?? FALLBACK_BACKOFF_MS[retries];
310
+ if (waitedMs + waitMs <= Math.min(maxTotalWaitMs, MAX_TOTAL_WAIT_MS)) {
311
+ retries += 1;
312
+ waitedMs += waitMs;
313
+ await sleep(waitMs);
314
+ continue;
315
+ }
238
316
  }
239
- }
240
- if (!res.ok) {
241
317
  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}`;
242
318
  const message = json?.message || (typeof json?.error === "string" ? json.error : json?.error?.message) || `HTTP ${res.status}`;
319
+ const rateLimit = rateLimitInfo(res);
243
320
  throw new SkillError(code, redact(String(message)), {
244
321
  httpStatus: res.status,
245
322
  url: redactUrl(url),
246
- body: json ?? redact(text).slice(0, 800)
323
+ body: json ?? redact(text).slice(0, 800),
324
+ ...retryable ? {
325
+ retries,
326
+ retryAfterSeconds: hintMs === null ? null : Math.ceil(hintMs / 1e3)
327
+ } : {},
328
+ ...rateLimit ? { rateLimit } : {}
247
329
  });
248
330
  }
249
- if (json === null) {
250
- throw new SkillError("HTTP_BAD_JSON", "Endpoint returned a non-JSON body.", {
251
- url: redactUrl(url),
252
- snippet: redact(text).slice(0, 400)
253
- });
254
- }
255
- return json;
256
331
  }
257
332
  function redactUrl(url) {
258
333
  try {
@@ -277,31 +352,331 @@ function normalizeUtmSource(value) {
277
352
  return s;
278
353
  }
279
354
 
280
- // scripts/src/lib/api.mjs
281
- var PKG_VERSION = (() => {
282
- const requireFrom = createRequire(import.meta.url);
283
- for (const candidate of ["../../package.json", "../../../package.json"]) {
355
+ // scripts/src/lib/identity.mjs
356
+ import crypto2 from "node:crypto";
357
+
358
+ // scripts/src/lib/prefs.mjs
359
+ import fs2 from "node:fs";
360
+ import path2 from "node:path";
361
+ import os2 from "node:os";
362
+
363
+ // scripts/src/lib/state.mjs
364
+ import fs from "node:fs";
365
+ import path from "node:path";
366
+ import os from "node:os";
367
+ import crypto from "node:crypto";
368
+ var LOCK_STALE_MS = 6e4;
369
+ var LOCK_WAIT_MS = 1e4;
370
+ var LOCK_POLL_MS = 25;
371
+ function lockPath() {
372
+ return path.join(stateRoot(), ".send.lock");
373
+ }
374
+ function sleepSync(ms) {
375
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
376
+ }
377
+ function tryAcquire(file) {
378
+ try {
379
+ const fd = fs.openSync(file, "wx", 384);
380
+ fs.writeFileSync(fd, JSON.stringify({ pid: process.pid, at: (/* @__PURE__ */ new Date()).toISOString() }));
381
+ fs.closeSync(fd);
382
+ return true;
383
+ } catch (err) {
384
+ if (err.code !== "EEXIST") throw err;
385
+ return false;
386
+ }
387
+ }
388
+ function withLock(fn) {
389
+ const file = lockPath();
390
+ fs.mkdirSync(path.dirname(file), { recursive: true, mode: 448 });
391
+ const deadline = Date.now() + LOCK_WAIT_MS;
392
+ for (; ; ) {
393
+ if (tryAcquire(file)) break;
284
394
  try {
285
- const pkg = requireFrom(candidate);
286
- if (pkg?.name === "@rozoai/checkout" && pkg.version) return pkg.version;
395
+ const age = Date.now() - fs.statSync(file).mtimeMs;
396
+ if (age > LOCK_STALE_MS) {
397
+ const stolen = `${file}.stale.${crypto.randomBytes(4).toString("hex")}`;
398
+ try {
399
+ fs.renameSync(file, stolen);
400
+ fs.unlinkSync(stolen);
401
+ } catch {
402
+ }
403
+ continue;
404
+ }
405
+ } catch (err) {
406
+ if (err.code !== "ENOENT") throw err;
407
+ continue;
408
+ }
409
+ if (Date.now() > deadline) {
410
+ throw new SkillError(
411
+ "LOCK_TIMEOUT",
412
+ "Another rozo-checkout process is holding the send lock. Refusing to proceed rather than risk a concurrent second send."
413
+ );
414
+ }
415
+ sleepSync(LOCK_POLL_MS);
416
+ }
417
+ try {
418
+ return fn();
419
+ } finally {
420
+ try {
421
+ fs.unlinkSync(file);
287
422
  } catch {
288
423
  }
289
424
  }
290
- return "0.0.0";
291
- })();
425
+ }
426
+ function stateRoot() {
427
+ return process.env.ROZO_CHECKOUT_STATE_DIR || path.join(os.homedir(), ".rozo-checkout", "state");
428
+ }
429
+ function statePath(rozoPaymentId) {
430
+ if (!/^[A-Za-z0-9-]{8,64}$/.test(String(rozoPaymentId || ""))) {
431
+ throw new SkillError("BAD_ROZO_PAYMENT_ID", "Refusing to build a state path from that id.");
432
+ }
433
+ return path.join(stateRoot(), `${rozoPaymentId}.json`);
434
+ }
435
+ function writeAtomic(file, data) {
436
+ const dir = path.dirname(file);
437
+ fs.mkdirSync(dir, { recursive: true, mode: 448 });
438
+ const tmp = path.join(dir, `.${path.basename(file)}.${crypto.randomBytes(6).toString("hex")}.tmp`);
439
+ const fd = fs.openSync(tmp, "wx", 384);
440
+ try {
441
+ fs.writeFileSync(fd, JSON.stringify(data, null, 2) + "\n", "utf8");
442
+ fs.fsyncSync(fd);
443
+ } finally {
444
+ fs.closeSync(fd);
445
+ }
446
+ fs.renameSync(tmp, file);
447
+ }
448
+ function readState(rozoPaymentId) {
449
+ const file = statePath(rozoPaymentId);
450
+ let raw;
451
+ try {
452
+ raw = fs.readFileSync(file, "utf8");
453
+ } catch (err) {
454
+ if (err.code === "ENOENT") return null;
455
+ throw new SkillError("STATE_UNREADABLE", `Cannot read local state: ${err.code}`);
456
+ }
457
+ try {
458
+ return JSON.parse(raw);
459
+ } catch {
460
+ throw new SkillError(
461
+ "STATE_CORRUPT",
462
+ "The local state file for this order is corrupt. Refusing to act; inspect it manually."
463
+ );
464
+ }
465
+ }
466
+ function createOrderRecord(record) {
467
+ return withLock(() => createOrderRecordUnlocked(record));
468
+ }
469
+ function mergeBitrefill(prev, next) {
470
+ if (!next) return prev ?? null;
471
+ if (!prev?.expiresAt || !next.expiresAt) return { ...next, expiresAt: next.expiresAt ?? prev?.expiresAt ?? null };
472
+ const a = Date.parse(prev.expiresAt);
473
+ const b = Date.parse(next.expiresAt);
474
+ return { ...next, expiresAt: Number.isFinite(a) && (!Number.isFinite(b) || a < b) ? prev.expiresAt : next.expiresAt };
475
+ }
476
+ function createOrderRecordUnlocked(record) {
477
+ const { rozoPaymentId } = record;
478
+ const existing = readState(rozoPaymentId);
479
+ const next = {
480
+ version: 1,
481
+ rozoPaymentId,
482
+ // 'coinbase' (default, historical records) or 'bitrefill'. Drives which
483
+ // payability checks presend and status run.
484
+ provider: record.provider ?? existing?.provider ?? "coinbase",
485
+ bitrefill: mergeBitrefill(existing?.bitrefill, record.bitrefill),
486
+ linkId: record.linkId,
487
+ paymentLink: record.paymentLink ?? null,
488
+ merchant: record.merchant ?? null,
489
+ invoiceAmount: record.invoiceAmount ?? null,
490
+ source: record.source,
491
+ receiverAddress: record.receiverAddress,
492
+ receiverMemo: record.receiverMemo ?? null,
493
+ amount: record.amount,
494
+ amountUnit: record.amountUnit ?? null,
495
+ expiresAt: record.expiresAt ?? null,
496
+ createdAt: existing?.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
497
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
498
+ confirmation: existing?.confirmation ?? null,
499
+ send: existing?.send ?? null
500
+ };
501
+ writeAtomic(statePath(rozoPaymentId), next);
502
+ return next;
503
+ }
504
+ function depositDigest(source) {
505
+ const canonical = JSON.stringify({
506
+ chainId: String(source?.chainId ?? ""),
507
+ tokenSymbol: String(source?.tokenSymbol ?? "").toUpperCase(),
508
+ tokenAddress: String(source?.tokenAddress ?? ""),
509
+ receiverAddress: String(source?.receiverAddress ?? ""),
510
+ receiverMemo: source?.receiverMemo ?? null,
511
+ amount: String(source?.amount ?? ""),
512
+ amountUnit: source?.amountUnit ?? null,
513
+ lnInvoice: source?.lnInvoice ?? null
514
+ });
515
+ return crypto.createHash("sha256").update(canonical, "utf8").digest("hex");
516
+ }
517
+ function recordConfirmation(rozoPaymentId, { source, invoiceAmount, tier }) {
518
+ return withLock(() => {
519
+ const state = readState(rozoPaymentId);
520
+ if (!state) {
521
+ throw new SkillError("NO_ORDER_STATE", "Cannot confirm an order with no local record.");
522
+ }
523
+ const next = {
524
+ ...state,
525
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
526
+ confirmation: {
527
+ confirmedAt: (/* @__PURE__ */ new Date()).toISOString(),
528
+ depositDigest: depositDigest(source),
529
+ invoiceAmount: invoiceAmount ?? state.invoiceAmount ?? null,
530
+ tier: tier ?? null
531
+ }
532
+ };
533
+ writeAtomic(statePath(rozoPaymentId), next);
534
+ return next;
535
+ });
536
+ }
537
+
538
+ // scripts/src/lib/prefs.mjs
539
+ function prefsPath() {
540
+ const root = process.env.ROZO_CHECKOUT_STATE_DIR ? path2.dirname(stateRoot()) : path2.join(os2.homedir(), ".rozo-checkout");
541
+ return path2.join(root, "prefs.json");
542
+ }
543
+ var ALLOWED = ["lastPayerAddress", "lastAddressFamily", "lastPreset", "installId", "updatedAt"];
544
+ var INSTALL_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
545
+ var LOCK_WAIT_MS2 = 2e3;
546
+ var LOCK_STALE_MS2 = 1e4;
547
+ function sleepSync2(ms) {
548
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
549
+ }
550
+ function withPrefsLock(fn) {
551
+ const lock = `${prefsPath()}.lock`;
552
+ fs2.mkdirSync(path2.dirname(lock), { recursive: true, mode: 448 });
553
+ const deadline = Date.now() + LOCK_WAIT_MS2;
554
+ for (; ; ) {
555
+ try {
556
+ fs2.closeSync(fs2.openSync(lock, "wx", 384));
557
+ break;
558
+ } catch (err) {
559
+ if (err.code !== "EEXIST") throw err;
560
+ }
561
+ try {
562
+ if (Date.now() - fs2.statSync(lock).mtimeMs > LOCK_STALE_MS2) {
563
+ fs2.rmSync(lock, { force: true });
564
+ continue;
565
+ }
566
+ } catch {
567
+ continue;
568
+ }
569
+ if (Date.now() > deadline) return void 0;
570
+ sleepSync2(10);
571
+ }
572
+ try {
573
+ return fn();
574
+ } finally {
575
+ fs2.rmSync(lock, { force: true });
576
+ }
577
+ }
578
+ var USER_FIELDS = ["lastPayerAddress", "lastAddressFamily", "lastPreset"];
579
+ function readPrefs() {
580
+ let raw;
581
+ try {
582
+ raw = fs2.readFileSync(prefsPath(), "utf8");
583
+ } catch {
584
+ return null;
585
+ }
586
+ let doc;
587
+ try {
588
+ doc = JSON.parse(raw);
589
+ } catch {
590
+ return null;
591
+ }
592
+ if (!doc || typeof doc !== "object" || Array.isArray(doc)) return null;
593
+ const out = {};
594
+ for (const k of ALLOWED) {
595
+ const v = doc[k];
596
+ if (typeof v === "string" && v.trim()) out[k] = v.trim();
597
+ }
598
+ return Object.keys(out).length ? out : null;
599
+ }
600
+ function savePrefs(update, { installIdIfAbsent } = {}) {
601
+ try {
602
+ const saved = withPrefsLock(() => {
603
+ const existing = readPrefs() || {};
604
+ const next = { ...existing };
605
+ for (const k of USER_FIELDS) {
606
+ const v = update?.[k];
607
+ if (typeof v === "string" && v.trim()) next[k] = v.trim();
608
+ }
609
+ if (!INSTALL_ID_RE.test(String(existing.installId ?? "")) && INSTALL_ID_RE.test(String(installIdIfAbsent ?? ""))) {
610
+ next.installId = installIdIfAbsent;
611
+ }
612
+ next.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
613
+ writeAtomic(prefsPath(), next);
614
+ return next;
615
+ });
616
+ return saved ?? null;
617
+ } catch {
618
+ return null;
619
+ }
620
+ }
621
+
622
+ // scripts/src/lib/identity.mjs
623
+ var ACCOUNT_HASH_PREFIX = "rozo-acct-v1:";
624
+ function isValidInstallId(value) {
625
+ return typeof value === "string" && INSTALL_ID_RE.test(value);
626
+ }
627
+ function attributionDisabled(env = process.env) {
628
+ const v = String(env.ROZO_CHECKOUT_ANON_ID ?? "").trim().toLowerCase();
629
+ return ["off", "0", "false", "no"].includes(v);
630
+ }
631
+ function getOrCreateInstallId() {
632
+ const existing = readPrefs()?.installId;
633
+ if (isValidInstallId(existing)) return existing;
634
+ const saved = savePrefs({}, { installIdIfAbsent: crypto2.randomUUID().toLowerCase() });
635
+ if (isValidInstallId(saved?.installId)) return saved.installId;
636
+ const onDisk = readPrefs()?.installId;
637
+ return isValidInstallId(onDisk) ? onDisk : crypto2.randomUUID().toLowerCase();
638
+ }
639
+ function hashAccount(raw) {
640
+ return crypto2.createHash("sha256").update(`${ACCOUNT_HASH_PREFIX}${raw}`, "utf8").digest("hex");
641
+ }
642
+ function rawApiKey(env) {
643
+ const v = typeof env.OPENROUTER_API_KEY === "string" ? env.OPENROUTER_API_KEY.trim() : "";
644
+ return v && v.length <= 1024 ? v : null;
645
+ }
646
+ function attributionIdentity(env = process.env) {
647
+ if (attributionDisabled(env)) return {};
648
+ const out = {};
649
+ try {
650
+ const id = getOrCreateInstallId();
651
+ if (isValidInstallId(id)) out.install_id = id;
652
+ } catch {
653
+ }
654
+ const raw = rawApiKey(env);
655
+ if (raw) out.account_hash = hashAccount(raw);
656
+ return out;
657
+ }
658
+
659
+ // scripts/src/lib/api.mjs
292
660
  var MPP_BASE = process.env.ROZO_CHECKOUT_MPP_BASE || "https://apiserver.mpprouter.dev/v1/services/rozo-agent-api";
293
661
  var CLIENT_LABEL = `rozo-checkout-cli/${PKG_VERSION}`;
294
662
  var ATTRIBUTION_CLIENT = `rozo-checkout-skill/${PKG_VERSION}`;
295
- function buildAttribution({ utmSource } = {}) {
663
+ function buildAttribution({ utmSource, identity } = {}) {
296
664
  const raw = utmSource !== void 0 ? utmSource : process.env.ROZO_CHECKOUT_UTM_SOURCE;
297
665
  const utm_source = normalizeUtmSource(raw);
298
- return { client: ATTRIBUTION_CLIENT, ...utm_source ? { utm_source } : {} };
666
+ const ids = identity ?? attributionIdentity();
667
+ return {
668
+ client: ATTRIBUTION_CLIENT,
669
+ ...utm_source ? { utm_source } : {},
670
+ ...ids.install_id ? { install_id: ids.install_id } : {},
671
+ ...ids.account_hash ? { account_hash: ids.account_hash } : {}
672
+ };
299
673
  }
300
674
  var INTENTS_BASE = process.env.ROZO_CHECKOUT_INTENTS_BASE || "https://intentapiv4.rozo.ai/functions/v1/payment-api";
301
675
  async function quoteInvoice({ url, linkId }) {
302
676
  const body = url ? { url } : { payment_id: linkId };
303
677
  return postJson(`${MPP_BASE}/quote-invoice`, body);
304
678
  }
679
+ var QUOTE_RECEIPT_RETRY_BUDGET_MS = 2e4;
305
680
  async function createInvoice({ url, linkId, source, quoteReceipt, utmSource, email }) {
306
681
  const body = {
307
682
  ...url ? { url } : { payment_id: linkId },
@@ -313,7 +688,11 @@ async function createInvoice({ url, linkId, source, quoteReceipt, utmSource, ema
313
688
  // sent empty, when the user gave none.
314
689
  ...email ? { email } : {}
315
690
  };
316
- return postJson(`${MPP_BASE}/create-invoice`, body);
691
+ return postJson(
692
+ `${MPP_BASE}/create-invoice`,
693
+ body,
694
+ quoteReceipt ? { maxTotalWaitMs: QUOTE_RECEIPT_RETRY_BUDGET_MS } : void 0
695
+ );
317
696
  }
318
697
  async function invoiceStatus({ linkId, rozoPaymentId }) {
319
698
  const qs = new URLSearchParams();
@@ -502,6 +881,8 @@ var MARGINS_MS = {
502
881
  };
503
882
  var BOLT11_MIN_VALIDITY_MS = 10 * MINUTE;
504
883
  var DEFAULT_MARGIN_MS = 10 * MINUTE;
884
+ var DEFAULT_WATCH_MS = 10 * MINUTE;
885
+ var LIGHTNING_WATCH_CAP_MS = 60 * MINUTE;
505
886
  function formatRemaining(ms) {
506
887
  if (!Number.isFinite(ms)) return "unknown";
507
888
  if (ms <= 0) return "expired";
@@ -891,9 +1272,9 @@ function checkPayable(statusResponse, now = Date.now()) {
891
1272
  }
892
1273
 
893
1274
  // scripts/src/lib/blacklist.mjs
894
- import fs from "node:fs";
895
- import path from "node:path";
896
- import crypto from "node:crypto";
1275
+ import fs3 from "node:fs";
1276
+ import path3 from "node:path";
1277
+ import crypto3 from "node:crypto";
897
1278
  import { fileURLToPath } from "node:url";
898
1279
  var BlacklistError = class extends Error {
899
1280
  constructor(code, message) {
@@ -928,7 +1309,7 @@ function parseBlacklist(doc) {
928
1309
  }
929
1310
  addresses.push(e.address);
930
1311
  }
931
- const digest = crypto.createHash("sha256").update(JSON.stringify(addresses), "utf8").digest("hex");
1312
+ const digest = crypto3.createHash("sha256").update(JSON.stringify(addresses), "utf8").digest("hex");
932
1313
  if (typeof provenance.addressesSha256 !== "string" || !provenance.addressesSha256) {
933
1314
  throw new BlacklistError("BLACKLIST_UNAVAILABLE", "Blacklist provenance digest is missing.");
934
1315
  }
@@ -951,11 +1332,11 @@ function parseBlacklist(doc) {
951
1332
  return { entries, index, provenance, digest };
952
1333
  }
953
1334
  function candidatePaths(moduleUrl) {
954
- const here = path.dirname(fileURLToPath(moduleUrl));
1335
+ const here = path3.dirname(fileURLToPath(moduleUrl));
955
1336
  return [
956
- path.join(here, "blacklist.json"),
957
- path.join(here, "..", "src", "lib", "blacklist.json"),
958
- path.join(here, "..", "..", "src", "lib", "blacklist.json")
1337
+ path3.join(here, "blacklist.json"),
1338
+ path3.join(here, "..", "src", "lib", "blacklist.json"),
1339
+ path3.join(here, "..", "..", "src", "lib", "blacklist.json")
959
1340
  ];
960
1341
  }
961
1342
  var cached = null;
@@ -966,7 +1347,7 @@ function loadBlacklist(explicitPath) {
966
1347
  for (const p of paths) {
967
1348
  let raw;
968
1349
  try {
969
- raw = fs.readFileSync(p, "utf8");
1350
+ raw = fs3.readFileSync(p, "utf8");
970
1351
  } catch (err) {
971
1352
  lastErr = err;
972
1353
  continue;
@@ -1013,181 +1394,6 @@ function assertNotBlacklisted(targets, blacklist) {
1013
1394
  return true;
1014
1395
  }
1015
1396
 
1016
- // scripts/src/lib/state.mjs
1017
- import fs2 from "node:fs";
1018
- import path2 from "node:path";
1019
- import os from "node:os";
1020
- import crypto2 from "node:crypto";
1021
- var LOCK_STALE_MS = 6e4;
1022
- var LOCK_WAIT_MS = 1e4;
1023
- var LOCK_POLL_MS = 25;
1024
- function lockPath() {
1025
- return path2.join(stateRoot(), ".send.lock");
1026
- }
1027
- function sleepSync(ms) {
1028
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
1029
- }
1030
- function tryAcquire(file) {
1031
- try {
1032
- const fd = fs2.openSync(file, "wx", 384);
1033
- fs2.writeFileSync(fd, JSON.stringify({ pid: process.pid, at: (/* @__PURE__ */ new Date()).toISOString() }));
1034
- fs2.closeSync(fd);
1035
- return true;
1036
- } catch (err) {
1037
- if (err.code !== "EEXIST") throw err;
1038
- return false;
1039
- }
1040
- }
1041
- function withLock(fn) {
1042
- const file = lockPath();
1043
- fs2.mkdirSync(path2.dirname(file), { recursive: true, mode: 448 });
1044
- const deadline = Date.now() + LOCK_WAIT_MS;
1045
- for (; ; ) {
1046
- if (tryAcquire(file)) break;
1047
- try {
1048
- const age = Date.now() - fs2.statSync(file).mtimeMs;
1049
- if (age > LOCK_STALE_MS) {
1050
- const stolen = `${file}.stale.${crypto2.randomBytes(4).toString("hex")}`;
1051
- try {
1052
- fs2.renameSync(file, stolen);
1053
- fs2.unlinkSync(stolen);
1054
- } catch {
1055
- }
1056
- continue;
1057
- }
1058
- } catch (err) {
1059
- if (err.code !== "ENOENT") throw err;
1060
- continue;
1061
- }
1062
- if (Date.now() > deadline) {
1063
- throw new SkillError(
1064
- "LOCK_TIMEOUT",
1065
- "Another rozo-checkout process is holding the send lock. Refusing to proceed rather than risk a concurrent second send."
1066
- );
1067
- }
1068
- sleepSync(LOCK_POLL_MS);
1069
- }
1070
- try {
1071
- return fn();
1072
- } finally {
1073
- try {
1074
- fs2.unlinkSync(file);
1075
- } catch {
1076
- }
1077
- }
1078
- }
1079
- function stateRoot() {
1080
- return process.env.ROZO_CHECKOUT_STATE_DIR || path2.join(os.homedir(), ".rozo-checkout", "state");
1081
- }
1082
- function statePath(rozoPaymentId) {
1083
- if (!/^[A-Za-z0-9-]{8,64}$/.test(String(rozoPaymentId || ""))) {
1084
- throw new SkillError("BAD_ROZO_PAYMENT_ID", "Refusing to build a state path from that id.");
1085
- }
1086
- return path2.join(stateRoot(), `${rozoPaymentId}.json`);
1087
- }
1088
- function writeAtomic(file, data) {
1089
- const dir = path2.dirname(file);
1090
- fs2.mkdirSync(dir, { recursive: true, mode: 448 });
1091
- const tmp = path2.join(dir, `.${path2.basename(file)}.${crypto2.randomBytes(6).toString("hex")}.tmp`);
1092
- const fd = fs2.openSync(tmp, "wx", 384);
1093
- try {
1094
- fs2.writeFileSync(fd, JSON.stringify(data, null, 2) + "\n", "utf8");
1095
- fs2.fsyncSync(fd);
1096
- } finally {
1097
- fs2.closeSync(fd);
1098
- }
1099
- fs2.renameSync(tmp, file);
1100
- }
1101
- function readState(rozoPaymentId) {
1102
- const file = statePath(rozoPaymentId);
1103
- let raw;
1104
- try {
1105
- raw = fs2.readFileSync(file, "utf8");
1106
- } catch (err) {
1107
- if (err.code === "ENOENT") return null;
1108
- throw new SkillError("STATE_UNREADABLE", `Cannot read local state: ${err.code}`);
1109
- }
1110
- try {
1111
- return JSON.parse(raw);
1112
- } catch {
1113
- throw new SkillError(
1114
- "STATE_CORRUPT",
1115
- "The local state file for this order is corrupt. Refusing to act; inspect it manually."
1116
- );
1117
- }
1118
- }
1119
- function createOrderRecord(record) {
1120
- return withLock(() => createOrderRecordUnlocked(record));
1121
- }
1122
- function mergeBitrefill(prev, next) {
1123
- if (!next) return prev ?? null;
1124
- if (!prev?.expiresAt || !next.expiresAt) return { ...next, expiresAt: next.expiresAt ?? prev?.expiresAt ?? null };
1125
- const a = Date.parse(prev.expiresAt);
1126
- const b = Date.parse(next.expiresAt);
1127
- return { ...next, expiresAt: Number.isFinite(a) && (!Number.isFinite(b) || a < b) ? prev.expiresAt : next.expiresAt };
1128
- }
1129
- function createOrderRecordUnlocked(record) {
1130
- const { rozoPaymentId } = record;
1131
- const existing = readState(rozoPaymentId);
1132
- const next = {
1133
- version: 1,
1134
- rozoPaymentId,
1135
- // 'coinbase' (default, historical records) or 'bitrefill'. Drives which
1136
- // payability checks presend and status run.
1137
- provider: record.provider ?? existing?.provider ?? "coinbase",
1138
- bitrefill: mergeBitrefill(existing?.bitrefill, record.bitrefill),
1139
- linkId: record.linkId,
1140
- paymentLink: record.paymentLink ?? null,
1141
- merchant: record.merchant ?? null,
1142
- invoiceAmount: record.invoiceAmount ?? null,
1143
- source: record.source,
1144
- receiverAddress: record.receiverAddress,
1145
- receiverMemo: record.receiverMemo ?? null,
1146
- amount: record.amount,
1147
- amountUnit: record.amountUnit ?? null,
1148
- expiresAt: record.expiresAt ?? null,
1149
- createdAt: existing?.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
1150
- updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1151
- confirmation: existing?.confirmation ?? null,
1152
- send: existing?.send ?? null
1153
- };
1154
- writeAtomic(statePath(rozoPaymentId), next);
1155
- return next;
1156
- }
1157
- function depositDigest(source) {
1158
- const canonical = JSON.stringify({
1159
- chainId: String(source?.chainId ?? ""),
1160
- tokenSymbol: String(source?.tokenSymbol ?? "").toUpperCase(),
1161
- tokenAddress: String(source?.tokenAddress ?? ""),
1162
- receiverAddress: String(source?.receiverAddress ?? ""),
1163
- receiverMemo: source?.receiverMemo ?? null,
1164
- amount: String(source?.amount ?? ""),
1165
- amountUnit: source?.amountUnit ?? null,
1166
- lnInvoice: source?.lnInvoice ?? null
1167
- });
1168
- return crypto2.createHash("sha256").update(canonical, "utf8").digest("hex");
1169
- }
1170
- function recordConfirmation(rozoPaymentId, { source, invoiceAmount, tier }) {
1171
- return withLock(() => {
1172
- const state = readState(rozoPaymentId);
1173
- if (!state) {
1174
- throw new SkillError("NO_ORDER_STATE", "Cannot confirm an order with no local record.");
1175
- }
1176
- const next = {
1177
- ...state,
1178
- updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1179
- confirmation: {
1180
- confirmedAt: (/* @__PURE__ */ new Date()).toISOString(),
1181
- depositDigest: depositDigest(source),
1182
- invoiceAmount: invoiceAmount ?? state.invoiceAmount ?? null,
1183
- tier: tier ?? null
1184
- }
1185
- };
1186
- writeAtomic(statePath(rozoPaymentId), next);
1187
- return next;
1188
- });
1189
- }
1190
-
1191
1397
  // scripts/src/create-order.mjs
1192
1398
  function confirmTier(usdAmount) {
1193
1399
  const usd = Number(usdAmount);