@rozoai/checkout 0.1.17 → 0.1.19
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 +38 -2
- package/package.json +1 -1
- package/scripts/dist/cli.js +1073 -70
- package/scripts/dist/create-order.js +106 -34
- package/scripts/dist/quote.js +100 -33
- package/scripts/dist/receipt.js +100 -33
- package/scripts/dist/resume.js +100 -33
- package/scripts/dist/send-evm.js +100 -33
- package/scripts/dist/send-sol.js +100 -33
- package/scripts/dist/status.js +119 -35
package/scripts/dist/cli.js
CHANGED
|
@@ -20473,8 +20473,8 @@ var require_dist = __commonJS({
|
|
|
20473
20473
|
constructor(failure, failures) {
|
|
20474
20474
|
let cached2;
|
|
20475
20475
|
const { message, explanation, ...rest } = failure;
|
|
20476
|
-
const { path:
|
|
20477
|
-
const msg =
|
|
20476
|
+
const { path: path8 } = failure;
|
|
20477
|
+
const msg = path8.length === 0 ? message : `At path: ${path8.join(".")} -- ${message}`;
|
|
20478
20478
|
super(explanation ?? msg);
|
|
20479
20479
|
if (explanation != null)
|
|
20480
20480
|
this.cause = msg;
|
|
@@ -20519,15 +20519,15 @@ var require_dist = __commonJS({
|
|
|
20519
20519
|
} else if (typeof result === "string") {
|
|
20520
20520
|
result = { message: result };
|
|
20521
20521
|
}
|
|
20522
|
-
const { path:
|
|
20522
|
+
const { path: path8, branch } = context;
|
|
20523
20523
|
const { type: type2 } = struct2;
|
|
20524
20524
|
const { refinement, message = `Expected a value of type \`${type2}\`${refinement ? ` with refinement \`${refinement}\`` : ""}, but received: \`${print(value)}\`` } = result;
|
|
20525
20525
|
return {
|
|
20526
20526
|
value,
|
|
20527
20527
|
type: type2,
|
|
20528
20528
|
refinement,
|
|
20529
|
-
key:
|
|
20530
|
-
path:
|
|
20529
|
+
key: path8[path8.length - 1],
|
|
20530
|
+
path: path8,
|
|
20531
20531
|
branch,
|
|
20532
20532
|
...result,
|
|
20533
20533
|
message
|
|
@@ -20545,8 +20545,8 @@ var require_dist = __commonJS({
|
|
|
20545
20545
|
}
|
|
20546
20546
|
}
|
|
20547
20547
|
function* run9(value, struct2, options = {}) {
|
|
20548
|
-
const { path:
|
|
20549
|
-
const ctx = { path:
|
|
20548
|
+
const { path: path8 = [], branch = [value], coerce: coerce2 = false, mask: mask2 = false } = options;
|
|
20549
|
+
const ctx = { path: path8, branch, mask: mask2 };
|
|
20550
20550
|
if (coerce2) {
|
|
20551
20551
|
value = struct2.coercer(value, ctx);
|
|
20552
20552
|
}
|
|
@@ -20558,7 +20558,7 @@ var require_dist = __commonJS({
|
|
|
20558
20558
|
}
|
|
20559
20559
|
for (let [k, v, s] of struct2.entries(value, ctx)) {
|
|
20560
20560
|
const ts = run9(v, s, {
|
|
20561
|
-
path: k === void 0 ?
|
|
20561
|
+
path: k === void 0 ? path8 : [...path8, k],
|
|
20562
20562
|
branch: k === void 0 ? branch : [...branch, v],
|
|
20563
20563
|
coerce: coerce2,
|
|
20564
20564
|
mask: mask2,
|
|
@@ -20793,7 +20793,7 @@ var require_dist = __commonJS({
|
|
|
20793
20793
|
}
|
|
20794
20794
|
return object(schema);
|
|
20795
20795
|
}
|
|
20796
|
-
function
|
|
20796
|
+
function pick2(struct2, keys) {
|
|
20797
20797
|
const { schema } = struct2;
|
|
20798
20798
|
const subschema = {};
|
|
20799
20799
|
for (const key of keys) {
|
|
@@ -21254,7 +21254,7 @@ var require_dist = __commonJS({
|
|
|
21254
21254
|
exports2.optional = optional;
|
|
21255
21255
|
exports2.partial = partial;
|
|
21256
21256
|
exports2.pattern = pattern;
|
|
21257
|
-
exports2.pick =
|
|
21257
|
+
exports2.pick = pick2;
|
|
21258
21258
|
exports2.record = record;
|
|
21259
21259
|
exports2.refine = refine;
|
|
21260
21260
|
exports2.regexp = regexp;
|
|
@@ -22744,14 +22744,14 @@ var require_url_state_machine = __commonJS({
|
|
|
22744
22744
|
return url.replace(/\u0009|\u000A|\u000D/g, "");
|
|
22745
22745
|
}
|
|
22746
22746
|
function shortenPath(url) {
|
|
22747
|
-
const
|
|
22748
|
-
if (
|
|
22747
|
+
const path8 = url.path;
|
|
22748
|
+
if (path8.length === 0) {
|
|
22749
22749
|
return;
|
|
22750
22750
|
}
|
|
22751
|
-
if (url.scheme === "file" &&
|
|
22751
|
+
if (url.scheme === "file" && path8.length === 1 && isNormalizedWindowsDriveLetter(path8[0])) {
|
|
22752
22752
|
return;
|
|
22753
22753
|
}
|
|
22754
|
-
|
|
22754
|
+
path8.pop();
|
|
22755
22755
|
}
|
|
22756
22756
|
function includesCredentials(url) {
|
|
22757
22757
|
return url.username !== "" || url.password !== "";
|
|
@@ -25046,16 +25046,16 @@ var require_constants = __commonJS({
|
|
|
25046
25046
|
// node_modules/node-gyp-build/node-gyp-build.js
|
|
25047
25047
|
var require_node_gyp_build = __commonJS({
|
|
25048
25048
|
"node_modules/node-gyp-build/node-gyp-build.js"(exports, module) {
|
|
25049
|
-
var
|
|
25050
|
-
var
|
|
25051
|
-
var
|
|
25049
|
+
var fs8 = __require("fs");
|
|
25050
|
+
var path8 = __require("path");
|
|
25051
|
+
var os6 = __require("os");
|
|
25052
25052
|
var runtimeRequire = typeof __webpack_require__ === "function" ? __non_webpack_require__ : __require;
|
|
25053
25053
|
var vars = process.config && process.config.variables || {};
|
|
25054
25054
|
var prebuildsOnly = !!process.env.PREBUILDS_ONLY;
|
|
25055
25055
|
var abi2 = process.versions.modules;
|
|
25056
25056
|
var runtime = isElectron() ? "electron" : isNwjs() ? "node-webkit" : "node";
|
|
25057
|
-
var arch = process.env.npm_config_arch ||
|
|
25058
|
-
var platform = process.env.npm_config_platform ||
|
|
25057
|
+
var arch = process.env.npm_config_arch || os6.arch();
|
|
25058
|
+
var platform = process.env.npm_config_platform || os6.platform();
|
|
25059
25059
|
var libc = process.env.LIBC || (isAlpine(platform) ? "musl" : "glibc");
|
|
25060
25060
|
var armv = process.env.ARM_VERSION || (arch === "arm64" ? "8" : vars.arm_version) || "";
|
|
25061
25061
|
var uv = (process.versions.uv || "").split(".")[0];
|
|
@@ -25064,21 +25064,21 @@ var require_node_gyp_build = __commonJS({
|
|
|
25064
25064
|
return runtimeRequire(load.resolve(dir));
|
|
25065
25065
|
}
|
|
25066
25066
|
load.resolve = load.path = function(dir) {
|
|
25067
|
-
dir =
|
|
25067
|
+
dir = path8.resolve(dir || ".");
|
|
25068
25068
|
try {
|
|
25069
|
-
var name = runtimeRequire(
|
|
25069
|
+
var name = runtimeRequire(path8.join(dir, "package.json")).name.toUpperCase().replace(/-/g, "_");
|
|
25070
25070
|
if (process.env[name + "_PREBUILD"]) dir = process.env[name + "_PREBUILD"];
|
|
25071
25071
|
} catch (err) {
|
|
25072
25072
|
}
|
|
25073
25073
|
if (!prebuildsOnly) {
|
|
25074
|
-
var release = getFirst(
|
|
25074
|
+
var release = getFirst(path8.join(dir, "build/Release"), matchBuild);
|
|
25075
25075
|
if (release) return release;
|
|
25076
|
-
var debug = getFirst(
|
|
25076
|
+
var debug = getFirst(path8.join(dir, "build/Debug"), matchBuild);
|
|
25077
25077
|
if (debug) return debug;
|
|
25078
25078
|
}
|
|
25079
25079
|
var prebuild = resolve(dir);
|
|
25080
25080
|
if (prebuild) return prebuild;
|
|
25081
|
-
var nearby = resolve(
|
|
25081
|
+
var nearby = resolve(path8.dirname(process.execPath));
|
|
25082
25082
|
if (nearby) return nearby;
|
|
25083
25083
|
var target = [
|
|
25084
25084
|
"platform=" + platform,
|
|
@@ -25095,26 +25095,26 @@ var require_node_gyp_build = __commonJS({
|
|
|
25095
25095
|
].filter(Boolean).join(" ");
|
|
25096
25096
|
throw new Error("No native build was found for " + target + "\n loaded from: " + dir + "\n");
|
|
25097
25097
|
function resolve(dir2) {
|
|
25098
|
-
var tuples = readdirSync(
|
|
25098
|
+
var tuples = readdirSync(path8.join(dir2, "prebuilds")).map(parseTuple);
|
|
25099
25099
|
var tuple = tuples.filter(matchTuple(platform, arch)).sort(compareTuples)[0];
|
|
25100
25100
|
if (!tuple) return;
|
|
25101
|
-
var prebuilds =
|
|
25101
|
+
var prebuilds = path8.join(dir2, "prebuilds", tuple.name);
|
|
25102
25102
|
var parsed = readdirSync(prebuilds).map(parseTags);
|
|
25103
25103
|
var candidates = parsed.filter(matchTags(runtime, abi2));
|
|
25104
25104
|
var winner = candidates.sort(compareTags(runtime))[0];
|
|
25105
|
-
if (winner) return
|
|
25105
|
+
if (winner) return path8.join(prebuilds, winner.file);
|
|
25106
25106
|
}
|
|
25107
25107
|
};
|
|
25108
25108
|
function readdirSync(dir) {
|
|
25109
25109
|
try {
|
|
25110
|
-
return
|
|
25110
|
+
return fs8.readdirSync(dir);
|
|
25111
25111
|
} catch (err) {
|
|
25112
25112
|
return [];
|
|
25113
25113
|
}
|
|
25114
25114
|
}
|
|
25115
25115
|
function getFirst(dir, filter) {
|
|
25116
25116
|
var files = readdirSync(dir).filter(filter);
|
|
25117
|
-
return files[0] &&
|
|
25117
|
+
return files[0] && path8.join(dir, files[0]);
|
|
25118
25118
|
}
|
|
25119
25119
|
function matchBuild(name) {
|
|
25120
25120
|
return /\.node$/.test(name);
|
|
@@ -25201,7 +25201,7 @@ var require_node_gyp_build = __commonJS({
|
|
|
25201
25201
|
return typeof window !== "undefined" && window.process && window.process.type === "renderer";
|
|
25202
25202
|
}
|
|
25203
25203
|
function isAlpine(platform2) {
|
|
25204
|
-
return platform2 === "linux" &&
|
|
25204
|
+
return platform2 === "linux" && fs8.existsSync("/etc/alpine-release");
|
|
25205
25205
|
}
|
|
25206
25206
|
load.parseTags = parseTags;
|
|
25207
25207
|
load.matchTags = matchTags;
|
|
@@ -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) {
|
|
@@ -41508,6 +41510,7 @@ function redact(text) {
|
|
|
41508
41510
|
return `${scheme}://${host}${hadMore ? "/<redacted>" : ""}`;
|
|
41509
41511
|
});
|
|
41510
41512
|
s = s.replace(/\b(bearer)\s+[A-Za-z0-9._~+/=-]{8,}/gi, "$1 <redacted>");
|
|
41513
|
+
s = s.replace(/\bak_[A-Za-z0-9_-]{8,}/g, "ak_<redacted>");
|
|
41511
41514
|
s = s.replace(
|
|
41512
41515
|
/\b(api[-_]?key|apikey|access[-_]?token|auth[-_]?token|secret|token|password|passwd|pwd)\b(\s*[:=]\s*)("?)[A-Za-z0-9._~+/=-]{6,}\3/gi,
|
|
41513
41516
|
"$1$2<redacted>"
|
|
@@ -42095,7 +42098,9 @@ function parseCliArgs(argv) {
|
|
|
42095
42098
|
json,
|
|
42096
42099
|
provider: flags.provider,
|
|
42097
42100
|
watch: flags.watch === true,
|
|
42098
|
-
|
|
42101
|
+
// Left undefined when absent so status can size the window itself:
|
|
42102
|
+
// 600s, or a Lightning invoice's remaining validity (max 60 min).
|
|
42103
|
+
timeout: flags.timeout === void 0 ? void 0 : Number(flags.timeout)
|
|
42099
42104
|
};
|
|
42100
42105
|
}
|
|
42101
42106
|
const bitrefillFlags = ["bitrefill-invoice", "to", "amount", "expires-at"].filter(
|
|
@@ -42178,6 +42183,7 @@ function parseCliArgs(argv) {
|
|
|
42178
42183
|
dryRun: flags["dry-run"] === true,
|
|
42179
42184
|
watch: flags["no-watch"] !== true,
|
|
42180
42185
|
timeout,
|
|
42186
|
+
timeoutExplicit: flags.timeout !== void 0,
|
|
42181
42187
|
rpc: flags.rpc,
|
|
42182
42188
|
payer: flags.payer,
|
|
42183
42189
|
fresh: flags.fresh === true,
|
|
@@ -42198,6 +42204,7 @@ USAGE
|
|
|
42198
42204
|
npx @rozoai/checkout status <rozoPaymentId | coinbase-link>
|
|
42199
42205
|
npx @rozoai/checkout receipt <rozoPaymentId | coinbase-link>
|
|
42200
42206
|
npx @rozoai/checkout resume <rozoPaymentId>
|
|
42207
|
+
npx @rozoai/checkout x402 <topup|pay|balance> (pay any x402 API; see x402 --help)
|
|
42201
42208
|
|
|
42202
42209
|
COINS (--with)
|
|
42203
42210
|
usdt-solana usdc-solana usdt-bnb usdc-bnb
|
|
@@ -42225,7 +42232,8 @@ OPTIONS
|
|
|
42225
42232
|
--dry-run with --send, show what would be signed and sign nothing
|
|
42226
42233
|
--json, -j machine-readable output
|
|
42227
42234
|
--no-watch stop after showing the deposit instructions
|
|
42228
|
-
--timeout <s> how long to poll for settlement (default 900
|
|
42235
|
+
--timeout <s> how long to poll for settlement (default 900; for Lightning,
|
|
42236
|
+
the invoice's remaining validity, at most 60 minutes)
|
|
42229
42237
|
--bitrefill-invoice <id> pay a Bitrefill invoice created with "USDC on Base"
|
|
42230
42238
|
instead of a Coinbase link. Needs --to <0x\u2026 address Bitrefill
|
|
42231
42239
|
shows>, --amount <exact USDC amount> and --expires-at <ISO>
|
|
@@ -42912,6 +42920,25 @@ var MARGINS_MS = {
|
|
|
42912
42920
|
};
|
|
42913
42921
|
var BOLT11_MIN_VALIDITY_MS = 10 * MINUTE;
|
|
42914
42922
|
var DEFAULT_MARGIN_MS = 10 * MINUTE;
|
|
42923
|
+
var DEFAULT_WATCH_MS = 10 * MINUTE;
|
|
42924
|
+
var LIGHTNING_WATCH_CAP_MS = 60 * MINUTE;
|
|
42925
|
+
function lightningWatchMs(expiresAt, { now = Date.now(), fallbackMs = DEFAULT_WATCH_MS } = {}) {
|
|
42926
|
+
const at = parseDeadline(expiresAt);
|
|
42927
|
+
if (at === null) return fallbackMs;
|
|
42928
|
+
const remaining = at - now;
|
|
42929
|
+
if (!(remaining > 0)) return fallbackMs;
|
|
42930
|
+
return Math.min(remaining, LIGHTNING_WATCH_CAP_MS);
|
|
42931
|
+
}
|
|
42932
|
+
function settlementWatchSeconds(opts, deposit, now = Date.now()) {
|
|
42933
|
+
const fallbackMs = Number(opts.timeout ?? 900) * 1e3;
|
|
42934
|
+
if (opts.timeoutExplicit || !deposit?.lnInvoice) return Math.round(fallbackMs / 1e3);
|
|
42935
|
+
return Math.ceil(lightningWatchMs(deposit.expiresAt, { now, fallbackMs }) / 1e3);
|
|
42936
|
+
}
|
|
42937
|
+
function minutesRemaining(expiresAt, now = Date.now()) {
|
|
42938
|
+
const at = parseDeadline(expiresAt);
|
|
42939
|
+
if (at === null) return null;
|
|
42940
|
+
return Math.max(0, Math.floor((at - now) / MINUTE));
|
|
42941
|
+
}
|
|
42915
42942
|
function formatRemaining(ms) {
|
|
42916
42943
|
if (!Number.isFinite(ms)) return "unknown";
|
|
42917
42944
|
if (ms <= 0) return "expired";
|
|
@@ -54615,18 +54642,66 @@ function displayPath(p) {
|
|
|
54615
54642
|
return s.startsWith(home + path6.sep) ? `~${s.slice(home.length)}` : s;
|
|
54616
54643
|
}
|
|
54617
54644
|
|
|
54618
|
-
// scripts/src/lib/
|
|
54645
|
+
// scripts/src/lib/version.mjs
|
|
54619
54646
|
import { createRequire } from "node:module";
|
|
54647
|
+
var PKG_VERSION = (() => {
|
|
54648
|
+
const requireFrom = createRequire(import.meta.url);
|
|
54649
|
+
for (const candidate of ["../../package.json", "../../../package.json"]) {
|
|
54650
|
+
try {
|
|
54651
|
+
const pkg = requireFrom(candidate);
|
|
54652
|
+
if (pkg?.name === "@rozoai/checkout" && pkg.version) return pkg.version;
|
|
54653
|
+
} catch {
|
|
54654
|
+
}
|
|
54655
|
+
}
|
|
54656
|
+
return "0.0.0";
|
|
54657
|
+
})();
|
|
54620
54658
|
|
|
54621
54659
|
// scripts/src/lib/http.mjs
|
|
54622
54660
|
var DEFAULT_TIMEOUT_MS = 2e4;
|
|
54623
|
-
var USER_AGENT =
|
|
54624
|
-
|
|
54661
|
+
var USER_AGENT = `rozo-checkout-skill/${PKG_VERSION}`;
|
|
54662
|
+
var MAX_RETRIES = 3;
|
|
54663
|
+
var MAX_TOTAL_WAIT_MS = 12e4;
|
|
54664
|
+
var FALLBACK_BACKOFF_MS = [2e3, 4e3, 8e3];
|
|
54665
|
+
var defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
54666
|
+
function parseRetryAfter(value, now = Date.now()) {
|
|
54667
|
+
if (value === null || value === void 0) return null;
|
|
54668
|
+
const s = String(value).trim();
|
|
54669
|
+
if (!s) return null;
|
|
54670
|
+
if (/^\d+(\.\d+)?$/.test(s)) return Math.round(Number(s) * 1e3);
|
|
54671
|
+
const at = Date.parse(s);
|
|
54672
|
+
if (!Number.isFinite(at)) return null;
|
|
54673
|
+
return Math.max(0, at - now);
|
|
54674
|
+
}
|
|
54675
|
+
function bodyRetryAfterMs(json) {
|
|
54676
|
+
const raw = json?.retryAfterSeconds ?? json?.error?.retryAfterSeconds;
|
|
54677
|
+
if (raw === null || raw === void 0 || raw === "") return null;
|
|
54678
|
+
const n = Number(raw);
|
|
54679
|
+
return Number.isFinite(n) && n >= 0 ? Math.round(n * 1e3) : null;
|
|
54680
|
+
}
|
|
54681
|
+
function rateLimitInfo(res) {
|
|
54682
|
+
const pick2 = (name) => res.headers?.get?.(name) ?? null;
|
|
54683
|
+
const info = {
|
|
54684
|
+
limit: pick2("x-ratelimit-limit"),
|
|
54685
|
+
remaining: pick2("x-ratelimit-remaining"),
|
|
54686
|
+
tier: pick2("x-ratelimit-tier"),
|
|
54687
|
+
scope: pick2("x-ratelimit-scope")
|
|
54688
|
+
};
|
|
54689
|
+
const present = Object.fromEntries(Object.entries(info).filter(([, v]) => v !== null));
|
|
54690
|
+
return Object.keys(present).length ? present : null;
|
|
54691
|
+
}
|
|
54692
|
+
function parseBody(text) {
|
|
54693
|
+
if (!text) return null;
|
|
54694
|
+
try {
|
|
54695
|
+
return JSON.parse(text);
|
|
54696
|
+
} catch {
|
|
54697
|
+
return null;
|
|
54698
|
+
}
|
|
54699
|
+
}
|
|
54700
|
+
async function fetchOnce(method, url, body, timeoutMs) {
|
|
54625
54701
|
const controller = new AbortController();
|
|
54626
54702
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
54627
|
-
let res;
|
|
54628
54703
|
try {
|
|
54629
|
-
res = await fetch(url, {
|
|
54704
|
+
const res = await fetch(url, {
|
|
54630
54705
|
method,
|
|
54631
54706
|
headers: {
|
|
54632
54707
|
accept: "application/json",
|
|
@@ -54636,6 +54711,8 @@ async function request(method, url, { body, timeoutMs = DEFAULT_TIMEOUT_MS } = {
|
|
|
54636
54711
|
body: body !== void 0 ? JSON.stringify(body) : void 0,
|
|
54637
54712
|
signal: controller.signal
|
|
54638
54713
|
});
|
|
54714
|
+
const text = await res.text();
|
|
54715
|
+
return { res, text };
|
|
54639
54716
|
} catch (err) {
|
|
54640
54717
|
throw new SkillError(
|
|
54641
54718
|
err?.name === "AbortError" ? "HTTP_TIMEOUT" : "HTTP_UNREACHABLE",
|
|
@@ -54645,31 +54722,54 @@ async function request(method, url, { body, timeoutMs = DEFAULT_TIMEOUT_MS } = {
|
|
|
54645
54722
|
} finally {
|
|
54646
54723
|
clearTimeout(timer);
|
|
54647
54724
|
}
|
|
54648
|
-
|
|
54649
|
-
|
|
54650
|
-
|
|
54651
|
-
|
|
54652
|
-
|
|
54653
|
-
|
|
54654
|
-
|
|
54725
|
+
}
|
|
54726
|
+
async function request(method, url, {
|
|
54727
|
+
body,
|
|
54728
|
+
timeoutMs = DEFAULT_TIMEOUT_MS,
|
|
54729
|
+
sleep: sleep2 = defaultSleep,
|
|
54730
|
+
now = Date.now,
|
|
54731
|
+
maxTotalWaitMs = MAX_TOTAL_WAIT_MS
|
|
54732
|
+
} = {}) {
|
|
54733
|
+
let waitedMs = 0;
|
|
54734
|
+
let retries = 0;
|
|
54735
|
+
for (; ; ) {
|
|
54736
|
+
const { res, text } = await fetchOnce(method, url, body, timeoutMs);
|
|
54737
|
+
const json = parseBody(text);
|
|
54738
|
+
if (res.ok) {
|
|
54739
|
+
if (json === null) {
|
|
54740
|
+
throw new SkillError("HTTP_BAD_JSON", "Endpoint returned a non-JSON body.", {
|
|
54741
|
+
url: redactUrl(url),
|
|
54742
|
+
snippet: redact(text).slice(0, 400)
|
|
54743
|
+
});
|
|
54744
|
+
}
|
|
54745
|
+
return json;
|
|
54746
|
+
}
|
|
54747
|
+
const headerWaitMs = parseRetryAfter(res.headers?.get?.("retry-after"), now());
|
|
54748
|
+
const hintMs = headerWaitMs ?? bodyRetryAfterMs(json);
|
|
54749
|
+
const retryable = res.status === 429 || res.status === 503 && hintMs !== null;
|
|
54750
|
+
if (retryable && retries < MAX_RETRIES) {
|
|
54751
|
+
const waitMs = hintMs ?? FALLBACK_BACKOFF_MS[retries];
|
|
54752
|
+
if (waitedMs + waitMs <= Math.min(maxTotalWaitMs, MAX_TOTAL_WAIT_MS)) {
|
|
54753
|
+
retries += 1;
|
|
54754
|
+
waitedMs += waitMs;
|
|
54755
|
+
await sleep2(waitMs);
|
|
54756
|
+
continue;
|
|
54757
|
+
}
|
|
54655
54758
|
}
|
|
54656
|
-
}
|
|
54657
|
-
if (!res.ok) {
|
|
54658
54759
|
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
54760
|
const message = json?.message || (typeof json?.error === "string" ? json.error : json?.error?.message) || `HTTP ${res.status}`;
|
|
54761
|
+
const rateLimit = rateLimitInfo(res);
|
|
54660
54762
|
throw new SkillError(code, redact(String(message)), {
|
|
54661
54763
|
httpStatus: res.status,
|
|
54662
54764
|
url: redactUrl(url),
|
|
54663
|
-
body: json ?? redact(text).slice(0, 800)
|
|
54664
|
-
|
|
54665
|
-
|
|
54666
|
-
|
|
54667
|
-
|
|
54668
|
-
|
|
54669
|
-
snippet: redact(text).slice(0, 400)
|
|
54765
|
+
body: json ?? redact(text).slice(0, 800),
|
|
54766
|
+
...retryable ? {
|
|
54767
|
+
retries,
|
|
54768
|
+
retryAfterSeconds: hintMs === null ? null : Math.ceil(hintMs / 1e3)
|
|
54769
|
+
} : {},
|
|
54770
|
+
...rateLimit ? { rateLimit } : {}
|
|
54670
54771
|
});
|
|
54671
54772
|
}
|
|
54672
|
-
return json;
|
|
54673
54773
|
}
|
|
54674
54774
|
function redactUrl(url) {
|
|
54675
54775
|
try {
|
|
@@ -54725,17 +54825,6 @@ function attributionIdentity(env = process.env) {
|
|
|
54725
54825
|
}
|
|
54726
54826
|
|
|
54727
54827
|
// 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
54828
|
var MPP_BASE = process.env.ROZO_CHECKOUT_MPP_BASE || "https://apiserver.mpprouter.dev/v1/services/rozo-agent-api";
|
|
54740
54829
|
var CLIENT_LABEL = `rozo-checkout-cli/${PKG_VERSION}`;
|
|
54741
54830
|
var ATTRIBUTION_CLIENT = `rozo-checkout-skill/${PKG_VERSION}`;
|
|
@@ -54755,6 +54844,7 @@ async function quoteInvoice({ url, linkId }) {
|
|
|
54755
54844
|
const body = url ? { url } : { payment_id: linkId };
|
|
54756
54845
|
return postJson(`${MPP_BASE}/quote-invoice`, body);
|
|
54757
54846
|
}
|
|
54847
|
+
var QUOTE_RECEIPT_RETRY_BUDGET_MS = 2e4;
|
|
54758
54848
|
async function createInvoice({ url, linkId, source, quoteReceipt, utmSource, email }) {
|
|
54759
54849
|
const body = {
|
|
54760
54850
|
...url ? { url } : { payment_id: linkId },
|
|
@@ -54766,7 +54856,11 @@ async function createInvoice({ url, linkId, source, quoteReceipt, utmSource, ema
|
|
|
54766
54856
|
// sent empty, when the user gave none.
|
|
54767
54857
|
...email ? { email } : {}
|
|
54768
54858
|
};
|
|
54769
|
-
return postJson(
|
|
54859
|
+
return postJson(
|
|
54860
|
+
`${MPP_BASE}/create-invoice`,
|
|
54861
|
+
body,
|
|
54862
|
+
quoteReceipt ? { maxTotalWaitMs: QUOTE_RECEIPT_RETRY_BUDGET_MS } : void 0
|
|
54863
|
+
);
|
|
54770
54864
|
}
|
|
54771
54865
|
async function createBitrefillInvoice({ invoice, source, utmSource, email }) {
|
|
54772
54866
|
const body = {
|
|
@@ -56451,6 +56545,16 @@ function statusGuidance(result) {
|
|
|
56451
56545
|
if (result.state === "expired_unfunded") return EXPIRED_UNFUNDED_GUIDANCE;
|
|
56452
56546
|
return result.terminal ? "Done." : "Still in flight. Poll again in ~10s.";
|
|
56453
56547
|
}
|
|
56548
|
+
function watchTimeoutMs(timeoutArg, result, now = Date.now()) {
|
|
56549
|
+
if (timeoutArg !== void 0 && timeoutArg !== null && timeoutArg !== true) {
|
|
56550
|
+
const n = Number(timeoutArg);
|
|
56551
|
+
if (Number.isFinite(n)) return Math.max(0, n * 1e3);
|
|
56552
|
+
}
|
|
56553
|
+
if (result?.payin?.chain === chainName("lightning")) {
|
|
56554
|
+
return lightningWatchMs(result?.expiry?.expiresAt, { now, fallbackMs: DEFAULT_WATCH_MS });
|
|
56555
|
+
}
|
|
56556
|
+
return DEFAULT_WATCH_MS;
|
|
56557
|
+
}
|
|
56454
56558
|
async function main4(argv) {
|
|
56455
56559
|
const args = parseArgs(argv);
|
|
56456
56560
|
const rozoPaymentId = args["rozo-payment-id"] || (isRozoPaymentId(args._[0]) ? args._[0] : null);
|
|
@@ -56466,9 +56570,9 @@ async function main4(argv) {
|
|
|
56466
56570
|
usage("Required: --rozo-payment-id <uuid> and/or --link-id <pl_* | paymentSession_*>");
|
|
56467
56571
|
}
|
|
56468
56572
|
const watch = Boolean(args.watch);
|
|
56469
|
-
const
|
|
56470
|
-
const deadline = Date.now() + timeoutMs;
|
|
56573
|
+
const started = Date.now();
|
|
56471
56574
|
let result = await snapshot({ rozoPaymentId, linkId, provider });
|
|
56575
|
+
const deadline = started + watchTimeoutMs(args.timeout, result, started);
|
|
56472
56576
|
const history = [{ at: (/* @__PURE__ */ new Date()).toISOString(), state: result.state }];
|
|
56473
56577
|
while (watch && !result.terminal && !result.escalate && !result.unknown && Date.now() < deadline) {
|
|
56474
56578
|
await sleep(POLL_INTERVAL_MS);
|
|
@@ -58106,6 +58210,773 @@ async function run8(argv = process.argv.slice(2)) {
|
|
|
58106
58210
|
return main8(argv);
|
|
58107
58211
|
}
|
|
58108
58212
|
|
|
58213
|
+
// scripts/src/lib/x402-args.mjs
|
|
58214
|
+
var X402_SUBCOMMANDS = ["topup", "pay", "balance"];
|
|
58215
|
+
var BOOLEAN = /* @__PURE__ */ new Set(["json", "dry-run", "help"]);
|
|
58216
|
+
var VALUE = /* @__PURE__ */ new Set(["with", "method", "body", "header", "max-usd", "prefer", "idempotency-key"]);
|
|
58217
|
+
var REPEATABLE = /* @__PURE__ */ new Set(["header"]);
|
|
58218
|
+
var UUID_RE2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
58219
|
+
var X402_HELP = `rozo-checkout x402: pay any x402 API from a prepaid Rozo balance.
|
|
58220
|
+
|
|
58221
|
+
USAGE
|
|
58222
|
+
npx @rozoai/checkout x402 topup <usd> --with <coin>
|
|
58223
|
+
npx @rozoai/checkout x402 pay <url> [--method POST] [--body '{...}'] [--header 'Name: value']
|
|
58224
|
+
npx @rozoai/checkout x402 balance
|
|
58225
|
+
|
|
58226
|
+
TOPUP COINS (--with)
|
|
58227
|
+
usdt-solana usdc-solana usdt-bnb usdc-bnb usdt-ethereum usdc-ethereum
|
|
58228
|
+
usdt-polygon usdc-polygon usdc-base usdc-stellar btc-lightning
|
|
58229
|
+
native (beta): eth-ethereum eth-base eth-arbitrum bnb-bnb sol-solana
|
|
58230
|
+
|
|
58231
|
+
PAY OPTIONS
|
|
58232
|
+
--method <m> HTTP method (default GET)
|
|
58233
|
+
--body <s> request body, sent as is
|
|
58234
|
+
--header 'Name: value' extra request header; repeat for more
|
|
58235
|
+
--max-usd <n> most this one call may cost (default 1.00)
|
|
58236
|
+
--idempotency-key <id> reuse after an interrupted run so you are not charged twice
|
|
58237
|
+
--dry-run read the 402 and show what would be paid; sign nothing
|
|
58238
|
+
|
|
58239
|
+
Payment leg: USDC on Base only (eip155:8453, x402 scheme "exact").
|
|
58240
|
+
Solana payment leg is coming later.
|
|
58241
|
+
Your request goes straight from this machine to the endpoint; Rozo only sees
|
|
58242
|
+
the 402 payment requirements it is asked to sign.`;
|
|
58243
|
+
function parseX402Args(argv) {
|
|
58244
|
+
const positional = [];
|
|
58245
|
+
const flags = { header: [] };
|
|
58246
|
+
for (let i = 0; i < argv.length; i++) {
|
|
58247
|
+
const a = argv[i];
|
|
58248
|
+
if (a === "--") {
|
|
58249
|
+
positional.push(...argv.slice(i + 1));
|
|
58250
|
+
break;
|
|
58251
|
+
}
|
|
58252
|
+
if (a.startsWith("--")) {
|
|
58253
|
+
let key = a.slice(2);
|
|
58254
|
+
let value;
|
|
58255
|
+
const eq = key.indexOf("=");
|
|
58256
|
+
if (eq !== -1) {
|
|
58257
|
+
value = key.slice(eq + 1);
|
|
58258
|
+
key = key.slice(0, eq);
|
|
58259
|
+
}
|
|
58260
|
+
if (BOOLEAN.has(key)) {
|
|
58261
|
+
flags[key] = value === void 0 ? true : value !== "false";
|
|
58262
|
+
continue;
|
|
58263
|
+
}
|
|
58264
|
+
if (VALUE.has(key)) {
|
|
58265
|
+
if (value === void 0) {
|
|
58266
|
+
value = argv[i + 1];
|
|
58267
|
+
if (value === void 0 || value.startsWith("--") && key !== "body") {
|
|
58268
|
+
throw new CliError("MISSING_VALUE", `--${key} needs a value.`);
|
|
58269
|
+
}
|
|
58270
|
+
i++;
|
|
58271
|
+
}
|
|
58272
|
+
if (REPEATABLE.has(key)) flags[key].push(value);
|
|
58273
|
+
else flags[key] = value;
|
|
58274
|
+
continue;
|
|
58275
|
+
}
|
|
58276
|
+
throw new CliError("UNKNOWN_FLAG", `Unknown option --${key} for x402.`);
|
|
58277
|
+
}
|
|
58278
|
+
if (a === "-j") {
|
|
58279
|
+
flags.json = true;
|
|
58280
|
+
continue;
|
|
58281
|
+
}
|
|
58282
|
+
if (a === "-h") {
|
|
58283
|
+
flags.help = true;
|
|
58284
|
+
continue;
|
|
58285
|
+
}
|
|
58286
|
+
positional.push(a);
|
|
58287
|
+
}
|
|
58288
|
+
const sub = positional[1];
|
|
58289
|
+
const json = Boolean(flags.json);
|
|
58290
|
+
if (flags.help || !sub || sub === "help") return { command: "x402", sub: "help", json };
|
|
58291
|
+
if (!X402_SUBCOMMANDS.includes(sub)) {
|
|
58292
|
+
throw new CliError("UNKNOWN_COMMAND", `Unknown x402 command "${sub}". Expected one of: ${X402_SUBCOMMANDS.join(", ")}.`);
|
|
58293
|
+
}
|
|
58294
|
+
if (sub === "balance") return { command: "x402", sub, json };
|
|
58295
|
+
if (sub === "topup") {
|
|
58296
|
+
const amount = positional[2];
|
|
58297
|
+
if (!amount) throw new CliError("MISSING_TARGET", "Usage: rozo-checkout x402 topup <usd> --with <coin>");
|
|
58298
|
+
if (!flags.with) throw new CliError("MISSING_PRESET", "A coin is required, e.g. --with usdt-solana");
|
|
58299
|
+
return { command: "x402", sub, json, amount, coin: flags.with };
|
|
58300
|
+
}
|
|
58301
|
+
const url = positional[2];
|
|
58302
|
+
if (!url) throw new CliError("MISSING_TARGET", "Usage: rozo-checkout x402 pay <url>");
|
|
58303
|
+
if (flags.prefer !== void 0 && !["base", "solana"].includes(String(flags.prefer).toLowerCase())) {
|
|
58304
|
+
throw new CliError("BAD_VALUE", "--prefer must be base or solana.");
|
|
58305
|
+
}
|
|
58306
|
+
if (flags["idempotency-key"] !== void 0 && !UUID_RE2.test(flags["idempotency-key"])) {
|
|
58307
|
+
throw new CliError("BAD_VALUE", "--idempotency-key must be a UUID (the one a previous run printed).");
|
|
58308
|
+
}
|
|
58309
|
+
if (flags.method !== void 0 && !/^[A-Za-z]+$/.test(flags.method)) {
|
|
58310
|
+
throw new CliError("BAD_VALUE", "--method must be an HTTP method like GET or POST.");
|
|
58311
|
+
}
|
|
58312
|
+
return {
|
|
58313
|
+
command: "x402",
|
|
58314
|
+
sub,
|
|
58315
|
+
json,
|
|
58316
|
+
url,
|
|
58317
|
+
method: (flags.method ?? "GET").toUpperCase(),
|
|
58318
|
+
body: flags.body,
|
|
58319
|
+
headers: flags.header,
|
|
58320
|
+
maxUsd: flags["max-usd"],
|
|
58321
|
+
prefer: flags.prefer ? String(flags.prefer).toLowerCase() : void 0,
|
|
58322
|
+
idempotencyKey: flags["idempotency-key"],
|
|
58323
|
+
dryRun: flags["dry-run"] === true
|
|
58324
|
+
};
|
|
58325
|
+
}
|
|
58326
|
+
function isX402Argv(argv) {
|
|
58327
|
+
const first = argv.find((a) => !a.startsWith("-"));
|
|
58328
|
+
return first === "x402";
|
|
58329
|
+
}
|
|
58330
|
+
|
|
58331
|
+
// scripts/src/x402.mjs
|
|
58332
|
+
import crypto8 from "node:crypto";
|
|
58333
|
+
|
|
58334
|
+
// scripts/src/lib/x402-key.mjs
|
|
58335
|
+
import fs7 from "node:fs";
|
|
58336
|
+
import path7 from "node:path";
|
|
58337
|
+
import os5 from "node:os";
|
|
58338
|
+
var KEY_ENV = "ROZO_CHECKOUT_X402_KEY";
|
|
58339
|
+
function keyPath() {
|
|
58340
|
+
const root = process.env.ROZO_CHECKOUT_STATE_DIR ? path7.dirname(stateRoot()) : path7.join(os5.homedir(), ".rozo-checkout");
|
|
58341
|
+
return path7.join(root, "x402-key");
|
|
58342
|
+
}
|
|
58343
|
+
function readAgentKey() {
|
|
58344
|
+
const fromEnv = process.env[KEY_ENV];
|
|
58345
|
+
if (fromEnv && fromEnv.trim()) return { key: fromEnv.trim(), source: "env" };
|
|
58346
|
+
try {
|
|
58347
|
+
const text = fs7.readFileSync(keyPath(), "utf8").trim();
|
|
58348
|
+
if (text) return { key: text, source: "file" };
|
|
58349
|
+
} catch {
|
|
58350
|
+
}
|
|
58351
|
+
return null;
|
|
58352
|
+
}
|
|
58353
|
+
function saveAgentKey(key) {
|
|
58354
|
+
const file = keyPath();
|
|
58355
|
+
fs7.mkdirSync(path7.dirname(file), { recursive: true, mode: 448 });
|
|
58356
|
+
const fd = fs7.openSync(file, "wx", 384);
|
|
58357
|
+
try {
|
|
58358
|
+
fs7.writeSync(fd, `${key}
|
|
58359
|
+
`);
|
|
58360
|
+
fs7.fsyncSync(fd);
|
|
58361
|
+
} finally {
|
|
58362
|
+
fs7.closeSync(fd);
|
|
58363
|
+
}
|
|
58364
|
+
return file;
|
|
58365
|
+
}
|
|
58366
|
+
|
|
58367
|
+
// scripts/src/lib/x402.mjs
|
|
58368
|
+
var NETWORK_BASE = "eip155:8453";
|
|
58369
|
+
var NETWORK_SOLANA = "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp";
|
|
58370
|
+
var USDC_ASSET = {
|
|
58371
|
+
[NETWORK_BASE]: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
|
|
58372
|
+
[NETWORK_SOLANA]: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"
|
|
58373
|
+
};
|
|
58374
|
+
var USDC_DECIMALS = 6;
|
|
58375
|
+
var PAYABLE_NETWORKS = /* @__PURE__ */ new Set([NETWORK_BASE]);
|
|
58376
|
+
var SOLANA_COMING_LATER = "Solana payment leg is coming later";
|
|
58377
|
+
var V1_NETWORK_ALIASES = {
|
|
58378
|
+
base: NETWORK_BASE,
|
|
58379
|
+
solana: NETWORK_SOLANA
|
|
58380
|
+
};
|
|
58381
|
+
function networkFamily(network) {
|
|
58382
|
+
if (network === NETWORK_BASE) return "evm";
|
|
58383
|
+
if (network === NETWORK_SOLANA) return "solana";
|
|
58384
|
+
return null;
|
|
58385
|
+
}
|
|
58386
|
+
function networkLabel(network) {
|
|
58387
|
+
if (network === NETWORK_BASE) return "Base";
|
|
58388
|
+
if (network === NETWORK_SOLANA) return "Solana";
|
|
58389
|
+
return String(network);
|
|
58390
|
+
}
|
|
58391
|
+
function decodeBase64Json(value) {
|
|
58392
|
+
if (typeof value !== "string" || !value.trim()) return null;
|
|
58393
|
+
try {
|
|
58394
|
+
const normalized = value.trim().replace(/-/g, "+").replace(/_/g, "/");
|
|
58395
|
+
const text = Buffer.from(normalized, "base64").toString("utf8");
|
|
58396
|
+
const parsed = JSON.parse(text);
|
|
58397
|
+
return parsed && typeof parsed === "object" ? parsed : null;
|
|
58398
|
+
} catch {
|
|
58399
|
+
return null;
|
|
58400
|
+
}
|
|
58401
|
+
}
|
|
58402
|
+
function encodeBase64Json(value) {
|
|
58403
|
+
return Buffer.from(JSON.stringify(value), "utf8").toString("base64");
|
|
58404
|
+
}
|
|
58405
|
+
function parseJsonText(text) {
|
|
58406
|
+
if (typeof text !== "string" || !text.trim()) return null;
|
|
58407
|
+
try {
|
|
58408
|
+
const parsed = JSON.parse(text);
|
|
58409
|
+
return parsed && typeof parsed === "object" ? parsed : null;
|
|
58410
|
+
} catch {
|
|
58411
|
+
return null;
|
|
58412
|
+
}
|
|
58413
|
+
}
|
|
58414
|
+
function parseChallenge({ status, getHeader, bodyText }) {
|
|
58415
|
+
if (status !== 402) {
|
|
58416
|
+
throw new SkillError("X402_NOT_402", `Expected HTTP 402, got ${status}.`);
|
|
58417
|
+
}
|
|
58418
|
+
const headerValue = getHeader("payment-required");
|
|
58419
|
+
let doc = headerValue ? decodeBase64Json(headerValue) : null;
|
|
58420
|
+
let source = doc ? "header" : null;
|
|
58421
|
+
if (!doc) {
|
|
58422
|
+
doc = parseJsonText(bodyText);
|
|
58423
|
+
source = doc ? "body" : null;
|
|
58424
|
+
}
|
|
58425
|
+
if (!doc || !Array.isArray(doc.accepts) || doc.accepts.length === 0) {
|
|
58426
|
+
throw new SkillError(
|
|
58427
|
+
"X402_BAD_CHALLENGE",
|
|
58428
|
+
"The endpoint returned 402 but no x402 payment requirements (no PAYMENT-REQUIRED header and no accepts list in the body)."
|
|
58429
|
+
);
|
|
58430
|
+
}
|
|
58431
|
+
const x402Version = Number(doc.x402Version) || (source === "header" ? 2 : 1);
|
|
58432
|
+
return {
|
|
58433
|
+
x402Version,
|
|
58434
|
+
source,
|
|
58435
|
+
resource: doc.resource ?? null,
|
|
58436
|
+
error: typeof doc.error === "string" ? doc.error : null,
|
|
58437
|
+
extensions: doc.extensions ?? null,
|
|
58438
|
+
accepts: doc.accepts
|
|
58439
|
+
};
|
|
58440
|
+
}
|
|
58441
|
+
function atomicAmount(req) {
|
|
58442
|
+
const raw = req?.amount ?? req?.maxAmountRequired;
|
|
58443
|
+
if (raw === null || raw === void 0) return null;
|
|
58444
|
+
const s = String(raw).trim();
|
|
58445
|
+
if (!/^\d+$/.test(s)) return null;
|
|
58446
|
+
return BigInt(s);
|
|
58447
|
+
}
|
|
58448
|
+
function formatUsdc(atomic) {
|
|
58449
|
+
const a = BigInt(atomic);
|
|
58450
|
+
const whole = a / 1000000n;
|
|
58451
|
+
const frac = (a % 1000000n).toString().padStart(USDC_DECIMALS, "0").replace(/0+$/, "");
|
|
58452
|
+
const fracOut = frac.length < 2 ? frac.padEnd(2, "0") : frac;
|
|
58453
|
+
return `${whole}.${fracOut}`;
|
|
58454
|
+
}
|
|
58455
|
+
function usdToAtomic(value) {
|
|
58456
|
+
const s = String(value ?? "").trim();
|
|
58457
|
+
if (!/^\d+(\.\d{1,6})?$/.test(s)) {
|
|
58458
|
+
throw new SkillError("BAD_VALUE", `"${value}" is not a USD amount like 0.50.`);
|
|
58459
|
+
}
|
|
58460
|
+
const [w, f = ""] = s.split(".");
|
|
58461
|
+
return BigInt(w) * 1000000n + BigInt(f.padEnd(USDC_DECIMALS, "0"));
|
|
58462
|
+
}
|
|
58463
|
+
function classifyRequirement(req) {
|
|
58464
|
+
if (!req || typeof req !== "object") return { supported: false, reason: "not an object" };
|
|
58465
|
+
const network = V1_NETWORK_ALIASES[req.network] ?? req.network;
|
|
58466
|
+
if (req.scheme !== "exact") {
|
|
58467
|
+
return { supported: false, reason: `scheme ${req.scheme} (only exact)`, network };
|
|
58468
|
+
}
|
|
58469
|
+
const usdc = USDC_ASSET[network];
|
|
58470
|
+
if (!usdc) {
|
|
58471
|
+
return { supported: false, reason: `network ${req.network} (only Base USDC)`, network };
|
|
58472
|
+
}
|
|
58473
|
+
if (!PAYABLE_NETWORKS.has(network)) {
|
|
58474
|
+
const reason = network === NETWORK_SOLANA ? `USDC on Solana (${SOLANA_COMING_LATER})` : `network ${req.network} (only Base USDC)`;
|
|
58475
|
+
return { supported: false, reason, network };
|
|
58476
|
+
}
|
|
58477
|
+
const assetOk = network === NETWORK_BASE ? String(req.asset ?? "").toLowerCase() === usdc.toLowerCase() : String(req.asset ?? "") === usdc;
|
|
58478
|
+
if (!assetOk) {
|
|
58479
|
+
return { supported: false, reason: `asset ${req.asset} on ${networkLabel(network)} is not USDC`, network };
|
|
58480
|
+
}
|
|
58481
|
+
const amount = atomicAmount(req);
|
|
58482
|
+
if (amount === null || amount <= 0n) {
|
|
58483
|
+
return { supported: false, reason: "missing or invalid amount", network };
|
|
58484
|
+
}
|
|
58485
|
+
if (typeof req.payTo !== "string" || !req.payTo.trim()) {
|
|
58486
|
+
return { supported: false, reason: "missing payTo", network };
|
|
58487
|
+
}
|
|
58488
|
+
return {
|
|
58489
|
+
supported: true,
|
|
58490
|
+
option: {
|
|
58491
|
+
network,
|
|
58492
|
+
asset: req.asset,
|
|
58493
|
+
payTo: req.payTo,
|
|
58494
|
+
amountAtomic: amount,
|
|
58495
|
+
amountUsd: formatUsdc(amount),
|
|
58496
|
+
requirement: req
|
|
58497
|
+
}
|
|
58498
|
+
};
|
|
58499
|
+
}
|
|
58500
|
+
function selectRequirement(challenge2, { budgetAtomic, prefer } = {}) {
|
|
58501
|
+
const classified = challenge2.accepts.map(classifyRequirement);
|
|
58502
|
+
const supported = classified.filter((c2) => c2.supported).map((c2) => c2.option);
|
|
58503
|
+
if (supported.length === 0) {
|
|
58504
|
+
const solanaOffered = classified.some((c2) => c2.network === NETWORK_SOLANA);
|
|
58505
|
+
throw new SkillError(
|
|
58506
|
+
"X402_UNSUPPORTED",
|
|
58507
|
+
"This endpoint does not accept a payment Rozo can sign. Rozo pays with USDC on Base (eip155:8453, scheme exact) only." + (solanaOffered ? ` It offers USDC on Solana; the ${SOLANA_COMING_LATER}.` : ""),
|
|
58508
|
+
{ offered: classified.map((c2) => c2.reason ?? "supported") }
|
|
58509
|
+
);
|
|
58510
|
+
}
|
|
58511
|
+
const within = supported.filter((o) => budgetAtomic === void 0 || o.amountAtomic <= budgetAtomic);
|
|
58512
|
+
if (within.length === 0) {
|
|
58513
|
+
const cheapest = supported.reduce((a, b) => b.amountAtomic < a.amountAtomic ? b : a);
|
|
58514
|
+
throw new SkillError(
|
|
58515
|
+
"X402_OVER_BUDGET",
|
|
58516
|
+
`The endpoint asks for ${cheapest.amountUsd} USDC, above your budget of ${formatUsdc(budgetAtomic)} USD. Raise --max-usd if you mean to pay that much.`,
|
|
58517
|
+
{ askedUsd: cheapest.amountUsd, budgetUsd: formatUsdc(budgetAtomic) }
|
|
58518
|
+
);
|
|
58519
|
+
}
|
|
58520
|
+
const preferNet = prefer === "base" ? NETWORK_BASE : prefer === "solana" ? NETWORK_SOLANA : null;
|
|
58521
|
+
within.sort((a, b) => {
|
|
58522
|
+
if (preferNet) {
|
|
58523
|
+
const pa = a.network === preferNet ? 0 : 1;
|
|
58524
|
+
const pb = b.network === preferNet ? 0 : 1;
|
|
58525
|
+
if (pa !== pb) return pa - pb;
|
|
58526
|
+
}
|
|
58527
|
+
return a.amountAtomic < b.amountAtomic ? -1 : a.amountAtomic > b.amountAtomic ? 1 : 0;
|
|
58528
|
+
});
|
|
58529
|
+
return within[0];
|
|
58530
|
+
}
|
|
58531
|
+
function paymentHeaderName(x402Version) {
|
|
58532
|
+
return Number(x402Version) >= 2 ? "PAYMENT-SIGNATURE" : "X-PAYMENT";
|
|
58533
|
+
}
|
|
58534
|
+
function paymentResponseHeaderNames() {
|
|
58535
|
+
return ["payment-response", "x-payment-response"];
|
|
58536
|
+
}
|
|
58537
|
+
function extractSignature(resp) {
|
|
58538
|
+
if (!resp || typeof resp !== "object") return null;
|
|
58539
|
+
const direct = resp.paymentSignature ?? resp.PAYMENT_SIGNATURE ?? resp["PAYMENT-SIGNATURE"] ?? resp.signature ?? resp.headerValue ?? resp.header?.value ?? null;
|
|
58540
|
+
if (typeof direct === "string" && direct.trim()) return direct.trim();
|
|
58541
|
+
if (resp.paymentPayload && typeof resp.paymentPayload === "object") {
|
|
58542
|
+
return encodeBase64Json(resp.paymentPayload);
|
|
58543
|
+
}
|
|
58544
|
+
return null;
|
|
58545
|
+
}
|
|
58546
|
+
function decodeSettlement(headerValue) {
|
|
58547
|
+
const doc = decodeBase64Json(headerValue);
|
|
58548
|
+
if (!doc) return null;
|
|
58549
|
+
return {
|
|
58550
|
+
success: doc.success ?? null,
|
|
58551
|
+
transaction: doc.transaction ?? doc.txHash ?? null,
|
|
58552
|
+
network: doc.network ?? null,
|
|
58553
|
+
payer: doc.payer ?? null,
|
|
58554
|
+
errorReason: doc.errorReason ?? null
|
|
58555
|
+
};
|
|
58556
|
+
}
|
|
58557
|
+
var TOPUP_PRESETS = {
|
|
58558
|
+
"usdt-solana": { chain: "900", token: "USDT" },
|
|
58559
|
+
"usdc-solana": { chain: "900", token: "USDC" },
|
|
58560
|
+
"usdt-bnb": { chain: "56", token: "USDT" },
|
|
58561
|
+
"usdc-bnb": { chain: "56", token: "USDC" },
|
|
58562
|
+
"usdt-ethereum": { chain: "1", token: "USDT" },
|
|
58563
|
+
"usdc-ethereum": { chain: "1", token: "USDC" },
|
|
58564
|
+
"usdt-polygon": { chain: "137", token: "USDT" },
|
|
58565
|
+
"usdc-polygon": { chain: "137", token: "USDC" },
|
|
58566
|
+
"usdc-base": { chain: "8453", token: "USDC" },
|
|
58567
|
+
"usdc-stellar": { chain: "1500", token: "USDC" },
|
|
58568
|
+
"btc-lightning": { chain: "lightning", token: "BTC" },
|
|
58569
|
+
"eth-ethereum": { chain: "1", token: "ETH", native: true },
|
|
58570
|
+
"eth-base": { chain: "8453", token: "ETH", native: true },
|
|
58571
|
+
"eth-arbitrum": { chain: "42161", token: "ETH", native: true },
|
|
58572
|
+
"bnb-bnb": { chain: "56", token: "BNB", native: true },
|
|
58573
|
+
"sol-solana": { chain: "900", token: "SOL", native: true }
|
|
58574
|
+
};
|
|
58575
|
+
var TOPUP_MIN_USD = 5;
|
|
58576
|
+
function resolveTopupPreset(value) {
|
|
58577
|
+
const key = String(value ?? "").trim().toLowerCase();
|
|
58578
|
+
const hit = TOPUP_PRESETS[key];
|
|
58579
|
+
if (!hit) {
|
|
58580
|
+
throw new SkillError(
|
|
58581
|
+
"BAD_PRESET",
|
|
58582
|
+
`"${value}" is not a topup coin. Known: ${Object.keys(TOPUP_PRESETS).join(", ")}`
|
|
58583
|
+
);
|
|
58584
|
+
}
|
|
58585
|
+
return { preset: key, ...hit };
|
|
58586
|
+
}
|
|
58587
|
+
function maskKey(key) {
|
|
58588
|
+
const s = String(key ?? "");
|
|
58589
|
+
if (s.length <= 10) return "<redacted>";
|
|
58590
|
+
return `${s.slice(0, 3)}\u2026${s.slice(-4)}`;
|
|
58591
|
+
}
|
|
58592
|
+
|
|
58593
|
+
// scripts/src/x402.mjs
|
|
58594
|
+
var X402_BASE = process.env.ROZO_CHECKOUT_X402_BASE || "https://apiserver.mpprouter.dev/v1/x402";
|
|
58595
|
+
var DISABLED_MESSAGE = "x402 payer not enabled for this key right now; nothing was charged. Try again later, or email hi@rozo.ai with the masked key.";
|
|
58596
|
+
var SIGN_RETRIES = 2;
|
|
58597
|
+
var SIGN_BACKOFF_MS = [1e3, 3e3];
|
|
58598
|
+
var API_TIMEOUT_MS = 2e4;
|
|
58599
|
+
var TARGET_TIMEOUT_MS = 6e4;
|
|
58600
|
+
var MAX_BODY_CHARS = 1e6;
|
|
58601
|
+
var DEFAULT_MAX_USD = "1.00";
|
|
58602
|
+
var defaultSleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
58603
|
+
var RETRYABLE_503 = /* @__PURE__ */ new Set(["X402_RETRY", "X402_PAYER_MODE_CHANGED", "X402_LEDGER_UNAVAILABLE"]);
|
|
58604
|
+
function isTransient(err) {
|
|
58605
|
+
if (!(err instanceof SkillError)) return false;
|
|
58606
|
+
if (err.code === "HTTP_TIMEOUT" || err.code === "HTTP_UNREACHABLE") return true;
|
|
58607
|
+
if (RETRYABLE_503.has(err.code)) return true;
|
|
58608
|
+
const status = err.details?.httpStatus;
|
|
58609
|
+
return status === 500 || status === 502 || status === 504 || status === 429;
|
|
58610
|
+
}
|
|
58611
|
+
async function x402Api(method, path8, { body, key, fetchImpl = globalThis.fetch, base = X402_BASE } = {}) {
|
|
58612
|
+
const url = `${base.replace(/\/+$/, "")}${path8}`;
|
|
58613
|
+
const controller = new AbortController();
|
|
58614
|
+
const timer = setTimeout(() => controller.abort(), API_TIMEOUT_MS);
|
|
58615
|
+
let res;
|
|
58616
|
+
let text;
|
|
58617
|
+
try {
|
|
58618
|
+
res = await fetchImpl(url, {
|
|
58619
|
+
method,
|
|
58620
|
+
headers: {
|
|
58621
|
+
accept: "application/json",
|
|
58622
|
+
"user-agent": USER_AGENT,
|
|
58623
|
+
"x-rozo-client": ATTRIBUTION_CLIENT,
|
|
58624
|
+
...key ? { authorization: `Bearer ${key}` } : {},
|
|
58625
|
+
...body !== void 0 ? { "content-type": "application/json" } : {}
|
|
58626
|
+
},
|
|
58627
|
+
body: body !== void 0 ? JSON.stringify(body) : void 0,
|
|
58628
|
+
signal: controller.signal
|
|
58629
|
+
});
|
|
58630
|
+
text = await res.text();
|
|
58631
|
+
} catch (err) {
|
|
58632
|
+
throw new SkillError(
|
|
58633
|
+
err?.name === "AbortError" ? "HTTP_TIMEOUT" : "HTTP_UNREACHABLE",
|
|
58634
|
+
`${method} ${path8} failed: ${redact(err?.message || String(err))}`,
|
|
58635
|
+
{ url: redactUrl(url) }
|
|
58636
|
+
);
|
|
58637
|
+
} finally {
|
|
58638
|
+
clearTimeout(timer);
|
|
58639
|
+
}
|
|
58640
|
+
let json = null;
|
|
58641
|
+
try {
|
|
58642
|
+
json = text ? JSON.parse(text) : null;
|
|
58643
|
+
} catch {
|
|
58644
|
+
json = null;
|
|
58645
|
+
}
|
|
58646
|
+
if (res.status === 503) {
|
|
58647
|
+
const serverCode = json?.code || json?.error?.code || null;
|
|
58648
|
+
if (RETRYABLE_503.has(serverCode)) {
|
|
58649
|
+
throw new SkillError(serverCode, redact(String(json?.error?.message || json?.message || "Retry.")), { httpStatus: 503 });
|
|
58650
|
+
}
|
|
58651
|
+
throw new SkillError("X402_PAYER_DISABLED", DISABLED_MESSAGE, { httpStatus: 503, ...serverCode ? { serverCode } : {} });
|
|
58652
|
+
}
|
|
58653
|
+
if (!res.ok) {
|
|
58654
|
+
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}`;
|
|
58655
|
+
const message = json?.message || (typeof json?.error === "string" ? json.error : json?.error?.message) || `HTTP ${res.status}`;
|
|
58656
|
+
throw new SkillError(code, redact(String(message)), {
|
|
58657
|
+
httpStatus: res.status,
|
|
58658
|
+
url: redactUrl(url),
|
|
58659
|
+
body: redactDeep(json ?? redact(String(text ?? "")).slice(0, 800))
|
|
58660
|
+
});
|
|
58661
|
+
}
|
|
58662
|
+
if (json === null || typeof json !== "object") {
|
|
58663
|
+
throw new SkillError("HTTP_BAD_JSON", `${method} ${path8} returned a non-JSON body.`, { url: redactUrl(url) });
|
|
58664
|
+
}
|
|
58665
|
+
return json;
|
|
58666
|
+
}
|
|
58667
|
+
function requireKey() {
|
|
58668
|
+
const found = readAgentKey();
|
|
58669
|
+
if (!found) {
|
|
58670
|
+
throw new SkillError(
|
|
58671
|
+
"X402_NO_KEY",
|
|
58672
|
+
`No x402 agent key yet. Run "rozo-checkout x402 topup <usd> --with <coin>" first; it creates one and stores it in ${keyPath()}. Or set ${KEY_ENV}.`
|
|
58673
|
+
);
|
|
58674
|
+
}
|
|
58675
|
+
return found;
|
|
58676
|
+
}
|
|
58677
|
+
function pick(obj, ...names) {
|
|
58678
|
+
for (const n of names) {
|
|
58679
|
+
const v = n.split(".").reduce((o, k) => o == null ? void 0 : o[k], obj);
|
|
58680
|
+
if (v !== void 0 && v !== null && v !== "") return v;
|
|
58681
|
+
}
|
|
58682
|
+
return null;
|
|
58683
|
+
}
|
|
58684
|
+
function normalizeTopup(resp) {
|
|
58685
|
+
return {
|
|
58686
|
+
orderId: pick(resp, "orderId", "rozoPaymentId", "paymentId", "id", "order.id"),
|
|
58687
|
+
address: pick(resp, "depositAddress", "receiverAddress", "address", "deposit.address", "order.receiverAddress"),
|
|
58688
|
+
memo: pick(resp, "memo", "receiverMemo", "deposit.memo"),
|
|
58689
|
+
memoType: pick(resp, "memoType", "receiverMemoType", "deposit.memoType"),
|
|
58690
|
+
lnInvoice: pick(resp, "lnInvoice", "invoice", "deposit.lnInvoice"),
|
|
58691
|
+
amount: pick(resp, "payAmount", "amountToSend", "deposit.amount", "amount"),
|
|
58692
|
+
token: pick(resp, "deposit.tokenSymbol", "deposit.token", "tokenSymbol", "token"),
|
|
58693
|
+
chain: pick(resp, "deposit.chainId", "deposit.chain", "chainId", "chain"),
|
|
58694
|
+
creditUsd: pick(resp, "creditUsd", "credit", "amountUsd"),
|
|
58695
|
+
expiresAt: pick(resp, "expiresAt", "deposit.expiresAt"),
|
|
58696
|
+
paymentLink: pick(resp, "paymentLink", "payUrl")
|
|
58697
|
+
};
|
|
58698
|
+
}
|
|
58699
|
+
async function runTopup({ amountUsd, coin }, { fetchImpl, blacklist } = {}) {
|
|
58700
|
+
const usd = Number(amountUsd);
|
|
58701
|
+
if (!/^\d+(\.\d{1,2})?$/.test(String(amountUsd ?? "").trim()) || !Number.isFinite(usd)) {
|
|
58702
|
+
throw new SkillError("BAD_VALUE", `"${amountUsd}" is not a USD amount like 20 or 20.50.`);
|
|
58703
|
+
}
|
|
58704
|
+
if (usd < TOPUP_MIN_USD) {
|
|
58705
|
+
throw new SkillError("BAD_VALUE", `Minimum topup is $${TOPUP_MIN_USD}.`);
|
|
58706
|
+
}
|
|
58707
|
+
const preset = resolveTopupPreset(coin);
|
|
58708
|
+
let found = readAgentKey();
|
|
58709
|
+
let created = null;
|
|
58710
|
+
if (!found) {
|
|
58711
|
+
const resp2 = await x402Api("POST", "/keys", { body: {}, fetchImpl });
|
|
58712
|
+
const key = pick(resp2, "key", "agentKey", "apiKey");
|
|
58713
|
+
if (typeof key !== "string" || !key.startsWith("ak_")) {
|
|
58714
|
+
throw new SkillError("X402_BAD_KEY", "Rozo did not return an agent key (expected ak_\u2026).");
|
|
58715
|
+
}
|
|
58716
|
+
const file = saveAgentKey(key);
|
|
58717
|
+
found = { key, source: "file" };
|
|
58718
|
+
created = { keyMasked: maskKey(key), storedAt: file };
|
|
58719
|
+
}
|
|
58720
|
+
const resp = await x402Api("POST", "/topup", {
|
|
58721
|
+
// {amount, token, chain} is the documented contract; `source` is the same
|
|
58722
|
+
// choice in create-invoice's shape, sent too because a router that reads
|
|
58723
|
+
// only `source` would otherwise fall back to its default coin.
|
|
58724
|
+
body: {
|
|
58725
|
+
amount: String(amountUsd).trim(),
|
|
58726
|
+
token: preset.token,
|
|
58727
|
+
chain: preset.chain,
|
|
58728
|
+
source: { chainId: preset.chain, tokenSymbol: preset.token }
|
|
58729
|
+
},
|
|
58730
|
+
key: found.key,
|
|
58731
|
+
fetchImpl
|
|
58732
|
+
});
|
|
58733
|
+
const deposit = normalizeTopup(resp);
|
|
58734
|
+
if (deposit.chain !== null && String(deposit.chain) !== preset.chain || deposit.token !== null && String(deposit.token).toUpperCase() !== preset.token) {
|
|
58735
|
+
throw new SkillError(
|
|
58736
|
+
"X402_TOPUP_MISMATCH",
|
|
58737
|
+
`Rozo returned a deposit for ${deposit.token ?? "?"} on chain ${deposit.chain ?? "?"}, not ${preset.token} on chain ${preset.chain}. Do not send anything; the address is withheld.`,
|
|
58738
|
+
{ orderId: deposit.orderId }
|
|
58739
|
+
);
|
|
58740
|
+
}
|
|
58741
|
+
if (!deposit.address && !deposit.lnInvoice) {
|
|
58742
|
+
throw new SkillError("X402_BAD_TOPUP", "Rozo returned a topup order without a deposit address.", {
|
|
58743
|
+
orderId: deposit.orderId
|
|
58744
|
+
});
|
|
58745
|
+
}
|
|
58746
|
+
if (deposit.address) {
|
|
58747
|
+
const family = /^0x[0-9a-fA-F]{40}$/.test(deposit.address) ? "evm" : preset.chain === "900" ? "solana" : "other";
|
|
58748
|
+
assertNotBlacklisted([{ address: deposit.address, family, role: "topup deposit address" }], blacklist ?? loadBlacklist());
|
|
58749
|
+
}
|
|
58750
|
+
return {
|
|
58751
|
+
success: true,
|
|
58752
|
+
command: "x402 topup",
|
|
58753
|
+
...created ? { keyCreated: created } : {},
|
|
58754
|
+
key: { masked: maskKey(found.key), source: found.source },
|
|
58755
|
+
coin: preset.preset,
|
|
58756
|
+
native: Boolean(preset.native),
|
|
58757
|
+
requestedUsd: String(amountUsd).trim(),
|
|
58758
|
+
deposit,
|
|
58759
|
+
note: "Send exactly once, from your own wallet. Your x402 balance is credited after the deposit confirms."
|
|
58760
|
+
};
|
|
58761
|
+
}
|
|
58762
|
+
async function runBalance({ fetchImpl } = {}) {
|
|
58763
|
+
const found = requireKey();
|
|
58764
|
+
const resp = await x402Api("GET", "/balance", { key: found.key, fetchImpl });
|
|
58765
|
+
return {
|
|
58766
|
+
success: true,
|
|
58767
|
+
command: "x402 balance",
|
|
58768
|
+
key: { masked: maskKey(found.key), source: found.source },
|
|
58769
|
+
balanceUsd: pick(resp, "balanceUsd", "balance", "available"),
|
|
58770
|
+
pendingUsd: pick(resp, "pendingUsd", "pending"),
|
|
58771
|
+
status: pick(resp, "status"),
|
|
58772
|
+
mode: pick(resp, "mode"),
|
|
58773
|
+
limits: pick(resp, "limits") ?? {
|
|
58774
|
+
perPaymentUsd: pick(resp, "perTxLimitUsd"),
|
|
58775
|
+
dailyUsd: pick(resp, "dailyLimitUsd"),
|
|
58776
|
+
spentTodayUsd: pick(resp, "spentTodayUsd"),
|
|
58777
|
+
payToAllowlist: pick(resp, "payToAllowlist")
|
|
58778
|
+
},
|
|
58779
|
+
recent: pick(resp, "recent", "payments", "history")
|
|
58780
|
+
};
|
|
58781
|
+
}
|
|
58782
|
+
function parseHeaderFlags(list) {
|
|
58783
|
+
const out2 = {};
|
|
58784
|
+
for (const raw of list ?? []) {
|
|
58785
|
+
const idx = String(raw).indexOf(":");
|
|
58786
|
+
if (idx <= 0) throw new SkillError("BAD_VALUE", `--header must look like "Name: value", got "${raw}".`);
|
|
58787
|
+
const name = raw.slice(0, idx).trim();
|
|
58788
|
+
const value = raw.slice(idx + 1).trim();
|
|
58789
|
+
if (!/^[A-Za-z0-9!#$%&'*+.^_`|~-]+$/.test(name)) {
|
|
58790
|
+
throw new SkillError("BAD_VALUE", `--header name "${name}" is not a valid HTTP header name.`);
|
|
58791
|
+
}
|
|
58792
|
+
if (/^(payment-signature|x-payment)$/i.test(name)) {
|
|
58793
|
+
throw new SkillError("BAD_VALUE", `--header ${name} is set by the payer; do not pass it.`);
|
|
58794
|
+
}
|
|
58795
|
+
out2[name] = value;
|
|
58796
|
+
}
|
|
58797
|
+
return out2;
|
|
58798
|
+
}
|
|
58799
|
+
async function callTarget(fetchImpl, { url, method, headers, body }, extraHeaders = {}) {
|
|
58800
|
+
const controller = new AbortController();
|
|
58801
|
+
const timer = setTimeout(() => controller.abort(), TARGET_TIMEOUT_MS);
|
|
58802
|
+
try {
|
|
58803
|
+
const res = await fetchImpl(url, {
|
|
58804
|
+
method,
|
|
58805
|
+
headers: { "user-agent": USER_AGENT, ...headers, ...extraHeaders },
|
|
58806
|
+
body: body ?? void 0,
|
|
58807
|
+
signal: controller.signal,
|
|
58808
|
+
redirect: "manual"
|
|
58809
|
+
});
|
|
58810
|
+
const text = await res.text();
|
|
58811
|
+
return { res, text };
|
|
58812
|
+
} catch (err) {
|
|
58813
|
+
throw new SkillError(
|
|
58814
|
+
err?.name === "AbortError" ? "HTTP_TIMEOUT" : "HTTP_UNREACHABLE",
|
|
58815
|
+
`${method} ${redactUrl(url)} failed: ${redact(err?.message || String(err))}`
|
|
58816
|
+
);
|
|
58817
|
+
} finally {
|
|
58818
|
+
clearTimeout(timer);
|
|
58819
|
+
}
|
|
58820
|
+
}
|
|
58821
|
+
function headerGetter(res) {
|
|
58822
|
+
return (name) => res.headers?.get?.(name) ?? null;
|
|
58823
|
+
}
|
|
58824
|
+
function bodyOut(text, contentType) {
|
|
58825
|
+
const truncated = text.length > MAX_BODY_CHARS;
|
|
58826
|
+
const s = truncated ? text.slice(0, MAX_BODY_CHARS) : text;
|
|
58827
|
+
if (/json/i.test(contentType ?? "") && !truncated) {
|
|
58828
|
+
try {
|
|
58829
|
+
return { body: JSON.parse(s), truncated };
|
|
58830
|
+
} catch {
|
|
58831
|
+
}
|
|
58832
|
+
}
|
|
58833
|
+
return { body: s, truncated };
|
|
58834
|
+
}
|
|
58835
|
+
async function signWithRetry(signBody, { key, fetchImpl, sleep: sleep2 = defaultSleep2, retries = SIGN_RETRIES }) {
|
|
58836
|
+
let attempt = 0;
|
|
58837
|
+
for (; ; ) {
|
|
58838
|
+
try {
|
|
58839
|
+
const resp = await x402Api("POST", "/sign", { body: signBody, key, fetchImpl });
|
|
58840
|
+
return { resp, attempts: attempt + 1 };
|
|
58841
|
+
} catch (err) {
|
|
58842
|
+
if (attempt < retries && isTransient(err)) {
|
|
58843
|
+
await sleep2(SIGN_BACKOFF_MS[attempt] ?? 3e3);
|
|
58844
|
+
attempt += 1;
|
|
58845
|
+
continue;
|
|
58846
|
+
}
|
|
58847
|
+
if (err instanceof SkillError) {
|
|
58848
|
+
err.details = { ...err.details ?? {}, idempotencyKey: signBody.idempotencyKey, attempts: attempt + 1 };
|
|
58849
|
+
}
|
|
58850
|
+
throw err;
|
|
58851
|
+
}
|
|
58852
|
+
}
|
|
58853
|
+
}
|
|
58854
|
+
async function runPay({ url, method = "GET", headers = {}, body, maxUsd = DEFAULT_MAX_USD, prefer, idempotencyKey, dryRun = false }, { fetchImpl = globalThis.fetch, sleep: sleep2 = defaultSleep2, blacklist, uuid = () => crypto8.randomUUID() } = {}) {
|
|
58855
|
+
let target;
|
|
58856
|
+
try {
|
|
58857
|
+
target = new URL(url);
|
|
58858
|
+
} catch {
|
|
58859
|
+
throw new SkillError("BAD_VALUE", `"${url}" is not a URL.`);
|
|
58860
|
+
}
|
|
58861
|
+
if (target.protocol !== "https:" && target.protocol !== "http:") {
|
|
58862
|
+
throw new SkillError("BAD_VALUE", "x402 pay only calls http(s) URLs.");
|
|
58863
|
+
}
|
|
58864
|
+
const budgetAtomic = usdToAtomic(maxUsd);
|
|
58865
|
+
const request2 = { url: target.toString(), method: method.toUpperCase(), headers, body };
|
|
58866
|
+
const first = await callTarget(fetchImpl, request2);
|
|
58867
|
+
if (first.res.status !== 402) {
|
|
58868
|
+
const { body: outBody2, truncated: truncated2 } = bodyOut(first.text, first.res.headers.get("content-type"));
|
|
58869
|
+
return {
|
|
58870
|
+
success: first.res.ok,
|
|
58871
|
+
command: "x402 pay",
|
|
58872
|
+
paid: false,
|
|
58873
|
+
status: first.res.status,
|
|
58874
|
+
contentType: first.res.headers.get("content-type"),
|
|
58875
|
+
body: outBody2,
|
|
58876
|
+
...truncated2 ? { truncated: truncated2 } : {},
|
|
58877
|
+
note: "The endpoint did not ask for payment (no 402). Nothing was charged."
|
|
58878
|
+
};
|
|
58879
|
+
}
|
|
58880
|
+
const challenge2 = parseChallenge({ status: 402, getHeader: headerGetter(first.res), bodyText: first.text });
|
|
58881
|
+
const option = selectRequirement(challenge2, { budgetAtomic, prefer });
|
|
58882
|
+
assertNotBlacklisted(
|
|
58883
|
+
[{ address: option.payTo, family: networkFamily(option.network), role: "x402 payTo" }],
|
|
58884
|
+
blacklist ?? loadBlacklist()
|
|
58885
|
+
);
|
|
58886
|
+
const key = idempotencyKey || uuid();
|
|
58887
|
+
const plan = {
|
|
58888
|
+
network: option.network,
|
|
58889
|
+
networkLabel: networkLabel(option.network),
|
|
58890
|
+
asset: option.asset,
|
|
58891
|
+
payTo: option.payTo,
|
|
58892
|
+
amountUsd: option.amountUsd,
|
|
58893
|
+
budgetUsd: formatUsdc(budgetAtomic),
|
|
58894
|
+
idempotencyKey: key
|
|
58895
|
+
};
|
|
58896
|
+
if (dryRun) {
|
|
58897
|
+
return {
|
|
58898
|
+
success: true,
|
|
58899
|
+
command: "x402 pay",
|
|
58900
|
+
paid: false,
|
|
58901
|
+
dryRun: true,
|
|
58902
|
+
x402Version: challenge2.x402Version,
|
|
58903
|
+
plan,
|
|
58904
|
+
note: "Dry run: the challenge was read and an option chosen; Rozo was not asked to sign and nothing was charged."
|
|
58905
|
+
};
|
|
58906
|
+
}
|
|
58907
|
+
const found = requireKey();
|
|
58908
|
+
const signBody = {
|
|
58909
|
+
x402Version: challenge2.x402Version,
|
|
58910
|
+
// Only the seller's own declared resource, never our request URL: a query
|
|
58911
|
+
// string can carry the caller's credential for that seller.
|
|
58912
|
+
...challenge2.resource ? { resource: challenge2.resource } : {},
|
|
58913
|
+
accepts: [option.requirement],
|
|
58914
|
+
budget: plan.budgetUsd,
|
|
58915
|
+
// Same cap under the name the router reads.
|
|
58916
|
+
maxAmountUsd: plan.budgetUsd,
|
|
58917
|
+
idempotencyKey: key
|
|
58918
|
+
};
|
|
58919
|
+
const { resp: signed, attempts } = await signWithRetry(signBody, { key: found.key, fetchImpl, sleep: sleep2 });
|
|
58920
|
+
const signature = extractSignature(signed);
|
|
58921
|
+
if (!signature) {
|
|
58922
|
+
throw new SkillError("X402_BAD_SIGNATURE", "Rozo answered /sign without a PAYMENT-SIGNATURE value.", {
|
|
58923
|
+
idempotencyKey: key
|
|
58924
|
+
});
|
|
58925
|
+
}
|
|
58926
|
+
const named = [signed.headerName, signed.header].find((h) => typeof h === "string" && /^[A-Za-z-]+$/.test(h));
|
|
58927
|
+
const headerName = named || paymentHeaderName(challenge2.x402Version);
|
|
58928
|
+
let replay;
|
|
58929
|
+
try {
|
|
58930
|
+
try {
|
|
58931
|
+
replay = await callTarget(fetchImpl, request2, { [headerName]: signature });
|
|
58932
|
+
} catch (err) {
|
|
58933
|
+
if (err.code !== "HTTP_TIMEOUT" && err.code !== "HTTP_UNREACHABLE") throw err;
|
|
58934
|
+
await sleep2(1e3);
|
|
58935
|
+
replay = await callTarget(fetchImpl, request2, { [headerName]: signature });
|
|
58936
|
+
}
|
|
58937
|
+
} catch (err) {
|
|
58938
|
+
throw new SkillError(
|
|
58939
|
+
"X402_REPLAY_FAILED",
|
|
58940
|
+
`Rozo signed the payment but the paid request failed: ${redact(err?.message || String(err))}. The seller may or may not have settled it. Check "x402 balance", then rerun with the same --idempotency-key.`,
|
|
58941
|
+
{ idempotencyKey: key, paymentId: pick(signed, "paymentId", "id"), signed: true, cause: err?.code ?? null }
|
|
58942
|
+
);
|
|
58943
|
+
}
|
|
58944
|
+
const settlementHeader = paymentResponseHeaderNames().map((n) => replay.res.headers.get(n)).find((v) => v);
|
|
58945
|
+
const settlement = settlementHeader ? decodeSettlement(settlementHeader) : null;
|
|
58946
|
+
const { body: outBody, truncated } = bodyOut(replay.text, replay.res.headers.get("content-type"));
|
|
58947
|
+
const payment = {
|
|
58948
|
+
...plan,
|
|
58949
|
+
paymentId: pick(signed, "paymentId", "id"),
|
|
58950
|
+
signAttempts: attempts,
|
|
58951
|
+
settlement
|
|
58952
|
+
};
|
|
58953
|
+
if (replay.res.status === 402) {
|
|
58954
|
+
let reason = null;
|
|
58955
|
+
try {
|
|
58956
|
+
reason = parseChallenge({ status: 402, getHeader: headerGetter(replay.res), bodyText: replay.text }).error;
|
|
58957
|
+
} catch {
|
|
58958
|
+
}
|
|
58959
|
+
throw new SkillError(
|
|
58960
|
+
"X402_PAYMENT_REJECTED",
|
|
58961
|
+
`The endpoint rejected the payment${reason ? `: ${redact(reason)}` : ""}. Do not retry with a new idempotency key; check "x402 balance" first.`,
|
|
58962
|
+
{ payment: redactDeep(payment) }
|
|
58963
|
+
);
|
|
58964
|
+
}
|
|
58965
|
+
return {
|
|
58966
|
+
success: replay.res.ok,
|
|
58967
|
+
command: "x402 pay",
|
|
58968
|
+
paid: true,
|
|
58969
|
+
status: replay.res.status,
|
|
58970
|
+
contentType: replay.res.headers.get("content-type"),
|
|
58971
|
+
payment,
|
|
58972
|
+
body: outBody,
|
|
58973
|
+
...truncated ? { truncated } : {},
|
|
58974
|
+
...replay.res.ok ? {} : {
|
|
58975
|
+
note: 'The payment was signed but the endpoint did not return success. The seller may still have settled it; check "x402 balance" before trying again, and reuse --idempotency-key to avoid a second charge.'
|
|
58976
|
+
}
|
|
58977
|
+
};
|
|
58978
|
+
}
|
|
58979
|
+
|
|
58109
58980
|
// scripts/src/cli.mjs
|
|
58110
58981
|
var SkillErrorLike = class extends Error {
|
|
58111
58982
|
constructor(code, message) {
|
|
@@ -58142,6 +59013,9 @@ function printError(payload) {
|
|
|
58142
59013
|
function printSupport() {
|
|
58143
59014
|
out(` ${dim(SUPPORT_TEXT)}`);
|
|
58144
59015
|
}
|
|
59016
|
+
function printBulkHint() {
|
|
59017
|
+
out(` ${dim(BULK_HINT_TEXT)}`);
|
|
59018
|
+
}
|
|
58145
59019
|
function printMoneyWarning(payload) {
|
|
58146
59020
|
if (!payload?.moneyDetected) return;
|
|
58147
59021
|
out();
|
|
@@ -58279,7 +59153,8 @@ async function cmdQuote(opts) {
|
|
|
58279
59153
|
return exitCode;
|
|
58280
59154
|
}
|
|
58281
59155
|
async function cmdStatus(opts) {
|
|
58282
|
-
const argv = [...targetToArgs(opts.target)
|
|
59156
|
+
const argv = [...targetToArgs(opts.target)];
|
|
59157
|
+
if (opts.timeout !== void 0) argv.push("--timeout", String(opts.timeout));
|
|
58283
59158
|
if (opts.provider) argv.push("--provider", opts.provider);
|
|
58284
59159
|
if (opts.watch) argv.push("--watch");
|
|
58285
59160
|
if (!opts.json) out(dim(" Checking payment status\u2026 (no money moves)"));
|
|
@@ -58340,6 +59215,7 @@ async function cmdReceipt(opts) {
|
|
|
58340
59215
|
out();
|
|
58341
59216
|
out(` Next: ${r.nextAction.type === "contact_support" ? red(r.nextAction.message) : r.nextAction.message}`);
|
|
58342
59217
|
if (payload.support) printSupport();
|
|
59218
|
+
if (r.paymentOutcome === "settled") printBulkHint();
|
|
58343
59219
|
out();
|
|
58344
59220
|
return exitCode;
|
|
58345
59221
|
}
|
|
@@ -58650,7 +59526,7 @@ async function cmdPay(opts) {
|
|
|
58650
59526
|
rozoPaymentId,
|
|
58651
59527
|
"--watch",
|
|
58652
59528
|
"--timeout",
|
|
58653
|
-
String(opts
|
|
59529
|
+
String(settlementWatchSeconds(opts, deposit))
|
|
58654
59530
|
]);
|
|
58655
59531
|
if (opts.json) {
|
|
58656
59532
|
printJson({
|
|
@@ -58670,6 +59546,7 @@ async function cmdPay(opts) {
|
|
|
58670
59546
|
}
|
|
58671
59547
|
printMoneyWarning(watched.payload);
|
|
58672
59548
|
if (watched.payload.state !== "settled") printSupport();
|
|
59549
|
+
else printBulkHint();
|
|
58673
59550
|
out();
|
|
58674
59551
|
return watched.exitCode;
|
|
58675
59552
|
}
|
|
@@ -58680,6 +59557,10 @@ function printDeposit(deposit, opts) {
|
|
|
58680
59557
|
if (deposit.lnInvoice) {
|
|
58681
59558
|
out(` Amount ${bold(`${deposit.amount} ${isSatsUnit(deposit.amountUnit) ? "sats" : deposit.tokenSymbol}`)}`);
|
|
58682
59559
|
out(` BOLT11 ${deposit.lnInvoice}`);
|
|
59560
|
+
const validMin = minutesRemaining(deposit.expiresAt);
|
|
59561
|
+
if (validMin !== null) {
|
|
59562
|
+
out(` Valid ${bold(validMin < 1 ? "under 1 more minute" : `${validMin} more minutes`)}`);
|
|
59563
|
+
}
|
|
58683
59564
|
} else {
|
|
58684
59565
|
out(` Amount ${bold(`${deposit.amount} ${deposit.tokenSymbol}`)}`);
|
|
58685
59566
|
out(` Chain ${bold(deposit.chain)}`);
|
|
@@ -58701,7 +59582,129 @@ function printDeposit(deposit, opts) {
|
|
|
58701
59582
|
out(dim(" Send it exactly once."));
|
|
58702
59583
|
out();
|
|
58703
59584
|
}
|
|
59585
|
+
function x402Printable(payload) {
|
|
59586
|
+
const { body, ...rest } = payload;
|
|
59587
|
+
if (rest.payment?.settlement?.transaction) {
|
|
59588
|
+
rest.payment = {
|
|
59589
|
+
...rest.payment,
|
|
59590
|
+
settlement: { ...rest.payment.settlement, transaction: publicHash(rest.payment.settlement.transaction) }
|
|
59591
|
+
};
|
|
59592
|
+
}
|
|
59593
|
+
const safe = redactDeep(rest);
|
|
59594
|
+
return body === void 0 ? safe : { ...safe, body };
|
|
59595
|
+
}
|
|
59596
|
+
async function cmdX402(opts) {
|
|
59597
|
+
if (opts.sub === "help") {
|
|
59598
|
+
if (opts.json) printJson({ success: true, help: X402_HELP, version: VERSION });
|
|
59599
|
+
else out(X402_HELP);
|
|
59600
|
+
return EXIT_OK;
|
|
59601
|
+
}
|
|
59602
|
+
let payload;
|
|
59603
|
+
try {
|
|
59604
|
+
if (opts.sub === "balance") payload = await runBalance();
|
|
59605
|
+
else if (opts.sub === "topup") payload = await runTopup({ amountUsd: opts.amount, coin: opts.coin });
|
|
59606
|
+
else {
|
|
59607
|
+
payload = await runPay({
|
|
59608
|
+
url: opts.url,
|
|
59609
|
+
method: opts.method,
|
|
59610
|
+
headers: parseHeaderFlags(opts.headers),
|
|
59611
|
+
body: opts.body,
|
|
59612
|
+
maxUsd: opts.maxUsd,
|
|
59613
|
+
prefer: opts.prefer,
|
|
59614
|
+
idempotencyKey: opts.idempotencyKey,
|
|
59615
|
+
dryRun: opts.dryRun
|
|
59616
|
+
});
|
|
59617
|
+
}
|
|
59618
|
+
} catch (err2) {
|
|
59619
|
+
const failure = formatFailure(err2);
|
|
59620
|
+
if (opts.json) printJson(failure);
|
|
59621
|
+
else {
|
|
59622
|
+
printError(failure);
|
|
59623
|
+
if (failure.error?.details?.idempotencyKey && failure.error.code !== "X402_PAYER_DISABLED") {
|
|
59624
|
+
out(` ${dim(`idempotencyKey ${failure.error.details.idempotencyKey} (reuse it with --idempotency-key to avoid a second charge)`)}`);
|
|
59625
|
+
}
|
|
59626
|
+
}
|
|
59627
|
+
return err2?.code === "BAD_VALUE" || err2?.code === "BAD_PRESET" ? EXIT_USAGE : EXIT_ERROR;
|
|
59628
|
+
}
|
|
59629
|
+
const printable = x402Printable(payload);
|
|
59630
|
+
if (opts.json) {
|
|
59631
|
+
printJson(printable);
|
|
59632
|
+
return payload.success ? EXIT_OK : EXIT_ERROR;
|
|
59633
|
+
}
|
|
59634
|
+
if (opts.sub === "balance") {
|
|
59635
|
+
out();
|
|
59636
|
+
out(` Balance ${bold(`$${printable.balanceUsd ?? "?"}`)}`);
|
|
59637
|
+
if (printable.pendingUsd) out(` Pending $${printable.pendingUsd}`);
|
|
59638
|
+
if (printable.limits) out(` Limits ${dim(JSON.stringify(printable.limits))}`);
|
|
59639
|
+
out(` Key ${printable.key.masked} ${dim(`(${printable.key.source})`)}`);
|
|
59640
|
+
out();
|
|
59641
|
+
return EXIT_OK;
|
|
59642
|
+
}
|
|
59643
|
+
if (opts.sub === "topup") {
|
|
59644
|
+
const d = printable.deposit;
|
|
59645
|
+
out();
|
|
59646
|
+
if (printable.keyCreated) {
|
|
59647
|
+
out(` ${green("New x402 agent key")} ${printable.keyCreated.keyMasked}, stored in ${printable.keyCreated.storedAt}`);
|
|
59648
|
+
out(` ${dim("Keep that file private: it owns your x402 balance.")}`);
|
|
59649
|
+
out();
|
|
59650
|
+
}
|
|
59651
|
+
out(` ${bold("Send exactly:")}`);
|
|
59652
|
+
out();
|
|
59653
|
+
if (d.lnInvoice) {
|
|
59654
|
+
if (d.amount) out(` Amount ${bold(String(d.amount))}`);
|
|
59655
|
+
out(` BOLT11 ${payload.deposit.lnInvoice}`);
|
|
59656
|
+
} else {
|
|
59657
|
+
out(` Amount ${bold(`${d.amount ?? "?"} ${d.token ?? ""}`.trim())}`);
|
|
59658
|
+
if (d.chain) out(` Chain ${bold(String(d.chain))}`);
|
|
59659
|
+
out(` Address ${bold(payload.deposit.address)}`);
|
|
59660
|
+
if (d.memo) out(` Memo ${bold(String(d.memo))} ${red(`(required${d.memoType ? `, ${d.memoType}` : ""})`)}`);
|
|
59661
|
+
}
|
|
59662
|
+
if (d.expiresAt) out(` Expires ${d.expiresAt}`);
|
|
59663
|
+
if (d.orderId) out(` Order ${d.orderId}`);
|
|
59664
|
+
out();
|
|
59665
|
+
out(dim(` ${printable.note}`));
|
|
59666
|
+
out(dim(" Then: rozo-checkout x402 balance"));
|
|
59667
|
+
out();
|
|
59668
|
+
return EXIT_OK;
|
|
59669
|
+
}
|
|
59670
|
+
const err = (s = "") => process.stderr.write(`${s}
|
|
59671
|
+
`);
|
|
59672
|
+
if (printable.dryRun) {
|
|
59673
|
+
const p = printable.plan;
|
|
59674
|
+
err(` Would pay ${bold(`${p.amountUsd} USDC`)} on ${p.networkLabel} to ${p.payTo} (budget ${p.budgetUsd}).`);
|
|
59675
|
+
err(dim(` idempotencyKey ${p.idempotencyKey}`));
|
|
59676
|
+
return EXIT_OK;
|
|
59677
|
+
}
|
|
59678
|
+
if (printable.paid) {
|
|
59679
|
+
const p = printable.payment;
|
|
59680
|
+
err(` ${green("Paid")} ${p.amountUsd} USDC on ${p.networkLabel} \u2192 HTTP ${printable.status}`);
|
|
59681
|
+
if (p.settlement?.transaction) err(dim(` tx ${p.settlement.transaction}`));
|
|
59682
|
+
err(dim(` idempotencyKey ${p.idempotencyKey}`));
|
|
59683
|
+
} else {
|
|
59684
|
+
err(dim(` HTTP ${printable.status}, no payment needed.`));
|
|
59685
|
+
}
|
|
59686
|
+
if (printable.note) err(yellow(` ${printable.note}`));
|
|
59687
|
+
const body = payload.body;
|
|
59688
|
+
process.stdout.write(typeof body === "string" ? body : JSON.stringify(body, null, 2));
|
|
59689
|
+
process.stdout.write("\n");
|
|
59690
|
+
return payload.success ? EXIT_OK : EXIT_ERROR;
|
|
59691
|
+
}
|
|
58704
59692
|
async function main9() {
|
|
59693
|
+
if (isX402Argv(process.argv.slice(2))) {
|
|
59694
|
+
let x402opts;
|
|
59695
|
+
try {
|
|
59696
|
+
x402opts = parseX402Args(process.argv.slice(2));
|
|
59697
|
+
} catch (err) {
|
|
59698
|
+
const payload = formatFailure(err);
|
|
59699
|
+
if (process.argv.includes("--json")) printJson(payload);
|
|
59700
|
+
else {
|
|
59701
|
+
printError(payload);
|
|
59702
|
+
out(dim("\n rozo-checkout x402 --help for usage."));
|
|
59703
|
+
}
|
|
59704
|
+
return EXIT_USAGE;
|
|
59705
|
+
}
|
|
59706
|
+
return cmdX402(x402opts);
|
|
59707
|
+
}
|
|
58705
59708
|
let opts;
|
|
58706
59709
|
try {
|
|
58707
59710
|
opts = parseCliArgs(process.argv.slice(2));
|