@rozoai/checkout 0.1.11 → 0.1.14

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.
@@ -6,7 +6,16 @@ const require = __rozoCreateRequire(import.meta.url);
6
6
  const __filename = __rozoFileURLToPath(import.meta.url);
7
7
  const __dirname = __rozoDirname(__filename);
8
8
 
9
+ // scripts/src/lib/support.mjs
10
+ var SUPPORT = Object.freeze({
11
+ email: "hi@rozo.ai",
12
+ x: "https://x.com/ROZOai",
13
+ discord: "https://discord.gg/EfWejgTbuU"
14
+ });
15
+ var SUPPORT_TEXT = `Need help? Email ${SUPPORT.email}, or reach ROZO on X ${SUPPORT.x} or Discord ${SUPPORT.discord}.`;
16
+
9
17
  // scripts/src/lib/output.mjs
18
+ var PUBLIC_SUPPORT_URLS = /* @__PURE__ */ new Set([SUPPORT.x, SUPPORT.discord]);
10
19
  var EXIT_OK = 0;
11
20
  var EXIT_ERROR = 1;
12
21
  var EXIT_USAGE = 2;
@@ -18,7 +27,8 @@ function redact(text) {
18
27
  s = s.replace(/\b[0-9a-fA-F]{64}\b/g, "<redacted>");
19
28
  s = s.replace(/\b[1-9A-HJ-NP-Za-km-z]{80,90}\b/g, "<redacted>");
20
29
  s = s.replace(/\[(?:\s*\d{1,3}\s*,){40,}\s*\d{1,3}\s*\]/g, "[<redacted>]");
21
- s = s.replace(/\b([a-zA-Z][a-zA-Z0-9+.-]*):\/\/([^\s"'<>]+)/g, (_m, scheme, rest) => {
30
+ s = s.replace(/\b([a-zA-Z][a-zA-Z0-9+.-]*):\/\/([^\s"'<>]+)/g, (m, scheme, rest) => {
31
+ if (PUBLIC_SUPPORT_URLS.has(m.replace(/[.,;:!?)]+$/, ""))) return m;
22
32
  const withoutUserinfo = rest.includes("@") ? rest.slice(rest.indexOf("@") + 1) : rest;
23
33
  const host = withoutUserinfo.split(/[/?#]/)[0];
24
34
  const hadMore = withoutUserinfo.length > host.length;
@@ -156,7 +166,7 @@ async function request(method, url, { body, timeoutMs = DEFAULT_TIMEOUT_MS } = {
156
166
  }
157
167
  }
158
168
  if (!res.ok) {
159
- const code = json?.code || json?.error?.code || (typeof json?.error === "string" ? null : null) || `HTTP_${res.status}`;
169
+ 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}`;
160
170
  const message = json?.message || (typeof json?.error === "string" ? json.error : json?.error?.message) || `HTTP ${res.status}`;
161
171
  throw new SkillError(code, redact(String(message)), {
162
172
  httpStatus: res.status,
@@ -342,7 +352,15 @@ function receiptSignal(source) {
342
352
  return { money: true, receipt: null, unparsable: true };
343
353
  }
344
354
  }
345
- function classifyStatus({ payment, routerState, coinbase, now = Date.now(), viewsFailed = false }) {
355
+ function classifyStatus({
356
+ payment,
357
+ routerState,
358
+ coinbase,
359
+ now = Date.now(),
360
+ viewsFailed = false,
361
+ provider = "coinbase"
362
+ }) {
363
+ const bitrefill = provider === "bitrefill";
346
364
  const source = payment?.source || {};
347
365
  const hasTx = Boolean(source.txHash);
348
366
  const confirmed = Boolean(source.confirmedAt);
@@ -375,7 +393,7 @@ function classifyStatus({ payment, routerState, coinbase, now = Date.now(), view
375
393
  { escalate: true }
376
394
  );
377
395
  }
378
- if (routerStatus === "paid" || coinbase?.settled === true) {
396
+ if (!bitrefill && (routerStatus === "paid" || coinbase?.settled === true)) {
379
397
  return mk("settled", "Coinbase invoice settled by the funder wallet.", { terminal: true });
380
398
  }
381
399
  if (routerStatus === "failed_pay_invoice" || routerStatus === "failed_insufficient_balance") {
@@ -397,6 +415,17 @@ function classifyStatus({ payment, routerState, coinbase, now = Date.now(), view
397
415
  escalate: true
398
416
  });
399
417
  }
418
+ if (bitrefill) {
419
+ const payoutTx = Boolean(payment?.destination?.txHash);
420
+ if (payment?.status === "payment_payout_completed" || payment?.status === "payment_completed" && payoutTx) {
421
+ return mk("settled", "USDC delivered to the Bitrefill invoice address on Base.", {
422
+ terminal: true
423
+ });
424
+ }
425
+ if (payment?.status === "payment_completed") {
426
+ return mk("bridging", "Payment completed but no payout transaction is visible yet. Keep polling.");
427
+ }
428
+ }
400
429
  switch (payment?.status) {
401
430
  case "payment_unpaid": {
402
431
  if (moneyDetected) {
@@ -471,6 +500,19 @@ function formatRemaining(ms) {
471
500
  const m = totalMinutes % 60;
472
501
  return m ? `${h}h ${m}m` : `${h}h`;
473
502
  }
503
+ function parseDeadline(value) {
504
+ if (value === null || value === void 0 || value === "") return null;
505
+ if (typeof value === "number" && Number.isFinite(value)) {
506
+ return value < 1e12 ? Math.round(value * 1e3) : Math.round(value);
507
+ }
508
+ const s = String(value).trim();
509
+ if (/^\d+$/.test(s)) {
510
+ const n = Number(s);
511
+ return n < 1e12 ? n * 1e3 : n;
512
+ }
513
+ const t = Date.parse(s);
514
+ return Number.isFinite(t) ? t : null;
515
+ }
474
516
 
475
517
  // scripts/src/lib/state.mjs
476
518
  import fs from "node:fs";
@@ -479,6 +521,30 @@ import os from "node:os";
479
521
  function stateRoot() {
480
522
  return process.env.ROZO_CHECKOUT_STATE_DIR || path.join(os.homedir(), ".rozo-checkout", "state");
481
523
  }
524
+ function statePath(rozoPaymentId) {
525
+ if (!/^[A-Za-z0-9-]{8,64}$/.test(String(rozoPaymentId || ""))) {
526
+ throw new SkillError("BAD_ROZO_PAYMENT_ID", "Refusing to build a state path from that id.");
527
+ }
528
+ return path.join(stateRoot(), `${rozoPaymentId}.json`);
529
+ }
530
+ function readState(rozoPaymentId) {
531
+ const file = statePath(rozoPaymentId);
532
+ let raw;
533
+ try {
534
+ raw = fs.readFileSync(file, "utf8");
535
+ } catch (err) {
536
+ if (err.code === "ENOENT") return null;
537
+ throw new SkillError("STATE_UNREADABLE", `Cannot read local state: ${err.code}`);
538
+ }
539
+ try {
540
+ return JSON.parse(raw);
541
+ } catch {
542
+ throw new SkillError(
543
+ "STATE_CORRUPT",
544
+ "The local state file for this order is corrupt. Refusing to act; inspect it manually."
545
+ );
546
+ }
547
+ }
482
548
  function findByLinkId(linkId) {
483
549
  const dir = stateRoot();
484
550
  let files = [];
@@ -499,16 +565,61 @@ function findByLinkId(linkId) {
499
565
  return best;
500
566
  }
501
567
 
568
+ // scripts/src/lib/bitrefill.mjs
569
+ var PROVIDER_BITREFILL = "bitrefill";
570
+ var BITREFILL_DESTINATION = Object.freeze({ chainId: "8453", tokenSymbol: "USDC" });
571
+ var BITREFILL_MIN_EXPIRY_MS = 5 * 60 * 1e3;
572
+ var BITREFILL_MIN_PAY_WINDOW_MS = 2 * 60 * 1e3;
573
+ var BITREFILL_MAX_EXPIRY_AHEAD_MS = 30 * 60 * 1e3;
574
+ function providerFromPayment(payment) {
575
+ if (!payment) return null;
576
+ if (String(payment.orderId ?? payment.order_id ?? "").startsWith("bitrefill_")) return PROVIDER_BITREFILL;
577
+ const meta = payment.metadata?.provider;
578
+ if (meta) return String(meta).toLowerCase();
579
+ return null;
580
+ }
581
+ function earliestExpiry(...values) {
582
+ const ms = values.map((v) => parseDeadline(v)).filter((v) => v !== null);
583
+ return ms.length ? new Date(Math.min(...ms)).toISOString() : null;
584
+ }
585
+ function intentBitrefillExpiry(payment) {
586
+ return payment?.metadata?.bitrefillExpiresAt ?? null;
587
+ }
588
+
502
589
  // scripts/src/status.mjs
503
590
  var POLL_INTERVAL_MS = 1e4;
504
591
  var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
505
- async function snapshot({ rozoPaymentId, linkId }) {
592
+ function resolveProvider(explicit, rozoPaymentId) {
593
+ if (explicit) return String(explicit).toLowerCase();
594
+ if (rozoPaymentId && isRozoPaymentId(rozoPaymentId)) {
595
+ try {
596
+ return readState(rozoPaymentId)?.provider ?? null;
597
+ } catch {
598
+ return null;
599
+ }
600
+ }
601
+ return null;
602
+ }
603
+ async function snapshot({ rozoPaymentId, linkId, provider: explicitProvider }) {
604
+ let provider = resolveProvider(explicitProvider, rozoPaymentId);
605
+ let prefetched = null;
606
+ if (!provider && rozoPaymentId && isRozoPaymentId(rozoPaymentId)) {
607
+ try {
608
+ prefetched = await getPayment(rozoPaymentId);
609
+ provider = providerFromPayment(prefetched);
610
+ } catch {
611
+ prefetched = null;
612
+ }
613
+ }
614
+ provider = provider === "bitrefill" ? "bitrefill" : "coinbase";
506
615
  let status = null;
507
616
  let statusError = null;
508
- try {
509
- status = await invoiceStatus({ linkId, rozoPaymentId });
510
- } catch (err) {
511
- statusError = { code: err.code, message: err.message };
617
+ if (provider !== "bitrefill") {
618
+ try {
619
+ status = await invoiceStatus({ linkId, rozoPaymentId });
620
+ } catch (err) {
621
+ statusError = { code: err.code, message: err.message };
622
+ }
512
623
  }
513
624
  let id = rozoPaymentId || status?.rozo_payment_id || null;
514
625
  let idSource = rozoPaymentId ? "argument" : status?.rozo_payment_id ? "invoice-status" : null;
@@ -521,33 +632,61 @@ async function snapshot({ rozoPaymentId, linkId }) {
521
632
  }
522
633
  let payment = null;
523
634
  let paymentError = null;
524
- if (id && isRozoPaymentId(id)) {
635
+ if (prefetched && id === rozoPaymentId) {
636
+ payment = prefetched;
637
+ } else if (id && isRozoPaymentId(id)) {
525
638
  try {
526
639
  payment = await getPayment(id);
527
640
  } catch (err) {
528
641
  paymentError = { code: err.code, message: err.message };
529
642
  }
530
643
  }
531
- const viewsFailed = Boolean(statusError) && !payment;
644
+ const viewsFailed = provider === "bitrefill" ? !payment : Boolean(statusError) && !payment;
532
645
  const verdict = classifyStatus({
533
646
  payment: payment || status?.rozoPayment || {},
534
647
  routerState: status?.routerState,
535
648
  coinbase: status?.coinbase,
536
- viewsFailed
649
+ viewsFailed,
650
+ provider
537
651
  });
538
652
  const source = payment?.source || status?.rozoPayment?.source || {};
653
+ let bitrefillExpiry = null;
654
+ let state = verdict.state;
655
+ let terminal = verdict.terminal;
656
+ let detail = verdict.detail;
657
+ if (provider === "bitrefill") {
658
+ let local = null;
659
+ try {
660
+ local = id && isRozoPaymentId(id) ? readState(id) : null;
661
+ } catch {
662
+ local = null;
663
+ }
664
+ bitrefillExpiry = earliestExpiry(local?.bitrefill?.expiresAt, intentBitrefillExpiry(payment));
665
+ if (!verdict.moneyDetected && ["awaiting_deposit", "expired_unfunded"].includes(verdict.state)) {
666
+ const ms = bitrefillExpiry ? Date.parse(bitrefillExpiry) : NaN;
667
+ if (Number.isFinite(ms) && ms <= Date.now()) {
668
+ state = "invoice_expired";
669
+ terminal = true;
670
+ detail = "The Bitrefill invoice has expired. Do NOT fund this order; create a fresh invoice.";
671
+ } else if (!bitrefillExpiry && verdict.state === "awaiting_deposit") {
672
+ state = "deadline_unknown";
673
+ detail = "The Bitrefill invoice deadline is unknown on this machine, so this order is NOT shown as payable. Re-run pay with the invoice details, or create a fresh invoice.";
674
+ }
675
+ }
676
+ }
539
677
  return {
678
+ provider,
540
679
  rozoPaymentId: id,
541
680
  rozoPaymentIdSource: idSource,
542
681
  // Without the authoritative payment object we are reading a partial view.
543
682
  authoritativeView: Boolean(payment),
544
683
  linkId: linkId || status?.pl_id || null,
545
- state: verdict.state,
684
+ state,
546
685
  unknown: verdict.unknown,
547
686
  moneyDetected: verdict.moneyDetected,
548
- terminal: verdict.terminal,
687
+ terminal,
549
688
  escalate: verdict.escalate,
550
- detail: verdict.detail,
689
+ detail,
551
690
  backend: {
552
691
  paymentStatus: payment?.status ?? status?.rozoPayment?.status ?? null,
553
692
  routerStatus: verdict.routerStatus,
@@ -564,7 +703,10 @@ async function snapshot({ rozoPaymentId, linkId }) {
564
703
  chain: source.chainId ? chainName(source.chainId) : null
565
704
  },
566
705
  expiry: (() => {
567
- const iso = payment?.expiresAt ?? status?.rozoPayment?.expiresAt ?? null;
706
+ if (provider === "bitrefill" && !bitrefillExpiry) {
707
+ return { expiresAt: null, expiresIn: null, msRemaining: null, deadlineUnknown: true };
708
+ }
709
+ const iso = provider === "bitrefill" ? bitrefillExpiry : payment?.expiresAt ?? status?.rozoPayment?.expiresAt ?? null;
568
710
  if (!iso) return { expiresAt: null, expiresIn: null, msRemaining: null };
569
711
  const ms = Date.parse(iso) - Date.now();
570
712
  return {
@@ -584,21 +726,28 @@ async function main(argv) {
584
726
  const args = parseArgs(argv);
585
727
  const rozoPaymentId = args["rozo-payment-id"] || (isRozoPaymentId(args._[0]) ? args._[0] : null);
586
728
  const linkId = args["link-id"] || (!rozoPaymentId ? args._[0] : null);
729
+ const provider = args.provider ? String(args.provider) : null;
730
+ if (provider && provider !== "bitrefill" && provider !== "coinbase") {
731
+ usage("--provider must be coinbase or bitrefill");
732
+ }
733
+ if (provider === "bitrefill" && !rozoPaymentId) {
734
+ usage("A Bitrefill order is tracked by --rozo-payment-id <uuid>.");
735
+ }
587
736
  if (!rozoPaymentId && !linkId) {
588
737
  usage("Required: --rozo-payment-id <uuid> and/or --link-id <pl_* | paymentSession_*>");
589
738
  }
590
739
  const watch = Boolean(args.watch);
591
740
  const timeoutMs = Math.max(0, Number(args.timeout ?? 600) * 1e3);
592
741
  const deadline = Date.now() + timeoutMs;
593
- let result = await snapshot({ rozoPaymentId, linkId });
742
+ let result = await snapshot({ rozoPaymentId, linkId, provider });
594
743
  const history = [{ at: (/* @__PURE__ */ new Date()).toISOString(), state: result.state }];
595
744
  while (watch && !result.terminal && !result.escalate && !result.unknown && Date.now() < deadline) {
596
745
  await sleep(POLL_INTERVAL_MS);
597
- const next = await snapshot({ rozoPaymentId: result.rozoPaymentId || rozoPaymentId, linkId });
746
+ const next = await snapshot({ rozoPaymentId: result.rozoPaymentId || rozoPaymentId, linkId, provider });
598
747
  if (next.state !== result.state) history.push({ at: (/* @__PURE__ */ new Date()).toISOString(), state: next.state });
599
748
  result = next;
600
749
  }
601
- const guidance = result.escalate ? "MONEY DETECTED and the order is not on a healthy path. Do NOT pay again and do NOT create a new order for this link. Preserve linkId, rozoPaymentId and every tx hash, then escalate to the operator for manual reconciliation." : result.unknown ? "The order state could not be established. This is NOT evidence that nothing was paid \u2014 do not create a new order and do not send again on the strength of it. Retry, or pass --rozo-payment-id so the authoritative pay-in view can be read." : !result.authoritativeView ? "Only the fulfilment view was readable; the pay-in view is unavailable, so the money-detected rule cannot be enforced. Pass --rozo-payment-id for a complete answer." : result.state === "expired_unfunded" ? "Nothing was funded, so nothing was lost. Start a fresh order with: rozo-checkout pay <coinbase-link> --with <coin> (or create-order.js --url <link> --chain <id> --token <SYMBOL>)" : result.terminal ? "Done." : "Still in flight. Poll again in ~10s.";
750
+ const guidance = result.escalate ? "MONEY DETECTED and the order is not on a healthy path. Do NOT pay again and do NOT create a new order for this link. Preserve linkId, rozoPaymentId and every tx hash, then escalate to the operator for manual reconciliation." : result.unknown ? "The order state could not be established. This is NOT evidence that nothing was paid \u2014 do not create a new order and do not send again on the strength of it. Retry, or pass --rozo-payment-id so the authoritative pay-in view can be read." : !result.authoritativeView ? "Only the fulfilment view was readable; the pay-in view is unavailable, so the money-detected rule cannot be enforced. Pass --rozo-payment-id for a complete answer." : (result.state === "expired_unfunded" || result.state === "invoice_expired") && result.provider === "bitrefill" ? "Nothing was funded, so nothing was lost. Create a fresh Bitrefill invoice and run rozo-checkout pay --bitrefill-invoice <id> --to <0x\u2026> --amount <USDC> --with <coin>" : result.state === "expired_unfunded" ? "Nothing was funded, so nothing was lost. Start a fresh order with: rozo-checkout pay <coinbase-link> --with <coin> (or create-order.js --url <link> --chain <id> --token <SYMBOL>)" : result.terminal ? "Done." : "Still in flight. Poll again in ~10s.";
602
751
  const unresolved = watch && !result.terminal && !result.escalate && !result.unknown;
603
752
  const failed = result.escalate || result.unknown || !result.authoritativeView;
604
753
  emit(
@@ -608,7 +757,9 @@ async function main(argv) {
608
757
  ...result,
609
758
  history,
610
759
  guidance,
611
- timedOut: unresolved
760
+ timedOut: unresolved,
761
+ // Anything short of a clean answer gets a human contact.
762
+ ...failed || unresolved ? { support: SUPPORT } : {}
612
763
  },
613
764
  failed ? EXIT_ERROR : unresolved ? EXIT_UNCONFIRMED : 0
614
765
  );