@rozoai/checkout 0.1.10 → 0.1.12

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.
@@ -116,6 +116,9 @@ function maskAddress(address) {
116
116
  return `${s.slice(0, 6)}...${s.slice(-4)}`;
117
117
  }
118
118
 
119
+ // scripts/src/lib/api.mjs
120
+ import { createRequire } from "node:module";
121
+
119
122
  // scripts/src/lib/http.mjs
120
123
  var DEFAULT_TIMEOUT_MS = 2e4;
121
124
  var USER_AGENT = "rozo-checkout-skill/1.0";
@@ -153,7 +156,7 @@ async function request(method, url, { body, timeoutMs = DEFAULT_TIMEOUT_MS } = {
153
156
  }
154
157
  }
155
158
  if (!res.ok) {
156
- const code = json?.code || json?.error?.code || (typeof json?.error === "string" ? null : null) || `HTTP_${res.status}`;
159
+ 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}`;
157
160
  const message = json?.message || (typeof json?.error === "string" ? json.error : json?.error?.message) || `HTTP ${res.status}`;
158
161
  throw new SkillError(code, redact(String(message)), {
159
162
  httpStatus: res.status,
@@ -182,7 +185,20 @@ function getJson(url, opts) {
182
185
  }
183
186
 
184
187
  // scripts/src/lib/api.mjs
188
+ var PKG_VERSION = (() => {
189
+ const requireFrom = createRequire(import.meta.url);
190
+ for (const candidate of ["../../package.json", "../../../package.json"]) {
191
+ try {
192
+ const pkg = requireFrom(candidate);
193
+ if (pkg?.name === "@rozoai/checkout" && pkg.version) return pkg.version;
194
+ } catch {
195
+ }
196
+ }
197
+ return "0.0.0";
198
+ })();
185
199
  var MPP_BASE = process.env.ROZO_CHECKOUT_MPP_BASE || "https://apiserver.mpprouter.dev/v1/services/rozo-agent-api";
200
+ var CLIENT_LABEL = `rozo-checkout-cli/${PKG_VERSION}`;
201
+ var ATTRIBUTION_CLIENT = `rozo-checkout-skill/${PKG_VERSION}`;
186
202
  var INTENTS_BASE = process.env.ROZO_CHECKOUT_INTENTS_BASE || "https://intentapiv4.rozo.ai/functions/v1/payment-api";
187
203
  async function invoiceStatus({ linkId, rozoPaymentId }) {
188
204
  const qs = new URLSearchParams();
@@ -326,7 +342,15 @@ function receiptSignal(source) {
326
342
  return { money: true, receipt: null, unparsable: true };
327
343
  }
328
344
  }
329
- function classifyStatus({ payment, routerState, coinbase, now = Date.now(), viewsFailed = false }) {
345
+ function classifyStatus({
346
+ payment,
347
+ routerState,
348
+ coinbase,
349
+ now = Date.now(),
350
+ viewsFailed = false,
351
+ provider = "coinbase"
352
+ }) {
353
+ const bitrefill = provider === "bitrefill";
330
354
  const source = payment?.source || {};
331
355
  const hasTx = Boolean(source.txHash);
332
356
  const confirmed = Boolean(source.confirmedAt);
@@ -359,7 +383,7 @@ function classifyStatus({ payment, routerState, coinbase, now = Date.now(), view
359
383
  { escalate: true }
360
384
  );
361
385
  }
362
- if (routerStatus === "paid" || coinbase?.settled === true) {
386
+ if (!bitrefill && (routerStatus === "paid" || coinbase?.settled === true)) {
363
387
  return mk("settled", "Coinbase invoice settled by the funder wallet.", { terminal: true });
364
388
  }
365
389
  if (routerStatus === "failed_pay_invoice" || routerStatus === "failed_insufficient_balance") {
@@ -381,6 +405,17 @@ function classifyStatus({ payment, routerState, coinbase, now = Date.now(), view
381
405
  escalate: true
382
406
  });
383
407
  }
408
+ if (bitrefill) {
409
+ const payoutTx = Boolean(payment?.destination?.txHash);
410
+ if (payment?.status === "payment_payout_completed" || payment?.status === "payment_completed" && payoutTx) {
411
+ return mk("settled", "USDC delivered to the Bitrefill invoice address on Base.", {
412
+ terminal: true
413
+ });
414
+ }
415
+ if (payment?.status === "payment_completed") {
416
+ return mk("bridging", "Payment completed but no payout transaction is visible yet. Keep polling.");
417
+ }
418
+ }
384
419
  switch (payment?.status) {
385
420
  case "payment_unpaid": {
386
421
  if (moneyDetected) {
@@ -455,6 +490,19 @@ function formatRemaining(ms) {
455
490
  const m = totalMinutes % 60;
456
491
  return m ? `${h}h ${m}m` : `${h}h`;
457
492
  }
493
+ function parseDeadline(value) {
494
+ if (value === null || value === void 0 || value === "") return null;
495
+ if (typeof value === "number" && Number.isFinite(value)) {
496
+ return value < 1e12 ? Math.round(value * 1e3) : Math.round(value);
497
+ }
498
+ const s = String(value).trim();
499
+ if (/^\d+$/.test(s)) {
500
+ const n = Number(s);
501
+ return n < 1e12 ? n * 1e3 : n;
502
+ }
503
+ const t = Date.parse(s);
504
+ return Number.isFinite(t) ? t : null;
505
+ }
458
506
 
459
507
  // scripts/src/lib/state.mjs
460
508
  import fs from "node:fs";
@@ -463,6 +511,30 @@ import os from "node:os";
463
511
  function stateRoot() {
464
512
  return process.env.ROZO_CHECKOUT_STATE_DIR || path.join(os.homedir(), ".rozo-checkout", "state");
465
513
  }
514
+ function statePath(rozoPaymentId) {
515
+ if (!/^[A-Za-z0-9-]{8,64}$/.test(String(rozoPaymentId || ""))) {
516
+ throw new SkillError("BAD_ROZO_PAYMENT_ID", "Refusing to build a state path from that id.");
517
+ }
518
+ return path.join(stateRoot(), `${rozoPaymentId}.json`);
519
+ }
520
+ function readState(rozoPaymentId) {
521
+ const file = statePath(rozoPaymentId);
522
+ let raw;
523
+ try {
524
+ raw = fs.readFileSync(file, "utf8");
525
+ } catch (err) {
526
+ if (err.code === "ENOENT") return null;
527
+ throw new SkillError("STATE_UNREADABLE", `Cannot read local state: ${err.code}`);
528
+ }
529
+ try {
530
+ return JSON.parse(raw);
531
+ } catch {
532
+ throw new SkillError(
533
+ "STATE_CORRUPT",
534
+ "The local state file for this order is corrupt. Refusing to act; inspect it manually."
535
+ );
536
+ }
537
+ }
466
538
  function findByLinkId(linkId) {
467
539
  const dir = stateRoot();
468
540
  let files = [];
@@ -483,16 +555,61 @@ function findByLinkId(linkId) {
483
555
  return best;
484
556
  }
485
557
 
558
+ // scripts/src/lib/bitrefill.mjs
559
+ var PROVIDER_BITREFILL = "bitrefill";
560
+ var BITREFILL_DESTINATION = Object.freeze({ chainId: "8453", tokenSymbol: "USDC" });
561
+ var BITREFILL_MIN_EXPIRY_MS = 5 * 60 * 1e3;
562
+ var BITREFILL_MIN_PAY_WINDOW_MS = 2 * 60 * 1e3;
563
+ var BITREFILL_MAX_EXPIRY_AHEAD_MS = 30 * 60 * 1e3;
564
+ function providerFromPayment(payment) {
565
+ if (!payment) return null;
566
+ if (String(payment.orderId ?? payment.order_id ?? "").startsWith("bitrefill_")) return PROVIDER_BITREFILL;
567
+ const meta = payment.metadata?.provider;
568
+ if (meta) return String(meta).toLowerCase();
569
+ return null;
570
+ }
571
+ function earliestExpiry(...values) {
572
+ const ms = values.map((v) => parseDeadline(v)).filter((v) => v !== null);
573
+ return ms.length ? new Date(Math.min(...ms)).toISOString() : null;
574
+ }
575
+ function intentBitrefillExpiry(payment) {
576
+ return payment?.metadata?.bitrefillExpiresAt ?? null;
577
+ }
578
+
486
579
  // scripts/src/status.mjs
487
580
  var POLL_INTERVAL_MS = 1e4;
488
581
  var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
489
- async function snapshot({ rozoPaymentId, linkId }) {
582
+ function resolveProvider(explicit, rozoPaymentId) {
583
+ if (explicit) return String(explicit).toLowerCase();
584
+ if (rozoPaymentId && isRozoPaymentId(rozoPaymentId)) {
585
+ try {
586
+ return readState(rozoPaymentId)?.provider ?? null;
587
+ } catch {
588
+ return null;
589
+ }
590
+ }
591
+ return null;
592
+ }
593
+ async function snapshot({ rozoPaymentId, linkId, provider: explicitProvider }) {
594
+ let provider = resolveProvider(explicitProvider, rozoPaymentId);
595
+ let prefetched = null;
596
+ if (!provider && rozoPaymentId && isRozoPaymentId(rozoPaymentId)) {
597
+ try {
598
+ prefetched = await getPayment(rozoPaymentId);
599
+ provider = providerFromPayment(prefetched);
600
+ } catch {
601
+ prefetched = null;
602
+ }
603
+ }
604
+ provider = provider === "bitrefill" ? "bitrefill" : "coinbase";
490
605
  let status = null;
491
606
  let statusError = null;
492
- try {
493
- status = await invoiceStatus({ linkId, rozoPaymentId });
494
- } catch (err) {
495
- statusError = { code: err.code, message: err.message };
607
+ if (provider !== "bitrefill") {
608
+ try {
609
+ status = await invoiceStatus({ linkId, rozoPaymentId });
610
+ } catch (err) {
611
+ statusError = { code: err.code, message: err.message };
612
+ }
496
613
  }
497
614
  let id = rozoPaymentId || status?.rozo_payment_id || null;
498
615
  let idSource = rozoPaymentId ? "argument" : status?.rozo_payment_id ? "invoice-status" : null;
@@ -505,33 +622,61 @@ async function snapshot({ rozoPaymentId, linkId }) {
505
622
  }
506
623
  let payment = null;
507
624
  let paymentError = null;
508
- if (id && isRozoPaymentId(id)) {
625
+ if (prefetched && id === rozoPaymentId) {
626
+ payment = prefetched;
627
+ } else if (id && isRozoPaymentId(id)) {
509
628
  try {
510
629
  payment = await getPayment(id);
511
630
  } catch (err) {
512
631
  paymentError = { code: err.code, message: err.message };
513
632
  }
514
633
  }
515
- const viewsFailed = Boolean(statusError) && !payment;
634
+ const viewsFailed = provider === "bitrefill" ? !payment : Boolean(statusError) && !payment;
516
635
  const verdict = classifyStatus({
517
636
  payment: payment || status?.rozoPayment || {},
518
637
  routerState: status?.routerState,
519
638
  coinbase: status?.coinbase,
520
- viewsFailed
639
+ viewsFailed,
640
+ provider
521
641
  });
522
642
  const source = payment?.source || status?.rozoPayment?.source || {};
643
+ let bitrefillExpiry = null;
644
+ let state = verdict.state;
645
+ let terminal = verdict.terminal;
646
+ let detail = verdict.detail;
647
+ if (provider === "bitrefill") {
648
+ let local = null;
649
+ try {
650
+ local = id && isRozoPaymentId(id) ? readState(id) : null;
651
+ } catch {
652
+ local = null;
653
+ }
654
+ bitrefillExpiry = earliestExpiry(local?.bitrefill?.expiresAt, intentBitrefillExpiry(payment));
655
+ if (!verdict.moneyDetected && ["awaiting_deposit", "expired_unfunded"].includes(verdict.state)) {
656
+ const ms = bitrefillExpiry ? Date.parse(bitrefillExpiry) : NaN;
657
+ if (Number.isFinite(ms) && ms <= Date.now()) {
658
+ state = "invoice_expired";
659
+ terminal = true;
660
+ detail = "The Bitrefill invoice has expired. Do NOT fund this order; create a fresh invoice.";
661
+ } else if (!bitrefillExpiry && verdict.state === "awaiting_deposit") {
662
+ state = "deadline_unknown";
663
+ 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.";
664
+ }
665
+ }
666
+ }
523
667
  return {
668
+ provider,
524
669
  rozoPaymentId: id,
525
670
  rozoPaymentIdSource: idSource,
526
671
  // Without the authoritative payment object we are reading a partial view.
527
672
  authoritativeView: Boolean(payment),
528
673
  linkId: linkId || status?.pl_id || null,
529
- state: verdict.state,
674
+ state,
530
675
  unknown: verdict.unknown,
531
676
  moneyDetected: verdict.moneyDetected,
532
- terminal: verdict.terminal,
677
+ terminal,
533
678
  escalate: verdict.escalate,
534
- detail: verdict.detail,
679
+ detail,
535
680
  backend: {
536
681
  paymentStatus: payment?.status ?? status?.rozoPayment?.status ?? null,
537
682
  routerStatus: verdict.routerStatus,
@@ -548,7 +693,10 @@ async function snapshot({ rozoPaymentId, linkId }) {
548
693
  chain: source.chainId ? chainName(source.chainId) : null
549
694
  },
550
695
  expiry: (() => {
551
- const iso = payment?.expiresAt ?? status?.rozoPayment?.expiresAt ?? null;
696
+ if (provider === "bitrefill" && !bitrefillExpiry) {
697
+ return { expiresAt: null, expiresIn: null, msRemaining: null, deadlineUnknown: true };
698
+ }
699
+ const iso = provider === "bitrefill" ? bitrefillExpiry : payment?.expiresAt ?? status?.rozoPayment?.expiresAt ?? null;
552
700
  if (!iso) return { expiresAt: null, expiresIn: null, msRemaining: null };
553
701
  const ms = Date.parse(iso) - Date.now();
554
702
  return {
@@ -568,21 +716,28 @@ async function main(argv) {
568
716
  const args = parseArgs(argv);
569
717
  const rozoPaymentId = args["rozo-payment-id"] || (isRozoPaymentId(args._[0]) ? args._[0] : null);
570
718
  const linkId = args["link-id"] || (!rozoPaymentId ? args._[0] : null);
719
+ const provider = args.provider ? String(args.provider) : null;
720
+ if (provider && provider !== "bitrefill" && provider !== "coinbase") {
721
+ usage("--provider must be coinbase or bitrefill");
722
+ }
723
+ if (provider === "bitrefill" && !rozoPaymentId) {
724
+ usage("A Bitrefill order is tracked by --rozo-payment-id <uuid>.");
725
+ }
571
726
  if (!rozoPaymentId && !linkId) {
572
727
  usage("Required: --rozo-payment-id <uuid> and/or --link-id <pl_* | paymentSession_*>");
573
728
  }
574
729
  const watch = Boolean(args.watch);
575
730
  const timeoutMs = Math.max(0, Number(args.timeout ?? 600) * 1e3);
576
731
  const deadline = Date.now() + timeoutMs;
577
- let result = await snapshot({ rozoPaymentId, linkId });
732
+ let result = await snapshot({ rozoPaymentId, linkId, provider });
578
733
  const history = [{ at: (/* @__PURE__ */ new Date()).toISOString(), state: result.state }];
579
734
  while (watch && !result.terminal && !result.escalate && !result.unknown && Date.now() < deadline) {
580
735
  await sleep(POLL_INTERVAL_MS);
581
- const next = await snapshot({ rozoPaymentId: result.rozoPaymentId || rozoPaymentId, linkId });
736
+ const next = await snapshot({ rozoPaymentId: result.rozoPaymentId || rozoPaymentId, linkId, provider });
582
737
  if (next.state !== result.state) history.push({ at: (/* @__PURE__ */ new Date()).toISOString(), state: next.state });
583
738
  result = next;
584
739
  }
585
- 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.";
740
+ 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.";
586
741
  const unresolved = watch && !result.terminal && !result.escalate && !result.unknown;
587
742
  const failed = result.escalate || result.unknown || !result.authoritativeView;
588
743
  emit(