@rozoai/checkout 0.1.17 → 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.
- package/README.md +15 -2
- package/llms.txt +5 -2
- package/package.json +1 -1
- package/scripts/dist/cli.js +152 -40
- package/scripts/dist/create-order.js +105 -34
- package/scripts/dist/quote.js +99 -33
- package/scripts/dist/receipt.js +99 -33
- package/scripts/dist/resume.js +99 -33
- package/scripts/dist/send-evm.js +99 -33
- package/scripts/dist/send-sol.js +99 -33
- package/scripts/dist/status.js +118 -35
package/README.md
CHANGED
|
@@ -388,6 +388,14 @@ keypair or an encrypted keystore, and supports EVM chains and Solana only —
|
|
|
388
388
|
Stellar and Lightning are Mode A only.
|
|
389
389
|
</details>
|
|
390
390
|
|
|
391
|
+
## Bulk and automated use
|
|
392
|
+
|
|
393
|
+
Buying credits regularly, reselling them, or building an agent that tops up
|
|
394
|
+
OpenRouter for its users? The [Bulk Purchases and Agents](https://docs.rozo.ai/products/checkout/bulk-and-agents)
|
|
395
|
+
guide covers the CLI, the MCP server and the raw HTTP flow side by side, plus
|
|
396
|
+
fees, rate limits, idempotency, error codes and what each final status means.
|
|
397
|
+
The batch script below is the CLI half of it.
|
|
398
|
+
|
|
391
399
|
## Paying many invoices (batch / resellers)
|
|
392
400
|
|
|
393
401
|
Each OpenRouter top-up is its own Coinbase link, so paying many is a loop: one
|
|
@@ -418,7 +426,8 @@ done < links.txt
|
|
|
418
426
|
- `--yes` is required when stdin is not a terminal, `--json` prints exactly one
|
|
419
427
|
JSON object, and `--no-watch` returns as soon as the deposit details exist.
|
|
420
428
|
Without `--no-watch`, `pay --json` first waits for settlement (up to
|
|
421
|
-
`--timeout`, default 900 s
|
|
429
|
+
`--timeout`, default 900 s; for Lightning, the invoice's remaining validity,
|
|
430
|
+
at most 60 minutes) and prints nothing until then, invoice included.
|
|
422
431
|
- Without `--send` (the loop above), a successful `pay --no-watch` means the
|
|
423
432
|
order and its deposit details exist. **No money has moved yet**, which is
|
|
424
433
|
why the log says `pending`. With `--send`, the CLI may already have
|
|
@@ -511,7 +520,11 @@ a link again, run `status` on its `rozoPaymentId`:
|
|
|
511
520
|
- `RATE_LIMITED`: order creation is capped per IP per hour (currently 30
|
|
512
521
|
requests, and each `pay` makes two), so plan on about 15 links per hour from
|
|
513
522
|
one IP. Wait for the next hour and resume with the links not yet `settled`.
|
|
514
|
-
Need more? Email us.
|
|
523
|
+
Need more? Email us. The CLI retries a 429 by itself, up to 3 times, when
|
|
524
|
+
the server's `Retry-After` (or `retryAfterSeconds`) fits in 2 minutes of
|
|
525
|
+
total waiting. A longer wait fails at once: read
|
|
526
|
+
`error.details.retryAfterSeconds`, and `error.details.rateLimit` (limit,
|
|
527
|
+
remaining, tier, scope) when the server sends it.
|
|
515
528
|
|
|
516
529
|
Questions about a batch? Email hi@rozo.ai, or reach us on
|
|
517
530
|
[X](https://x.com/ROZOai) or [Discord](https://discord.gg/EfWejgTbuU).
|
package/llms.txt
CHANGED
|
@@ -86,7 +86,8 @@ override; larger invoices go through the keyless path.
|
|
|
86
86
|
|
|
87
87
|
Other flags: --yes (skip the interactive confirm; required when stdin is not a
|
|
88
88
|
TTY), --dry-run, --json (exactly one JSON object on stdout), --no-watch,
|
|
89
|
-
--timeout <s
|
|
89
|
+
--timeout <s> (default 900; Lightning: the invoice's remaining validity, max 60
|
|
90
|
+
min). Exit codes: 0 success, 1 refused/failed (read error.code),
|
|
90
91
|
2 usage, 3 submitted but unconfirmed.
|
|
91
92
|
|
|
92
93
|
`pay` withholds the full deposit address until you confirm, then prints it as a
|
|
@@ -176,7 +177,9 @@ private key (EVM and Solana only); every other path is keyless.
|
|
|
176
177
|
|
|
177
178
|
409 = link already used · 410 = link expired · UNSUPPORTED_SOURCE = the response's
|
|
178
179
|
"supported" object lists valid (chainId, tokenSymbol) pairs (Lightning is supported
|
|
179
|
-
but omitted from that list) · RATE_LIMITED = per-IP creation cap, retry later
|
|
180
|
+
but omitted from that list) · RATE_LIMITED = per-IP creation cap, retry later (the CLI itself retries a 429 up
|
|
181
|
+
to 3 times within 2 minutes of total waiting, per Retry-After; a longer wait
|
|
182
|
+
fails at once with error.details.retryAfterSeconds and rateLimit) ·
|
|
180
183
|
INVALID_EMAIL = the optional email is malformed; fix it or omit it.
|
|
181
184
|
|
|
182
185
|
## Support
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rozoai/checkout",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.18",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Pay OpenRouter, or any Coinbase invoice, with the crypto you already hold: USDT/USDC on Solana, BNB Chain, Ethereum or Polygon, USDC on Base or Stellar, or BTC over Lightning. No Coinbase account.",
|
|
6
6
|
"license": "MIT",
|
package/scripts/dist/cli.js
CHANGED
|
@@ -41455,6 +41455,8 @@ var SUPPORT = Object.freeze({
|
|
|
41455
41455
|
discord: "https://discord.gg/EfWejgTbuU"
|
|
41456
41456
|
});
|
|
41457
41457
|
var SUPPORT_TEXT = `Need help? Email ${SUPPORT.email}, or reach ROZO on X ${SUPPORT.x} or Discord ${SUPPORT.discord}.`;
|
|
41458
|
+
var BULK_DOCS_URL = "https://docs.rozo.ai/products/checkout/bulk-and-agents";
|
|
41459
|
+
var BULK_HINT_TEXT = `Buying regularly or for others? ${BULK_DOCS_URL}`;
|
|
41458
41460
|
var CONTACT_EMAIL_MAX_LENGTH = 254;
|
|
41459
41461
|
var EMAIL_RE = /^[a-z0-9][^\s@]*@[^\s@.]+(\.[^\s@.]+)+$/;
|
|
41460
41462
|
function normalizeContactEmail(raw) {
|
|
@@ -42095,7 +42097,9 @@ function parseCliArgs(argv) {
|
|
|
42095
42097
|
json,
|
|
42096
42098
|
provider: flags.provider,
|
|
42097
42099
|
watch: flags.watch === true,
|
|
42098
|
-
|
|
42100
|
+
// Left undefined when absent so status can size the window itself:
|
|
42101
|
+
// 600s, or a Lightning invoice's remaining validity (max 60 min).
|
|
42102
|
+
timeout: flags.timeout === void 0 ? void 0 : Number(flags.timeout)
|
|
42099
42103
|
};
|
|
42100
42104
|
}
|
|
42101
42105
|
const bitrefillFlags = ["bitrefill-invoice", "to", "amount", "expires-at"].filter(
|
|
@@ -42178,6 +42182,7 @@ function parseCliArgs(argv) {
|
|
|
42178
42182
|
dryRun: flags["dry-run"] === true,
|
|
42179
42183
|
watch: flags["no-watch"] !== true,
|
|
42180
42184
|
timeout,
|
|
42185
|
+
timeoutExplicit: flags.timeout !== void 0,
|
|
42181
42186
|
rpc: flags.rpc,
|
|
42182
42187
|
payer: flags.payer,
|
|
42183
42188
|
fresh: flags.fresh === true,
|
|
@@ -42225,7 +42230,8 @@ OPTIONS
|
|
|
42225
42230
|
--dry-run with --send, show what would be signed and sign nothing
|
|
42226
42231
|
--json, -j machine-readable output
|
|
42227
42232
|
--no-watch stop after showing the deposit instructions
|
|
42228
|
-
--timeout <s> how long to poll for settlement (default 900
|
|
42233
|
+
--timeout <s> how long to poll for settlement (default 900; for Lightning,
|
|
42234
|
+
the invoice's remaining validity, at most 60 minutes)
|
|
42229
42235
|
--bitrefill-invoice <id> pay a Bitrefill invoice created with "USDC on Base"
|
|
42230
42236
|
instead of a Coinbase link. Needs --to <0x\u2026 address Bitrefill
|
|
42231
42237
|
shows>, --amount <exact USDC amount> and --expires-at <ISO>
|
|
@@ -42912,6 +42918,25 @@ var MARGINS_MS = {
|
|
|
42912
42918
|
};
|
|
42913
42919
|
var BOLT11_MIN_VALIDITY_MS = 10 * MINUTE;
|
|
42914
42920
|
var DEFAULT_MARGIN_MS = 10 * MINUTE;
|
|
42921
|
+
var DEFAULT_WATCH_MS = 10 * MINUTE;
|
|
42922
|
+
var LIGHTNING_WATCH_CAP_MS = 60 * MINUTE;
|
|
42923
|
+
function lightningWatchMs(expiresAt, { now = Date.now(), fallbackMs = DEFAULT_WATCH_MS } = {}) {
|
|
42924
|
+
const at = parseDeadline(expiresAt);
|
|
42925
|
+
if (at === null) return fallbackMs;
|
|
42926
|
+
const remaining = at - now;
|
|
42927
|
+
if (!(remaining > 0)) return fallbackMs;
|
|
42928
|
+
return Math.min(remaining, LIGHTNING_WATCH_CAP_MS);
|
|
42929
|
+
}
|
|
42930
|
+
function settlementWatchSeconds(opts, deposit, now = Date.now()) {
|
|
42931
|
+
const fallbackMs = Number(opts.timeout ?? 900) * 1e3;
|
|
42932
|
+
if (opts.timeoutExplicit || !deposit?.lnInvoice) return Math.round(fallbackMs / 1e3);
|
|
42933
|
+
return Math.ceil(lightningWatchMs(deposit.expiresAt, { now, fallbackMs }) / 1e3);
|
|
42934
|
+
}
|
|
42935
|
+
function minutesRemaining(expiresAt, now = Date.now()) {
|
|
42936
|
+
const at = parseDeadline(expiresAt);
|
|
42937
|
+
if (at === null) return null;
|
|
42938
|
+
return Math.max(0, Math.floor((at - now) / MINUTE));
|
|
42939
|
+
}
|
|
42915
42940
|
function formatRemaining(ms) {
|
|
42916
42941
|
if (!Number.isFinite(ms)) return "unknown";
|
|
42917
42942
|
if (ms <= 0) return "expired";
|
|
@@ -54615,18 +54640,66 @@ function displayPath(p) {
|
|
|
54615
54640
|
return s.startsWith(home + path6.sep) ? `~${s.slice(home.length)}` : s;
|
|
54616
54641
|
}
|
|
54617
54642
|
|
|
54618
|
-
// scripts/src/lib/
|
|
54643
|
+
// scripts/src/lib/version.mjs
|
|
54619
54644
|
import { createRequire } from "node:module";
|
|
54645
|
+
var PKG_VERSION = (() => {
|
|
54646
|
+
const requireFrom = createRequire(import.meta.url);
|
|
54647
|
+
for (const candidate of ["../../package.json", "../../../package.json"]) {
|
|
54648
|
+
try {
|
|
54649
|
+
const pkg = requireFrom(candidate);
|
|
54650
|
+
if (pkg?.name === "@rozoai/checkout" && pkg.version) return pkg.version;
|
|
54651
|
+
} catch {
|
|
54652
|
+
}
|
|
54653
|
+
}
|
|
54654
|
+
return "0.0.0";
|
|
54655
|
+
})();
|
|
54620
54656
|
|
|
54621
54657
|
// scripts/src/lib/http.mjs
|
|
54622
54658
|
var DEFAULT_TIMEOUT_MS = 2e4;
|
|
54623
|
-
var USER_AGENT =
|
|
54624
|
-
|
|
54659
|
+
var USER_AGENT = `rozo-checkout-skill/${PKG_VERSION}`;
|
|
54660
|
+
var MAX_RETRIES = 3;
|
|
54661
|
+
var MAX_TOTAL_WAIT_MS = 12e4;
|
|
54662
|
+
var FALLBACK_BACKOFF_MS = [2e3, 4e3, 8e3];
|
|
54663
|
+
var defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
54664
|
+
function parseRetryAfter(value, now = Date.now()) {
|
|
54665
|
+
if (value === null || value === void 0) return null;
|
|
54666
|
+
const s = String(value).trim();
|
|
54667
|
+
if (!s) return null;
|
|
54668
|
+
if (/^\d+(\.\d+)?$/.test(s)) return Math.round(Number(s) * 1e3);
|
|
54669
|
+
const at = Date.parse(s);
|
|
54670
|
+
if (!Number.isFinite(at)) return null;
|
|
54671
|
+
return Math.max(0, at - now);
|
|
54672
|
+
}
|
|
54673
|
+
function bodyRetryAfterMs(json) {
|
|
54674
|
+
const raw = json?.retryAfterSeconds ?? json?.error?.retryAfterSeconds;
|
|
54675
|
+
if (raw === null || raw === void 0 || raw === "") return null;
|
|
54676
|
+
const n = Number(raw);
|
|
54677
|
+
return Number.isFinite(n) && n >= 0 ? Math.round(n * 1e3) : null;
|
|
54678
|
+
}
|
|
54679
|
+
function rateLimitInfo(res) {
|
|
54680
|
+
const pick = (name) => res.headers?.get?.(name) ?? null;
|
|
54681
|
+
const info = {
|
|
54682
|
+
limit: pick("x-ratelimit-limit"),
|
|
54683
|
+
remaining: pick("x-ratelimit-remaining"),
|
|
54684
|
+
tier: pick("x-ratelimit-tier"),
|
|
54685
|
+
scope: pick("x-ratelimit-scope")
|
|
54686
|
+
};
|
|
54687
|
+
const present = Object.fromEntries(Object.entries(info).filter(([, v]) => v !== null));
|
|
54688
|
+
return Object.keys(present).length ? present : null;
|
|
54689
|
+
}
|
|
54690
|
+
function parseBody(text) {
|
|
54691
|
+
if (!text) return null;
|
|
54692
|
+
try {
|
|
54693
|
+
return JSON.parse(text);
|
|
54694
|
+
} catch {
|
|
54695
|
+
return null;
|
|
54696
|
+
}
|
|
54697
|
+
}
|
|
54698
|
+
async function fetchOnce(method, url, body, timeoutMs) {
|
|
54625
54699
|
const controller = new AbortController();
|
|
54626
54700
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
54627
|
-
let res;
|
|
54628
54701
|
try {
|
|
54629
|
-
res = await fetch(url, {
|
|
54702
|
+
const res = await fetch(url, {
|
|
54630
54703
|
method,
|
|
54631
54704
|
headers: {
|
|
54632
54705
|
accept: "application/json",
|
|
@@ -54636,6 +54709,8 @@ async function request(method, url, { body, timeoutMs = DEFAULT_TIMEOUT_MS } = {
|
|
|
54636
54709
|
body: body !== void 0 ? JSON.stringify(body) : void 0,
|
|
54637
54710
|
signal: controller.signal
|
|
54638
54711
|
});
|
|
54712
|
+
const text = await res.text();
|
|
54713
|
+
return { res, text };
|
|
54639
54714
|
} catch (err) {
|
|
54640
54715
|
throw new SkillError(
|
|
54641
54716
|
err?.name === "AbortError" ? "HTTP_TIMEOUT" : "HTTP_UNREACHABLE",
|
|
@@ -54645,31 +54720,54 @@ async function request(method, url, { body, timeoutMs = DEFAULT_TIMEOUT_MS } = {
|
|
|
54645
54720
|
} finally {
|
|
54646
54721
|
clearTimeout(timer);
|
|
54647
54722
|
}
|
|
54648
|
-
|
|
54649
|
-
|
|
54650
|
-
|
|
54651
|
-
|
|
54652
|
-
|
|
54653
|
-
|
|
54654
|
-
|
|
54723
|
+
}
|
|
54724
|
+
async function request(method, url, {
|
|
54725
|
+
body,
|
|
54726
|
+
timeoutMs = DEFAULT_TIMEOUT_MS,
|
|
54727
|
+
sleep: sleep2 = defaultSleep,
|
|
54728
|
+
now = Date.now,
|
|
54729
|
+
maxTotalWaitMs = MAX_TOTAL_WAIT_MS
|
|
54730
|
+
} = {}) {
|
|
54731
|
+
let waitedMs = 0;
|
|
54732
|
+
let retries = 0;
|
|
54733
|
+
for (; ; ) {
|
|
54734
|
+
const { res, text } = await fetchOnce(method, url, body, timeoutMs);
|
|
54735
|
+
const json = parseBody(text);
|
|
54736
|
+
if (res.ok) {
|
|
54737
|
+
if (json === null) {
|
|
54738
|
+
throw new SkillError("HTTP_BAD_JSON", "Endpoint returned a non-JSON body.", {
|
|
54739
|
+
url: redactUrl(url),
|
|
54740
|
+
snippet: redact(text).slice(0, 400)
|
|
54741
|
+
});
|
|
54742
|
+
}
|
|
54743
|
+
return json;
|
|
54744
|
+
}
|
|
54745
|
+
const headerWaitMs = parseRetryAfter(res.headers?.get?.("retry-after"), now());
|
|
54746
|
+
const hintMs = headerWaitMs ?? bodyRetryAfterMs(json);
|
|
54747
|
+
const retryable = res.status === 429 || res.status === 503 && hintMs !== null;
|
|
54748
|
+
if (retryable && retries < MAX_RETRIES) {
|
|
54749
|
+
const waitMs = hintMs ?? FALLBACK_BACKOFF_MS[retries];
|
|
54750
|
+
if (waitedMs + waitMs <= Math.min(maxTotalWaitMs, MAX_TOTAL_WAIT_MS)) {
|
|
54751
|
+
retries += 1;
|
|
54752
|
+
waitedMs += waitMs;
|
|
54753
|
+
await sleep2(waitMs);
|
|
54754
|
+
continue;
|
|
54755
|
+
}
|
|
54655
54756
|
}
|
|
54656
|
-
}
|
|
54657
|
-
if (!res.ok) {
|
|
54658
54757
|
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}`;
|
|
54659
54758
|
const message = json?.message || (typeof json?.error === "string" ? json.error : json?.error?.message) || `HTTP ${res.status}`;
|
|
54759
|
+
const rateLimit = rateLimitInfo(res);
|
|
54660
54760
|
throw new SkillError(code, redact(String(message)), {
|
|
54661
54761
|
httpStatus: res.status,
|
|
54662
54762
|
url: redactUrl(url),
|
|
54663
|
-
body: json ?? redact(text).slice(0, 800)
|
|
54763
|
+
body: json ?? redact(text).slice(0, 800),
|
|
54764
|
+
...retryable ? {
|
|
54765
|
+
retries,
|
|
54766
|
+
retryAfterSeconds: hintMs === null ? null : Math.ceil(hintMs / 1e3)
|
|
54767
|
+
} : {},
|
|
54768
|
+
...rateLimit ? { rateLimit } : {}
|
|
54664
54769
|
});
|
|
54665
54770
|
}
|
|
54666
|
-
if (json === null) {
|
|
54667
|
-
throw new SkillError("HTTP_BAD_JSON", "Endpoint returned a non-JSON body.", {
|
|
54668
|
-
url: redactUrl(url),
|
|
54669
|
-
snippet: redact(text).slice(0, 400)
|
|
54670
|
-
});
|
|
54671
|
-
}
|
|
54672
|
-
return json;
|
|
54673
54771
|
}
|
|
54674
54772
|
function redactUrl(url) {
|
|
54675
54773
|
try {
|
|
@@ -54725,17 +54823,6 @@ function attributionIdentity(env = process.env) {
|
|
|
54725
54823
|
}
|
|
54726
54824
|
|
|
54727
54825
|
// scripts/src/lib/api.mjs
|
|
54728
|
-
var PKG_VERSION = (() => {
|
|
54729
|
-
const requireFrom = createRequire(import.meta.url);
|
|
54730
|
-
for (const candidate of ["../../package.json", "../../../package.json"]) {
|
|
54731
|
-
try {
|
|
54732
|
-
const pkg = requireFrom(candidate);
|
|
54733
|
-
if (pkg?.name === "@rozoai/checkout" && pkg.version) return pkg.version;
|
|
54734
|
-
} catch {
|
|
54735
|
-
}
|
|
54736
|
-
}
|
|
54737
|
-
return "0.0.0";
|
|
54738
|
-
})();
|
|
54739
54826
|
var MPP_BASE = process.env.ROZO_CHECKOUT_MPP_BASE || "https://apiserver.mpprouter.dev/v1/services/rozo-agent-api";
|
|
54740
54827
|
var CLIENT_LABEL = `rozo-checkout-cli/${PKG_VERSION}`;
|
|
54741
54828
|
var ATTRIBUTION_CLIENT = `rozo-checkout-skill/${PKG_VERSION}`;
|
|
@@ -54755,6 +54842,7 @@ async function quoteInvoice({ url, linkId }) {
|
|
|
54755
54842
|
const body = url ? { url } : { payment_id: linkId };
|
|
54756
54843
|
return postJson(`${MPP_BASE}/quote-invoice`, body);
|
|
54757
54844
|
}
|
|
54845
|
+
var QUOTE_RECEIPT_RETRY_BUDGET_MS = 2e4;
|
|
54758
54846
|
async function createInvoice({ url, linkId, source, quoteReceipt, utmSource, email }) {
|
|
54759
54847
|
const body = {
|
|
54760
54848
|
...url ? { url } : { payment_id: linkId },
|
|
@@ -54766,7 +54854,11 @@ async function createInvoice({ url, linkId, source, quoteReceipt, utmSource, ema
|
|
|
54766
54854
|
// sent empty, when the user gave none.
|
|
54767
54855
|
...email ? { email } : {}
|
|
54768
54856
|
};
|
|
54769
|
-
return postJson(
|
|
54857
|
+
return postJson(
|
|
54858
|
+
`${MPP_BASE}/create-invoice`,
|
|
54859
|
+
body,
|
|
54860
|
+
quoteReceipt ? { maxTotalWaitMs: QUOTE_RECEIPT_RETRY_BUDGET_MS } : void 0
|
|
54861
|
+
);
|
|
54770
54862
|
}
|
|
54771
54863
|
async function createBitrefillInvoice({ invoice, source, utmSource, email }) {
|
|
54772
54864
|
const body = {
|
|
@@ -56451,6 +56543,16 @@ function statusGuidance(result) {
|
|
|
56451
56543
|
if (result.state === "expired_unfunded") return EXPIRED_UNFUNDED_GUIDANCE;
|
|
56452
56544
|
return result.terminal ? "Done." : "Still in flight. Poll again in ~10s.";
|
|
56453
56545
|
}
|
|
56546
|
+
function watchTimeoutMs(timeoutArg, result, now = Date.now()) {
|
|
56547
|
+
if (timeoutArg !== void 0 && timeoutArg !== null && timeoutArg !== true) {
|
|
56548
|
+
const n = Number(timeoutArg);
|
|
56549
|
+
if (Number.isFinite(n)) return Math.max(0, n * 1e3);
|
|
56550
|
+
}
|
|
56551
|
+
if (result?.payin?.chain === chainName("lightning")) {
|
|
56552
|
+
return lightningWatchMs(result?.expiry?.expiresAt, { now, fallbackMs: DEFAULT_WATCH_MS });
|
|
56553
|
+
}
|
|
56554
|
+
return DEFAULT_WATCH_MS;
|
|
56555
|
+
}
|
|
56454
56556
|
async function main4(argv) {
|
|
56455
56557
|
const args = parseArgs(argv);
|
|
56456
56558
|
const rozoPaymentId = args["rozo-payment-id"] || (isRozoPaymentId(args._[0]) ? args._[0] : null);
|
|
@@ -56466,9 +56568,9 @@ async function main4(argv) {
|
|
|
56466
56568
|
usage("Required: --rozo-payment-id <uuid> and/or --link-id <pl_* | paymentSession_*>");
|
|
56467
56569
|
}
|
|
56468
56570
|
const watch = Boolean(args.watch);
|
|
56469
|
-
const
|
|
56470
|
-
const deadline = Date.now() + timeoutMs;
|
|
56571
|
+
const started = Date.now();
|
|
56471
56572
|
let result = await snapshot({ rozoPaymentId, linkId, provider });
|
|
56573
|
+
const deadline = started + watchTimeoutMs(args.timeout, result, started);
|
|
56472
56574
|
const history = [{ at: (/* @__PURE__ */ new Date()).toISOString(), state: result.state }];
|
|
56473
56575
|
while (watch && !result.terminal && !result.escalate && !result.unknown && Date.now() < deadline) {
|
|
56474
56576
|
await sleep(POLL_INTERVAL_MS);
|
|
@@ -58142,6 +58244,9 @@ function printError(payload) {
|
|
|
58142
58244
|
function printSupport() {
|
|
58143
58245
|
out(` ${dim(SUPPORT_TEXT)}`);
|
|
58144
58246
|
}
|
|
58247
|
+
function printBulkHint() {
|
|
58248
|
+
out(` ${dim(BULK_HINT_TEXT)}`);
|
|
58249
|
+
}
|
|
58145
58250
|
function printMoneyWarning(payload) {
|
|
58146
58251
|
if (!payload?.moneyDetected) return;
|
|
58147
58252
|
out();
|
|
@@ -58279,7 +58384,8 @@ async function cmdQuote(opts) {
|
|
|
58279
58384
|
return exitCode;
|
|
58280
58385
|
}
|
|
58281
58386
|
async function cmdStatus(opts) {
|
|
58282
|
-
const argv = [...targetToArgs(opts.target)
|
|
58387
|
+
const argv = [...targetToArgs(opts.target)];
|
|
58388
|
+
if (opts.timeout !== void 0) argv.push("--timeout", String(opts.timeout));
|
|
58283
58389
|
if (opts.provider) argv.push("--provider", opts.provider);
|
|
58284
58390
|
if (opts.watch) argv.push("--watch");
|
|
58285
58391
|
if (!opts.json) out(dim(" Checking payment status\u2026 (no money moves)"));
|
|
@@ -58340,6 +58446,7 @@ async function cmdReceipt(opts) {
|
|
|
58340
58446
|
out();
|
|
58341
58447
|
out(` Next: ${r.nextAction.type === "contact_support" ? red(r.nextAction.message) : r.nextAction.message}`);
|
|
58342
58448
|
if (payload.support) printSupport();
|
|
58449
|
+
if (r.paymentOutcome === "settled") printBulkHint();
|
|
58343
58450
|
out();
|
|
58344
58451
|
return exitCode;
|
|
58345
58452
|
}
|
|
@@ -58650,7 +58757,7 @@ async function cmdPay(opts) {
|
|
|
58650
58757
|
rozoPaymentId,
|
|
58651
58758
|
"--watch",
|
|
58652
58759
|
"--timeout",
|
|
58653
|
-
String(opts
|
|
58760
|
+
String(settlementWatchSeconds(opts, deposit))
|
|
58654
58761
|
]);
|
|
58655
58762
|
if (opts.json) {
|
|
58656
58763
|
printJson({
|
|
@@ -58670,6 +58777,7 @@ async function cmdPay(opts) {
|
|
|
58670
58777
|
}
|
|
58671
58778
|
printMoneyWarning(watched.payload);
|
|
58672
58779
|
if (watched.payload.state !== "settled") printSupport();
|
|
58780
|
+
else printBulkHint();
|
|
58673
58781
|
out();
|
|
58674
58782
|
return watched.exitCode;
|
|
58675
58783
|
}
|
|
@@ -58680,6 +58788,10 @@ function printDeposit(deposit, opts) {
|
|
|
58680
58788
|
if (deposit.lnInvoice) {
|
|
58681
58789
|
out(` Amount ${bold(`${deposit.amount} ${isSatsUnit(deposit.amountUnit) ? "sats" : deposit.tokenSymbol}`)}`);
|
|
58682
58790
|
out(` BOLT11 ${deposit.lnInvoice}`);
|
|
58791
|
+
const validMin = minutesRemaining(deposit.expiresAt);
|
|
58792
|
+
if (validMin !== null) {
|
|
58793
|
+
out(` Valid ${bold(validMin < 1 ? "under 1 more minute" : `${validMin} more minutes`)}`);
|
|
58794
|
+
}
|
|
58683
58795
|
} else {
|
|
58684
58796
|
out(` Amount ${bold(`${deposit.amount} ${deposit.tokenSymbol}`)}`);
|
|
58685
58797
|
out(` Chain ${bold(deposit.chain)}`);
|
|
@@ -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/
|
|
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 =
|
|
207
|
-
|
|
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
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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)
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
snippet: redact(text).slice(0, 400)
|
|
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 } : {}
|
|
253
329
|
});
|
|
254
330
|
}
|
|
255
|
-
return json;
|
|
256
331
|
}
|
|
257
332
|
function redactUrl(url) {
|
|
258
333
|
try {
|
|
@@ -582,17 +657,6 @@ function attributionIdentity(env = process.env) {
|
|
|
582
657
|
}
|
|
583
658
|
|
|
584
659
|
// scripts/src/lib/api.mjs
|
|
585
|
-
var PKG_VERSION = (() => {
|
|
586
|
-
const requireFrom = createRequire(import.meta.url);
|
|
587
|
-
for (const candidate of ["../../package.json", "../../../package.json"]) {
|
|
588
|
-
try {
|
|
589
|
-
const pkg = requireFrom(candidate);
|
|
590
|
-
if (pkg?.name === "@rozoai/checkout" && pkg.version) return pkg.version;
|
|
591
|
-
} catch {
|
|
592
|
-
}
|
|
593
|
-
}
|
|
594
|
-
return "0.0.0";
|
|
595
|
-
})();
|
|
596
660
|
var MPP_BASE = process.env.ROZO_CHECKOUT_MPP_BASE || "https://apiserver.mpprouter.dev/v1/services/rozo-agent-api";
|
|
597
661
|
var CLIENT_LABEL = `rozo-checkout-cli/${PKG_VERSION}`;
|
|
598
662
|
var ATTRIBUTION_CLIENT = `rozo-checkout-skill/${PKG_VERSION}`;
|
|
@@ -612,6 +676,7 @@ async function quoteInvoice({ url, linkId }) {
|
|
|
612
676
|
const body = url ? { url } : { payment_id: linkId };
|
|
613
677
|
return postJson(`${MPP_BASE}/quote-invoice`, body);
|
|
614
678
|
}
|
|
679
|
+
var QUOTE_RECEIPT_RETRY_BUDGET_MS = 2e4;
|
|
615
680
|
async function createInvoice({ url, linkId, source, quoteReceipt, utmSource, email }) {
|
|
616
681
|
const body = {
|
|
617
682
|
...url ? { url } : { payment_id: linkId },
|
|
@@ -623,7 +688,11 @@ async function createInvoice({ url, linkId, source, quoteReceipt, utmSource, ema
|
|
|
623
688
|
// sent empty, when the user gave none.
|
|
624
689
|
...email ? { email } : {}
|
|
625
690
|
};
|
|
626
|
-
return postJson(
|
|
691
|
+
return postJson(
|
|
692
|
+
`${MPP_BASE}/create-invoice`,
|
|
693
|
+
body,
|
|
694
|
+
quoteReceipt ? { maxTotalWaitMs: QUOTE_RECEIPT_RETRY_BUDGET_MS } : void 0
|
|
695
|
+
);
|
|
627
696
|
}
|
|
628
697
|
async function invoiceStatus({ linkId, rozoPaymentId }) {
|
|
629
698
|
const qs = new URLSearchParams();
|
|
@@ -812,6 +881,8 @@ var MARGINS_MS = {
|
|
|
812
881
|
};
|
|
813
882
|
var BOLT11_MIN_VALIDITY_MS = 10 * MINUTE;
|
|
814
883
|
var DEFAULT_MARGIN_MS = 10 * MINUTE;
|
|
884
|
+
var DEFAULT_WATCH_MS = 10 * MINUTE;
|
|
885
|
+
var LIGHTNING_WATCH_CAP_MS = 60 * MINUTE;
|
|
815
886
|
function formatRemaining(ms) {
|
|
816
887
|
if (!Number.isFinite(ms)) return "unknown";
|
|
817
888
|
if (ms <= 0) return "expired";
|