@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.
@@ -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]);
@@ -35,6 +37,7 @@ function redact(text) {
35
37
  return `${scheme}://${host}${hadMore ? "/<redacted>" : ""}`;
36
38
  });
37
39
  s = s.replace(/\b(bearer)\s+[A-Za-z0-9._~+/=-]{8,}/gi, "$1 <redacted>");
40
+ s = s.replace(/\bak_[A-Za-z0-9_-]{8,}/g, "ak_<redacted>");
38
41
  s = s.replace(
39
42
  /\b(api[-_]?key|apikey|access[-_]?token|auth[-_]?token|secret|token|password|passwd|pwd)\b(\s*[:=]\s*)("?)[A-Za-z0-9._~+/=-]{6,}\3/gi,
40
43
  "$1$2<redacted>"
@@ -139,18 +142,66 @@ function maskAddress(address) {
139
142
  return `${s.slice(0, 6)}...${s.slice(-4)}`;
140
143
  }
141
144
 
142
- // scripts/src/lib/api.mjs
145
+ // scripts/src/lib/version.mjs
143
146
  import { createRequire } from "node:module";
147
+ var PKG_VERSION = (() => {
148
+ const requireFrom = createRequire(import.meta.url);
149
+ for (const candidate of ["../../package.json", "../../../package.json"]) {
150
+ try {
151
+ const pkg = requireFrom(candidate);
152
+ if (pkg?.name === "@rozoai/checkout" && pkg.version) return pkg.version;
153
+ } catch {
154
+ }
155
+ }
156
+ return "0.0.0";
157
+ })();
144
158
 
145
159
  // scripts/src/lib/http.mjs
146
160
  var DEFAULT_TIMEOUT_MS = 2e4;
147
- var USER_AGENT = "rozo-checkout-skill/1.0";
148
- async function request(method, url, { body, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
161
+ var USER_AGENT = `rozo-checkout-skill/${PKG_VERSION}`;
162
+ var MAX_RETRIES = 3;
163
+ var MAX_TOTAL_WAIT_MS = 12e4;
164
+ var FALLBACK_BACKOFF_MS = [2e3, 4e3, 8e3];
165
+ var defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
166
+ function parseRetryAfter(value, now = Date.now()) {
167
+ if (value === null || value === void 0) return null;
168
+ const s = String(value).trim();
169
+ if (!s) return null;
170
+ if (/^\d+(\.\d+)?$/.test(s)) return Math.round(Number(s) * 1e3);
171
+ const at = Date.parse(s);
172
+ if (!Number.isFinite(at)) return null;
173
+ return Math.max(0, at - now);
174
+ }
175
+ function bodyRetryAfterMs(json) {
176
+ const raw = json?.retryAfterSeconds ?? json?.error?.retryAfterSeconds;
177
+ if (raw === null || raw === void 0 || raw === "") return null;
178
+ const n = Number(raw);
179
+ return Number.isFinite(n) && n >= 0 ? Math.round(n * 1e3) : null;
180
+ }
181
+ function rateLimitInfo(res) {
182
+ const pick = (name) => res.headers?.get?.(name) ?? null;
183
+ const info = {
184
+ limit: pick("x-ratelimit-limit"),
185
+ remaining: pick("x-ratelimit-remaining"),
186
+ tier: pick("x-ratelimit-tier"),
187
+ scope: pick("x-ratelimit-scope")
188
+ };
189
+ const present = Object.fromEntries(Object.entries(info).filter(([, v]) => v !== null));
190
+ return Object.keys(present).length ? present : null;
191
+ }
192
+ function parseBody(text) {
193
+ if (!text) return null;
194
+ try {
195
+ return JSON.parse(text);
196
+ } catch {
197
+ return null;
198
+ }
199
+ }
200
+ async function fetchOnce(method, url, body, timeoutMs) {
149
201
  const controller = new AbortController();
150
202
  const timer = setTimeout(() => controller.abort(), timeoutMs);
151
- let res;
152
203
  try {
153
- res = await fetch(url, {
204
+ const res = await fetch(url, {
154
205
  method,
155
206
  headers: {
156
207
  accept: "application/json",
@@ -160,6 +211,8 @@ async function request(method, url, { body, timeoutMs = DEFAULT_TIMEOUT_MS } = {
160
211
  body: body !== void 0 ? JSON.stringify(body) : void 0,
161
212
  signal: controller.signal
162
213
  });
214
+ const text = await res.text();
215
+ return { res, text };
163
216
  } catch (err) {
164
217
  throw new SkillError(
165
218
  err?.name === "AbortError" ? "HTTP_TIMEOUT" : "HTTP_UNREACHABLE",
@@ -169,31 +222,54 @@ async function request(method, url, { body, timeoutMs = DEFAULT_TIMEOUT_MS } = {
169
222
  } finally {
170
223
  clearTimeout(timer);
171
224
  }
172
- const text = await res.text();
173
- let json = null;
174
- if (text) {
175
- try {
176
- json = JSON.parse(text);
177
- } catch {
178
- json = null;
225
+ }
226
+ async function request(method, url, {
227
+ body,
228
+ timeoutMs = DEFAULT_TIMEOUT_MS,
229
+ sleep: sleep2 = defaultSleep,
230
+ now = Date.now,
231
+ maxTotalWaitMs = MAX_TOTAL_WAIT_MS
232
+ } = {}) {
233
+ let waitedMs = 0;
234
+ let retries = 0;
235
+ for (; ; ) {
236
+ const { res, text } = await fetchOnce(method, url, body, timeoutMs);
237
+ const json = parseBody(text);
238
+ if (res.ok) {
239
+ if (json === null) {
240
+ throw new SkillError("HTTP_BAD_JSON", "Endpoint returned a non-JSON body.", {
241
+ url: redactUrl(url),
242
+ snippet: redact(text).slice(0, 400)
243
+ });
244
+ }
245
+ return json;
246
+ }
247
+ const headerWaitMs = parseRetryAfter(res.headers?.get?.("retry-after"), now());
248
+ const hintMs = headerWaitMs ?? bodyRetryAfterMs(json);
249
+ const retryable = res.status === 429 || res.status === 503 && hintMs !== null;
250
+ if (retryable && retries < MAX_RETRIES) {
251
+ const waitMs = hintMs ?? FALLBACK_BACKOFF_MS[retries];
252
+ if (waitedMs + waitMs <= Math.min(maxTotalWaitMs, MAX_TOTAL_WAIT_MS)) {
253
+ retries += 1;
254
+ waitedMs += waitMs;
255
+ await sleep2(waitMs);
256
+ continue;
257
+ }
179
258
  }
180
- }
181
- if (!res.ok) {
182
259
  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
260
  const message = json?.message || (typeof json?.error === "string" ? json.error : json?.error?.message) || `HTTP ${res.status}`;
261
+ const rateLimit = rateLimitInfo(res);
184
262
  throw new SkillError(code, redact(String(message)), {
185
263
  httpStatus: res.status,
186
264
  url: redactUrl(url),
187
- body: json ?? redact(text).slice(0, 800)
265
+ body: json ?? redact(text).slice(0, 800),
266
+ ...retryable ? {
267
+ retries,
268
+ retryAfterSeconds: hintMs === null ? null : Math.ceil(hintMs / 1e3)
269
+ } : {},
270
+ ...rateLimit ? { rateLimit } : {}
188
271
  });
189
272
  }
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
273
  }
198
274
  function redactUrl(url) {
199
275
  try {
@@ -259,17 +335,6 @@ function findByLinkId(linkId) {
259
335
  }
260
336
 
261
337
  // scripts/src/lib/api.mjs
262
- var PKG_VERSION = (() => {
263
- const requireFrom = createRequire(import.meta.url);
264
- for (const candidate of ["../../package.json", "../../../package.json"]) {
265
- try {
266
- const pkg = requireFrom(candidate);
267
- if (pkg?.name === "@rozoai/checkout" && pkg.version) return pkg.version;
268
- } catch {
269
- }
270
- }
271
- return "0.0.0";
272
- })();
273
338
  var MPP_BASE = process.env.ROZO_CHECKOUT_MPP_BASE || "https://apiserver.mpprouter.dev/v1/services/rozo-agent-api";
274
339
  var CLIENT_LABEL = `rozo-checkout-cli/${PKG_VERSION}`;
275
340
  var ATTRIBUTION_CLIENT = `rozo-checkout-skill/${PKG_VERSION}`;
@@ -620,6 +685,15 @@ var MARGINS_MS = {
620
685
  };
621
686
  var BOLT11_MIN_VALIDITY_MS = 10 * MINUTE;
622
687
  var DEFAULT_MARGIN_MS = 10 * MINUTE;
688
+ var DEFAULT_WATCH_MS = 10 * MINUTE;
689
+ var LIGHTNING_WATCH_CAP_MS = 60 * MINUTE;
690
+ function lightningWatchMs(expiresAt, { now = Date.now(), fallbackMs = DEFAULT_WATCH_MS } = {}) {
691
+ const at = parseDeadline(expiresAt);
692
+ if (at === null) return fallbackMs;
693
+ const remaining = at - now;
694
+ if (!(remaining > 0)) return fallbackMs;
695
+ return Math.min(remaining, LIGHTNING_WATCH_CAP_MS);
696
+ }
623
697
  function formatRemaining(ms) {
624
698
  if (!Number.isFinite(ms)) return "unknown";
625
699
  if (ms <= 0) return "expired";
@@ -1088,6 +1162,16 @@ function statusGuidance(result) {
1088
1162
  if (result.state === "expired_unfunded") return EXPIRED_UNFUNDED_GUIDANCE;
1089
1163
  return result.terminal ? "Done." : "Still in flight. Poll again in ~10s.";
1090
1164
  }
1165
+ function watchTimeoutMs(timeoutArg, result, now = Date.now()) {
1166
+ if (timeoutArg !== void 0 && timeoutArg !== null && timeoutArg !== true) {
1167
+ const n = Number(timeoutArg);
1168
+ if (Number.isFinite(n)) return Math.max(0, n * 1e3);
1169
+ }
1170
+ if (result?.payin?.chain === chainName("lightning")) {
1171
+ return lightningWatchMs(result?.expiry?.expiresAt, { now, fallbackMs: DEFAULT_WATCH_MS });
1172
+ }
1173
+ return DEFAULT_WATCH_MS;
1174
+ }
1091
1175
  async function main(argv) {
1092
1176
  const args = parseArgs(argv);
1093
1177
  const rozoPaymentId = args["rozo-payment-id"] || (isRozoPaymentId(args._[0]) ? args._[0] : null);
@@ -1103,9 +1187,9 @@ async function main(argv) {
1103
1187
  usage("Required: --rozo-payment-id <uuid> and/or --link-id <pl_* | paymentSession_*>");
1104
1188
  }
1105
1189
  const watch = Boolean(args.watch);
1106
- const timeoutMs = Math.max(0, Number(args.timeout ?? 600) * 1e3);
1107
- const deadline = Date.now() + timeoutMs;
1190
+ const started = Date.now();
1108
1191
  let result = await snapshot({ rozoPaymentId, linkId, provider });
1192
+ const deadline = started + watchTimeoutMs(args.timeout, result, started);
1109
1193
  const history = [{ at: (/* @__PURE__ */ new Date()).toISOString(), state: result.state }];
1110
1194
  while (watch && !result.terminal && !result.escalate && !result.unknown && Date.now() < deadline) {
1111
1195
  await sleep(POLL_INTERVAL_MS);