@rozoai/checkout 0.1.16 → 0.1.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +51 -2
- package/llms.txt +5 -2
- package/package.json +1 -1
- package/scripts/dist/cli.js +655 -123
- package/scripts/dist/create-order.js +423 -217
- package/scripts/dist/quote.js +99 -33
- package/scripts/dist/receipt.js +149 -83
- package/scripts/dist/resume.js +1552 -0
- package/scripts/dist/send-evm.js +298 -232
- package/scripts/dist/send-sol.js +298 -232
- package/scripts/dist/status.js +168 -85
package/scripts/dist/status.js
CHANGED
|
@@ -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
|
|
|
17
19
|
// scripts/src/lib/output.mjs
|
|
18
20
|
var PUBLIC_SUPPORT_URLS = /* @__PURE__ */ new Set([SUPPORT.x, SUPPORT.discord]);
|
|
@@ -139,18 +141,66 @@ function maskAddress(address) {
|
|
|
139
141
|
return `${s.slice(0, 6)}...${s.slice(-4)}`;
|
|
140
142
|
}
|
|
141
143
|
|
|
142
|
-
// scripts/src/lib/
|
|
144
|
+
// scripts/src/lib/version.mjs
|
|
143
145
|
import { createRequire } from "node:module";
|
|
146
|
+
var PKG_VERSION = (() => {
|
|
147
|
+
const requireFrom = createRequire(import.meta.url);
|
|
148
|
+
for (const candidate of ["../../package.json", "../../../package.json"]) {
|
|
149
|
+
try {
|
|
150
|
+
const pkg = requireFrom(candidate);
|
|
151
|
+
if (pkg?.name === "@rozoai/checkout" && pkg.version) return pkg.version;
|
|
152
|
+
} catch {
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return "0.0.0";
|
|
156
|
+
})();
|
|
144
157
|
|
|
145
158
|
// scripts/src/lib/http.mjs
|
|
146
159
|
var DEFAULT_TIMEOUT_MS = 2e4;
|
|
147
|
-
var USER_AGENT =
|
|
148
|
-
|
|
160
|
+
var USER_AGENT = `rozo-checkout-skill/${PKG_VERSION}`;
|
|
161
|
+
var MAX_RETRIES = 3;
|
|
162
|
+
var MAX_TOTAL_WAIT_MS = 12e4;
|
|
163
|
+
var FALLBACK_BACKOFF_MS = [2e3, 4e3, 8e3];
|
|
164
|
+
var defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
165
|
+
function parseRetryAfter(value, now = Date.now()) {
|
|
166
|
+
if (value === null || value === void 0) return null;
|
|
167
|
+
const s = String(value).trim();
|
|
168
|
+
if (!s) return null;
|
|
169
|
+
if (/^\d+(\.\d+)?$/.test(s)) return Math.round(Number(s) * 1e3);
|
|
170
|
+
const at = Date.parse(s);
|
|
171
|
+
if (!Number.isFinite(at)) return null;
|
|
172
|
+
return Math.max(0, at - now);
|
|
173
|
+
}
|
|
174
|
+
function bodyRetryAfterMs(json) {
|
|
175
|
+
const raw = json?.retryAfterSeconds ?? json?.error?.retryAfterSeconds;
|
|
176
|
+
if (raw === null || raw === void 0 || raw === "") return null;
|
|
177
|
+
const n = Number(raw);
|
|
178
|
+
return Number.isFinite(n) && n >= 0 ? Math.round(n * 1e3) : null;
|
|
179
|
+
}
|
|
180
|
+
function rateLimitInfo(res) {
|
|
181
|
+
const pick = (name) => res.headers?.get?.(name) ?? null;
|
|
182
|
+
const info = {
|
|
183
|
+
limit: pick("x-ratelimit-limit"),
|
|
184
|
+
remaining: pick("x-ratelimit-remaining"),
|
|
185
|
+
tier: pick("x-ratelimit-tier"),
|
|
186
|
+
scope: pick("x-ratelimit-scope")
|
|
187
|
+
};
|
|
188
|
+
const present = Object.fromEntries(Object.entries(info).filter(([, v]) => v !== null));
|
|
189
|
+
return Object.keys(present).length ? present : null;
|
|
190
|
+
}
|
|
191
|
+
function parseBody(text) {
|
|
192
|
+
if (!text) return null;
|
|
193
|
+
try {
|
|
194
|
+
return JSON.parse(text);
|
|
195
|
+
} catch {
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
async function fetchOnce(method, url, body, timeoutMs) {
|
|
149
200
|
const controller = new AbortController();
|
|
150
201
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
151
|
-
let res;
|
|
152
202
|
try {
|
|
153
|
-
res = await fetch(url, {
|
|
203
|
+
const res = await fetch(url, {
|
|
154
204
|
method,
|
|
155
205
|
headers: {
|
|
156
206
|
accept: "application/json",
|
|
@@ -160,6 +210,8 @@ async function request(method, url, { body, timeoutMs = DEFAULT_TIMEOUT_MS } = {
|
|
|
160
210
|
body: body !== void 0 ? JSON.stringify(body) : void 0,
|
|
161
211
|
signal: controller.signal
|
|
162
212
|
});
|
|
213
|
+
const text = await res.text();
|
|
214
|
+
return { res, text };
|
|
163
215
|
} catch (err) {
|
|
164
216
|
throw new SkillError(
|
|
165
217
|
err?.name === "AbortError" ? "HTTP_TIMEOUT" : "HTTP_UNREACHABLE",
|
|
@@ -169,31 +221,54 @@ async function request(method, url, { body, timeoutMs = DEFAULT_TIMEOUT_MS } = {
|
|
|
169
221
|
} finally {
|
|
170
222
|
clearTimeout(timer);
|
|
171
223
|
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
224
|
+
}
|
|
225
|
+
async function request(method, url, {
|
|
226
|
+
body,
|
|
227
|
+
timeoutMs = DEFAULT_TIMEOUT_MS,
|
|
228
|
+
sleep: sleep2 = defaultSleep,
|
|
229
|
+
now = Date.now,
|
|
230
|
+
maxTotalWaitMs = MAX_TOTAL_WAIT_MS
|
|
231
|
+
} = {}) {
|
|
232
|
+
let waitedMs = 0;
|
|
233
|
+
let retries = 0;
|
|
234
|
+
for (; ; ) {
|
|
235
|
+
const { res, text } = await fetchOnce(method, url, body, timeoutMs);
|
|
236
|
+
const json = parseBody(text);
|
|
237
|
+
if (res.ok) {
|
|
238
|
+
if (json === null) {
|
|
239
|
+
throw new SkillError("HTTP_BAD_JSON", "Endpoint returned a non-JSON body.", {
|
|
240
|
+
url: redactUrl(url),
|
|
241
|
+
snippet: redact(text).slice(0, 400)
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
return json;
|
|
245
|
+
}
|
|
246
|
+
const headerWaitMs = parseRetryAfter(res.headers?.get?.("retry-after"), now());
|
|
247
|
+
const hintMs = headerWaitMs ?? bodyRetryAfterMs(json);
|
|
248
|
+
const retryable = res.status === 429 || res.status === 503 && hintMs !== null;
|
|
249
|
+
if (retryable && retries < MAX_RETRIES) {
|
|
250
|
+
const waitMs = hintMs ?? FALLBACK_BACKOFF_MS[retries];
|
|
251
|
+
if (waitedMs + waitMs <= Math.min(maxTotalWaitMs, MAX_TOTAL_WAIT_MS)) {
|
|
252
|
+
retries += 1;
|
|
253
|
+
waitedMs += waitMs;
|
|
254
|
+
await sleep2(waitMs);
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
179
257
|
}
|
|
180
|
-
}
|
|
181
|
-
if (!res.ok) {
|
|
182
258
|
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}`;
|
|
183
259
|
const message = json?.message || (typeof json?.error === "string" ? json.error : json?.error?.message) || `HTTP ${res.status}`;
|
|
260
|
+
const rateLimit = rateLimitInfo(res);
|
|
184
261
|
throw new SkillError(code, redact(String(message)), {
|
|
185
262
|
httpStatus: res.status,
|
|
186
263
|
url: redactUrl(url),
|
|
187
|
-
body: json ?? redact(text).slice(0, 800)
|
|
264
|
+
body: json ?? redact(text).slice(0, 800),
|
|
265
|
+
...retryable ? {
|
|
266
|
+
retries,
|
|
267
|
+
retryAfterSeconds: hintMs === null ? null : Math.ceil(hintMs / 1e3)
|
|
268
|
+
} : {},
|
|
269
|
+
...rateLimit ? { rateLimit } : {}
|
|
188
270
|
});
|
|
189
271
|
}
|
|
190
|
-
if (json === null) {
|
|
191
|
-
throw new SkillError("HTTP_BAD_JSON", "Endpoint returned a non-JSON body.", {
|
|
192
|
-
url: redactUrl(url),
|
|
193
|
-
snippet: redact(text).slice(0, 400)
|
|
194
|
-
});
|
|
195
|
-
}
|
|
196
|
-
return json;
|
|
197
272
|
}
|
|
198
273
|
function redactUrl(url) {
|
|
199
274
|
try {
|
|
@@ -207,18 +282,58 @@ function getJson(url, opts) {
|
|
|
207
282
|
return request("GET", url, opts);
|
|
208
283
|
}
|
|
209
284
|
|
|
210
|
-
// scripts/src/lib/
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
285
|
+
// scripts/src/lib/state.mjs
|
|
286
|
+
import fs from "node:fs";
|
|
287
|
+
import path from "node:path";
|
|
288
|
+
import os from "node:os";
|
|
289
|
+
function stateRoot() {
|
|
290
|
+
return process.env.ROZO_CHECKOUT_STATE_DIR || path.join(os.homedir(), ".rozo-checkout", "state");
|
|
291
|
+
}
|
|
292
|
+
function statePath(rozoPaymentId) {
|
|
293
|
+
if (!/^[A-Za-z0-9-]{8,64}$/.test(String(rozoPaymentId || ""))) {
|
|
294
|
+
throw new SkillError("BAD_ROZO_PAYMENT_ID", "Refusing to build a state path from that id.");
|
|
295
|
+
}
|
|
296
|
+
return path.join(stateRoot(), `${rozoPaymentId}.json`);
|
|
297
|
+
}
|
|
298
|
+
function readState(rozoPaymentId) {
|
|
299
|
+
const file = statePath(rozoPaymentId);
|
|
300
|
+
let raw;
|
|
301
|
+
try {
|
|
302
|
+
raw = fs.readFileSync(file, "utf8");
|
|
303
|
+
} catch (err) {
|
|
304
|
+
if (err.code === "ENOENT") return null;
|
|
305
|
+
throw new SkillError("STATE_UNREADABLE", `Cannot read local state: ${err.code}`);
|
|
306
|
+
}
|
|
307
|
+
try {
|
|
308
|
+
return JSON.parse(raw);
|
|
309
|
+
} catch {
|
|
310
|
+
throw new SkillError(
|
|
311
|
+
"STATE_CORRUPT",
|
|
312
|
+
"The local state file for this order is corrupt. Refusing to act; inspect it manually."
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
function findByLinkId(linkId) {
|
|
317
|
+
const dir = stateRoot();
|
|
318
|
+
let files = [];
|
|
319
|
+
try {
|
|
320
|
+
files = fs.readdirSync(dir).filter((f) => f.endsWith(".json"));
|
|
321
|
+
} catch {
|
|
322
|
+
return null;
|
|
323
|
+
}
|
|
324
|
+
let best = null;
|
|
325
|
+
for (const f of files) {
|
|
214
326
|
try {
|
|
215
|
-
const
|
|
216
|
-
if (
|
|
327
|
+
const s = JSON.parse(fs.readFileSync(path.join(dir, f), "utf8"));
|
|
328
|
+
if (s?.linkId !== linkId) continue;
|
|
329
|
+
if (!best || String(s.createdAt) > String(best.createdAt)) best = s;
|
|
217
330
|
} catch {
|
|
218
331
|
}
|
|
219
332
|
}
|
|
220
|
-
return
|
|
221
|
-
}
|
|
333
|
+
return best;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// scripts/src/lib/api.mjs
|
|
222
337
|
var MPP_BASE = process.env.ROZO_CHECKOUT_MPP_BASE || "https://apiserver.mpprouter.dev/v1/services/rozo-agent-api";
|
|
223
338
|
var CLIENT_LABEL = `rozo-checkout-cli/${PKG_VERSION}`;
|
|
224
339
|
var ATTRIBUTION_CLIENT = `rozo-checkout-skill/${PKG_VERSION}`;
|
|
@@ -569,6 +684,15 @@ var MARGINS_MS = {
|
|
|
569
684
|
};
|
|
570
685
|
var BOLT11_MIN_VALIDITY_MS = 10 * MINUTE;
|
|
571
686
|
var DEFAULT_MARGIN_MS = 10 * MINUTE;
|
|
687
|
+
var DEFAULT_WATCH_MS = 10 * MINUTE;
|
|
688
|
+
var LIGHTNING_WATCH_CAP_MS = 60 * MINUTE;
|
|
689
|
+
function lightningWatchMs(expiresAt, { now = Date.now(), fallbackMs = DEFAULT_WATCH_MS } = {}) {
|
|
690
|
+
const at = parseDeadline(expiresAt);
|
|
691
|
+
if (at === null) return fallbackMs;
|
|
692
|
+
const remaining = at - now;
|
|
693
|
+
if (!(remaining > 0)) return fallbackMs;
|
|
694
|
+
return Math.min(remaining, LIGHTNING_WATCH_CAP_MS);
|
|
695
|
+
}
|
|
572
696
|
function formatRemaining(ms) {
|
|
573
697
|
if (!Number.isFinite(ms)) return "unknown";
|
|
574
698
|
if (ms <= 0) return "expired";
|
|
@@ -678,57 +802,6 @@ function checkExpiry({
|
|
|
678
802
|
return { ...withDeadline, ok: true, code: null, reason: null };
|
|
679
803
|
}
|
|
680
804
|
|
|
681
|
-
// scripts/src/lib/state.mjs
|
|
682
|
-
import fs from "node:fs";
|
|
683
|
-
import path from "node:path";
|
|
684
|
-
import os from "node:os";
|
|
685
|
-
function stateRoot() {
|
|
686
|
-
return process.env.ROZO_CHECKOUT_STATE_DIR || path.join(os.homedir(), ".rozo-checkout", "state");
|
|
687
|
-
}
|
|
688
|
-
function statePath(rozoPaymentId) {
|
|
689
|
-
if (!/^[A-Za-z0-9-]{8,64}$/.test(String(rozoPaymentId || ""))) {
|
|
690
|
-
throw new SkillError("BAD_ROZO_PAYMENT_ID", "Refusing to build a state path from that id.");
|
|
691
|
-
}
|
|
692
|
-
return path.join(stateRoot(), `${rozoPaymentId}.json`);
|
|
693
|
-
}
|
|
694
|
-
function readState(rozoPaymentId) {
|
|
695
|
-
const file = statePath(rozoPaymentId);
|
|
696
|
-
let raw;
|
|
697
|
-
try {
|
|
698
|
-
raw = fs.readFileSync(file, "utf8");
|
|
699
|
-
} catch (err) {
|
|
700
|
-
if (err.code === "ENOENT") return null;
|
|
701
|
-
throw new SkillError("STATE_UNREADABLE", `Cannot read local state: ${err.code}`);
|
|
702
|
-
}
|
|
703
|
-
try {
|
|
704
|
-
return JSON.parse(raw);
|
|
705
|
-
} catch {
|
|
706
|
-
throw new SkillError(
|
|
707
|
-
"STATE_CORRUPT",
|
|
708
|
-
"The local state file for this order is corrupt. Refusing to act; inspect it manually."
|
|
709
|
-
);
|
|
710
|
-
}
|
|
711
|
-
}
|
|
712
|
-
function findByLinkId(linkId) {
|
|
713
|
-
const dir = stateRoot();
|
|
714
|
-
let files = [];
|
|
715
|
-
try {
|
|
716
|
-
files = fs.readdirSync(dir).filter((f) => f.endsWith(".json"));
|
|
717
|
-
} catch {
|
|
718
|
-
return null;
|
|
719
|
-
}
|
|
720
|
-
let best = null;
|
|
721
|
-
for (const f of files) {
|
|
722
|
-
try {
|
|
723
|
-
const s = JSON.parse(fs.readFileSync(path.join(dir, f), "utf8"));
|
|
724
|
-
if (s?.linkId !== linkId) continue;
|
|
725
|
-
if (!best || String(s.createdAt) > String(best.createdAt)) best = s;
|
|
726
|
-
} catch {
|
|
727
|
-
}
|
|
728
|
-
}
|
|
729
|
-
return best;
|
|
730
|
-
}
|
|
731
|
-
|
|
732
805
|
// scripts/src/lib/bitrefill.mjs
|
|
733
806
|
var PROVIDER_BITREFILL = "bitrefill";
|
|
734
807
|
var BITREFILL_DESTINATION = Object.freeze({ chainId: "8453", tokenSymbol: "USDC" });
|
|
@@ -932,7 +1005,7 @@ function sendWindowFor({ provider, chainId, payment, status, bitrefillExpiry, no
|
|
|
932
1005
|
intentExpiresAt: payment?.expiresAt ?? deadline,
|
|
933
1006
|
coinbaseExpiry: deadline
|
|
934
1007
|
});
|
|
935
|
-
return expiry2.ok ? { ok: true, code: null, minutesOfSlack: Math.floor(expiry2.msOfSlack / 6e4) } : { ok: false, code: expiry2.code, reason: expiry2.reason };
|
|
1008
|
+
return expiry2.ok ? { ok: true, code: null, minutesOfSlack: Math.floor(expiry2.msOfSlack / 6e4), deadlineMs: expiry2.effectiveDeadlineMs } : { ok: false, code: expiry2.code, reason: expiry2.reason };
|
|
936
1009
|
}
|
|
937
1010
|
if (!status) {
|
|
938
1011
|
return { ok: false, code: "LINK_PAYABILITY_UNKNOWN", reason: "The Coinbase link state could not be read." };
|
|
@@ -945,7 +1018,7 @@ function sendWindowFor({ provider, chainId, payment, status, bitrefillExpiry, no
|
|
|
945
1018
|
intentExpiresAt: payment?.expiresAt ?? status?.rozoPayment?.expiresAt,
|
|
946
1019
|
coinbaseExpiry: status?.coinbase?.preApprovalExpiry
|
|
947
1020
|
});
|
|
948
|
-
return expiry.ok ? { ok: true, code: null, minutesOfSlack: Math.floor(expiry.msOfSlack / 6e4) } : { ok: false, code: expiry.code, reason: expiry.reason };
|
|
1021
|
+
return expiry.ok ? { ok: true, code: null, minutesOfSlack: Math.floor(expiry.msOfSlack / 6e4), deadlineMs: expiry.effectiveDeadlineMs } : { ok: false, code: expiry.code, reason: expiry.reason };
|
|
949
1022
|
}
|
|
950
1023
|
async function snapshot({ rozoPaymentId, linkId, provider: explicitProvider }) {
|
|
951
1024
|
let provider = resolveProvider(explicitProvider, rozoPaymentId);
|
|
@@ -1088,6 +1161,16 @@ function statusGuidance(result) {
|
|
|
1088
1161
|
if (result.state === "expired_unfunded") return EXPIRED_UNFUNDED_GUIDANCE;
|
|
1089
1162
|
return result.terminal ? "Done." : "Still in flight. Poll again in ~10s.";
|
|
1090
1163
|
}
|
|
1164
|
+
function watchTimeoutMs(timeoutArg, result, now = Date.now()) {
|
|
1165
|
+
if (timeoutArg !== void 0 && timeoutArg !== null && timeoutArg !== true) {
|
|
1166
|
+
const n = Number(timeoutArg);
|
|
1167
|
+
if (Number.isFinite(n)) return Math.max(0, n * 1e3);
|
|
1168
|
+
}
|
|
1169
|
+
if (result?.payin?.chain === chainName("lightning")) {
|
|
1170
|
+
return lightningWatchMs(result?.expiry?.expiresAt, { now, fallbackMs: DEFAULT_WATCH_MS });
|
|
1171
|
+
}
|
|
1172
|
+
return DEFAULT_WATCH_MS;
|
|
1173
|
+
}
|
|
1091
1174
|
async function main(argv) {
|
|
1092
1175
|
const args = parseArgs(argv);
|
|
1093
1176
|
const rozoPaymentId = args["rozo-payment-id"] || (isRozoPaymentId(args._[0]) ? args._[0] : null);
|
|
@@ -1103,9 +1186,9 @@ async function main(argv) {
|
|
|
1103
1186
|
usage("Required: --rozo-payment-id <uuid> and/or --link-id <pl_* | paymentSession_*>");
|
|
1104
1187
|
}
|
|
1105
1188
|
const watch = Boolean(args.watch);
|
|
1106
|
-
const
|
|
1107
|
-
const deadline = Date.now() + timeoutMs;
|
|
1189
|
+
const started = Date.now();
|
|
1108
1190
|
let result = await snapshot({ rozoPaymentId, linkId, provider });
|
|
1191
|
+
const deadline = started + watchTimeoutMs(args.timeout, result, started);
|
|
1109
1192
|
const history = [{ at: (/* @__PURE__ */ new Date()).toISOString(), state: result.state }];
|
|
1110
1193
|
while (watch && !result.terminal && !result.escalate && !result.unknown && Date.now() < deadline) {
|
|
1111
1194
|
await sleep(POLL_INTERVAL_MS);
|