@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
  var CONTACT_EMAIL_MAX_LENGTH = 254;
17
19
  var EMAIL_RE = /^[a-z0-9][^\s@]*@[^\s@.]+(\.[^\s@.]+)+$/;
18
20
  function normalizeContactEmail(raw) {
@@ -60,6 +62,7 @@ function redact(text) {
60
62
  return `${scheme}://${host}${hadMore ? "/<redacted>" : ""}`;
61
63
  });
62
64
  s = s.replace(/\b(bearer)\s+[A-Za-z0-9._~+/=-]{8,}/gi, "$1 <redacted>");
65
+ s = s.replace(/\bak_[A-Za-z0-9_-]{8,}/g, "ak_<redacted>");
63
66
  s = s.replace(
64
67
  /\b(api[-_]?key|apikey|access[-_]?token|auth[-_]?token|secret|token|password|passwd|pwd)\b(\s*[:=]\s*)("?)[A-Za-z0-9._~+/=-]{6,}\3/gi,
65
68
  "$1$2<redacted>"
@@ -198,18 +201,66 @@ function maskMemo(memo) {
198
201
  return `${s.slice(0, 4)}...${s.slice(-3)}`;
199
202
  }
200
203
 
201
- // scripts/src/lib/api.mjs
204
+ // scripts/src/lib/version.mjs
202
205
  import { createRequire } from "node:module";
206
+ var PKG_VERSION = (() => {
207
+ const requireFrom = createRequire(import.meta.url);
208
+ for (const candidate of ["../../package.json", "../../../package.json"]) {
209
+ try {
210
+ const pkg = requireFrom(candidate);
211
+ if (pkg?.name === "@rozoai/checkout" && pkg.version) return pkg.version;
212
+ } catch {
213
+ }
214
+ }
215
+ return "0.0.0";
216
+ })();
203
217
 
204
218
  // scripts/src/lib/http.mjs
205
219
  var DEFAULT_TIMEOUT_MS = 2e4;
206
- var USER_AGENT = "rozo-checkout-skill/1.0";
207
- async function request(method, url, { body, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
220
+ var USER_AGENT = `rozo-checkout-skill/${PKG_VERSION}`;
221
+ var MAX_RETRIES = 3;
222
+ var MAX_TOTAL_WAIT_MS = 12e4;
223
+ var FALLBACK_BACKOFF_MS = [2e3, 4e3, 8e3];
224
+ var defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
225
+ function parseRetryAfter(value, now = Date.now()) {
226
+ if (value === null || value === void 0) return null;
227
+ const s = String(value).trim();
228
+ if (!s) return null;
229
+ if (/^\d+(\.\d+)?$/.test(s)) return Math.round(Number(s) * 1e3);
230
+ const at = Date.parse(s);
231
+ if (!Number.isFinite(at)) return null;
232
+ return Math.max(0, at - now);
233
+ }
234
+ function bodyRetryAfterMs(json) {
235
+ const raw = json?.retryAfterSeconds ?? json?.error?.retryAfterSeconds;
236
+ if (raw === null || raw === void 0 || raw === "") return null;
237
+ const n = Number(raw);
238
+ return Number.isFinite(n) && n >= 0 ? Math.round(n * 1e3) : null;
239
+ }
240
+ function rateLimitInfo(res) {
241
+ const pick = (name) => res.headers?.get?.(name) ?? null;
242
+ const info = {
243
+ limit: pick("x-ratelimit-limit"),
244
+ remaining: pick("x-ratelimit-remaining"),
245
+ tier: pick("x-ratelimit-tier"),
246
+ scope: pick("x-ratelimit-scope")
247
+ };
248
+ const present = Object.fromEntries(Object.entries(info).filter(([, v]) => v !== null));
249
+ return Object.keys(present).length ? present : null;
250
+ }
251
+ function parseBody(text) {
252
+ if (!text) return null;
253
+ try {
254
+ return JSON.parse(text);
255
+ } catch {
256
+ return null;
257
+ }
258
+ }
259
+ async function fetchOnce(method, url, body, timeoutMs) {
208
260
  const controller = new AbortController();
209
261
  const timer = setTimeout(() => controller.abort(), timeoutMs);
210
- let res;
211
262
  try {
212
- res = await fetch(url, {
263
+ const res = await fetch(url, {
213
264
  method,
214
265
  headers: {
215
266
  accept: "application/json",
@@ -219,6 +270,8 @@ async function request(method, url, { body, timeoutMs = DEFAULT_TIMEOUT_MS } = {
219
270
  body: body !== void 0 ? JSON.stringify(body) : void 0,
220
271
  signal: controller.signal
221
272
  });
273
+ const text = await res.text();
274
+ return { res, text };
222
275
  } catch (err) {
223
276
  throw new SkillError(
224
277
  err?.name === "AbortError" ? "HTTP_TIMEOUT" : "HTTP_UNREACHABLE",
@@ -228,31 +281,54 @@ async function request(method, url, { body, timeoutMs = DEFAULT_TIMEOUT_MS } = {
228
281
  } finally {
229
282
  clearTimeout(timer);
230
283
  }
231
- const text = await res.text();
232
- let json = null;
233
- if (text) {
234
- try {
235
- json = JSON.parse(text);
236
- } catch {
237
- json = null;
284
+ }
285
+ async function request(method, url, {
286
+ body,
287
+ timeoutMs = DEFAULT_TIMEOUT_MS,
288
+ sleep = defaultSleep,
289
+ now = Date.now,
290
+ maxTotalWaitMs = MAX_TOTAL_WAIT_MS
291
+ } = {}) {
292
+ let waitedMs = 0;
293
+ let retries = 0;
294
+ for (; ; ) {
295
+ const { res, text } = await fetchOnce(method, url, body, timeoutMs);
296
+ const json = parseBody(text);
297
+ if (res.ok) {
298
+ if (json === null) {
299
+ throw new SkillError("HTTP_BAD_JSON", "Endpoint returned a non-JSON body.", {
300
+ url: redactUrl(url),
301
+ snippet: redact(text).slice(0, 400)
302
+ });
303
+ }
304
+ return json;
305
+ }
306
+ const headerWaitMs = parseRetryAfter(res.headers?.get?.("retry-after"), now());
307
+ const hintMs = headerWaitMs ?? bodyRetryAfterMs(json);
308
+ const retryable = res.status === 429 || res.status === 503 && hintMs !== null;
309
+ if (retryable && retries < MAX_RETRIES) {
310
+ const waitMs = hintMs ?? FALLBACK_BACKOFF_MS[retries];
311
+ if (waitedMs + waitMs <= Math.min(maxTotalWaitMs, MAX_TOTAL_WAIT_MS)) {
312
+ retries += 1;
313
+ waitedMs += waitMs;
314
+ await sleep(waitMs);
315
+ continue;
316
+ }
238
317
  }
239
- }
240
- if (!res.ok) {
241
318
  const code = json?.code || json?.error?.code || (typeof json?.error === "string" && /^[A-Z][A-Z0-9_]+$/.test(json.error) ? json.error : null) || `HTTP_${res.status}`;
242
319
  const message = json?.message || (typeof json?.error === "string" ? json.error : json?.error?.message) || `HTTP ${res.status}`;
320
+ const rateLimit = rateLimitInfo(res);
243
321
  throw new SkillError(code, redact(String(message)), {
244
322
  httpStatus: res.status,
245
323
  url: redactUrl(url),
246
- body: json ?? redact(text).slice(0, 800)
247
- });
248
- }
249
- if (json === null) {
250
- throw new SkillError("HTTP_BAD_JSON", "Endpoint returned a non-JSON body.", {
251
- url: redactUrl(url),
252
- snippet: redact(text).slice(0, 400)
324
+ body: json ?? redact(text).slice(0, 800),
325
+ ...retryable ? {
326
+ retries,
327
+ retryAfterSeconds: hintMs === null ? null : Math.ceil(hintMs / 1e3)
328
+ } : {},
329
+ ...rateLimit ? { rateLimit } : {}
253
330
  });
254
331
  }
255
- return json;
256
332
  }
257
333
  function redactUrl(url) {
258
334
  try {
@@ -582,17 +658,6 @@ function attributionIdentity(env = process.env) {
582
658
  }
583
659
 
584
660
  // scripts/src/lib/api.mjs
585
- var PKG_VERSION = (() => {
586
- const requireFrom = createRequire(import.meta.url);
587
- for (const candidate of ["../../package.json", "../../../package.json"]) {
588
- try {
589
- const pkg = requireFrom(candidate);
590
- if (pkg?.name === "@rozoai/checkout" && pkg.version) return pkg.version;
591
- } catch {
592
- }
593
- }
594
- return "0.0.0";
595
- })();
596
661
  var MPP_BASE = process.env.ROZO_CHECKOUT_MPP_BASE || "https://apiserver.mpprouter.dev/v1/services/rozo-agent-api";
597
662
  var CLIENT_LABEL = `rozo-checkout-cli/${PKG_VERSION}`;
598
663
  var ATTRIBUTION_CLIENT = `rozo-checkout-skill/${PKG_VERSION}`;
@@ -612,6 +677,7 @@ async function quoteInvoice({ url, linkId }) {
612
677
  const body = url ? { url } : { payment_id: linkId };
613
678
  return postJson(`${MPP_BASE}/quote-invoice`, body);
614
679
  }
680
+ var QUOTE_RECEIPT_RETRY_BUDGET_MS = 2e4;
615
681
  async function createInvoice({ url, linkId, source, quoteReceipt, utmSource, email }) {
616
682
  const body = {
617
683
  ...url ? { url } : { payment_id: linkId },
@@ -623,7 +689,11 @@ async function createInvoice({ url, linkId, source, quoteReceipt, utmSource, ema
623
689
  // sent empty, when the user gave none.
624
690
  ...email ? { email } : {}
625
691
  };
626
- return postJson(`${MPP_BASE}/create-invoice`, body);
692
+ return postJson(
693
+ `${MPP_BASE}/create-invoice`,
694
+ body,
695
+ quoteReceipt ? { maxTotalWaitMs: QUOTE_RECEIPT_RETRY_BUDGET_MS } : void 0
696
+ );
627
697
  }
628
698
  async function invoiceStatus({ linkId, rozoPaymentId }) {
629
699
  const qs = new URLSearchParams();
@@ -812,6 +882,8 @@ var MARGINS_MS = {
812
882
  };
813
883
  var BOLT11_MIN_VALIDITY_MS = 10 * MINUTE;
814
884
  var DEFAULT_MARGIN_MS = 10 * MINUTE;
885
+ var DEFAULT_WATCH_MS = 10 * MINUTE;
886
+ var LIGHTNING_WATCH_CAP_MS = 60 * MINUTE;
815
887
  function formatRemaining(ms) {
816
888
  if (!Number.isFinite(ms)) return "unknown";
817
889
  if (ms <= 0) return "expired";
@@ -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]);
@@ -34,6 +36,7 @@ function redact(text) {
34
36
  return `${scheme}://${host}${hadMore ? "/<redacted>" : ""}`;
35
37
  });
36
38
  s = s.replace(/\b(bearer)\s+[A-Za-z0-9._~+/=-]{8,}/gi, "$1 <redacted>");
39
+ s = s.replace(/\bak_[A-Za-z0-9_-]{8,}/g, "ak_<redacted>");
37
40
  s = s.replace(
38
41
  /\b(api[-_]?key|apikey|access[-_]?token|auth[-_]?token|secret|token|password|passwd|pwd)\b(\s*[:=]\s*)("?)[A-Za-z0-9._~+/=-]{6,}\3/gi,
39
42
  "$1$2<redacted>"
@@ -152,18 +155,66 @@ function extractLinkId(urlOrId) {
152
155
  );
153
156
  }
154
157
 
155
- // scripts/src/lib/api.mjs
158
+ // scripts/src/lib/version.mjs
156
159
  import { createRequire } from "node:module";
160
+ var PKG_VERSION = (() => {
161
+ const requireFrom = createRequire(import.meta.url);
162
+ for (const candidate of ["../../package.json", "../../../package.json"]) {
163
+ try {
164
+ const pkg = requireFrom(candidate);
165
+ if (pkg?.name === "@rozoai/checkout" && pkg.version) return pkg.version;
166
+ } catch {
167
+ }
168
+ }
169
+ return "0.0.0";
170
+ })();
157
171
 
158
172
  // scripts/src/lib/http.mjs
159
173
  var DEFAULT_TIMEOUT_MS = 2e4;
160
- var USER_AGENT = "rozo-checkout-skill/1.0";
161
- async function request(method, url, { body, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
174
+ var USER_AGENT = `rozo-checkout-skill/${PKG_VERSION}`;
175
+ var MAX_RETRIES = 3;
176
+ var MAX_TOTAL_WAIT_MS = 12e4;
177
+ var FALLBACK_BACKOFF_MS = [2e3, 4e3, 8e3];
178
+ var defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
179
+ function parseRetryAfter(value, now = Date.now()) {
180
+ if (value === null || value === void 0) return null;
181
+ const s = String(value).trim();
182
+ if (!s) return null;
183
+ if (/^\d+(\.\d+)?$/.test(s)) return Math.round(Number(s) * 1e3);
184
+ const at = Date.parse(s);
185
+ if (!Number.isFinite(at)) return null;
186
+ return Math.max(0, at - now);
187
+ }
188
+ function bodyRetryAfterMs(json) {
189
+ const raw = json?.retryAfterSeconds ?? json?.error?.retryAfterSeconds;
190
+ if (raw === null || raw === void 0 || raw === "") return null;
191
+ const n = Number(raw);
192
+ return Number.isFinite(n) && n >= 0 ? Math.round(n * 1e3) : null;
193
+ }
194
+ function rateLimitInfo(res) {
195
+ const pick = (name) => res.headers?.get?.(name) ?? null;
196
+ const info = {
197
+ limit: pick("x-ratelimit-limit"),
198
+ remaining: pick("x-ratelimit-remaining"),
199
+ tier: pick("x-ratelimit-tier"),
200
+ scope: pick("x-ratelimit-scope")
201
+ };
202
+ const present = Object.fromEntries(Object.entries(info).filter(([, v]) => v !== null));
203
+ return Object.keys(present).length ? present : null;
204
+ }
205
+ function parseBody(text) {
206
+ if (!text) return null;
207
+ try {
208
+ return JSON.parse(text);
209
+ } catch {
210
+ return null;
211
+ }
212
+ }
213
+ async function fetchOnce(method, url, body, timeoutMs) {
162
214
  const controller = new AbortController();
163
215
  const timer = setTimeout(() => controller.abort(), timeoutMs);
164
- let res;
165
216
  try {
166
- res = await fetch(url, {
217
+ const res = await fetch(url, {
167
218
  method,
168
219
  headers: {
169
220
  accept: "application/json",
@@ -173,6 +224,8 @@ async function request(method, url, { body, timeoutMs = DEFAULT_TIMEOUT_MS } = {
173
224
  body: body !== void 0 ? JSON.stringify(body) : void 0,
174
225
  signal: controller.signal
175
226
  });
227
+ const text = await res.text();
228
+ return { res, text };
176
229
  } catch (err) {
177
230
  throw new SkillError(
178
231
  err?.name === "AbortError" ? "HTTP_TIMEOUT" : "HTTP_UNREACHABLE",
@@ -182,31 +235,54 @@ async function request(method, url, { body, timeoutMs = DEFAULT_TIMEOUT_MS } = {
182
235
  } finally {
183
236
  clearTimeout(timer);
184
237
  }
185
- const text = await res.text();
186
- let json = null;
187
- if (text) {
188
- try {
189
- json = JSON.parse(text);
190
- } catch {
191
- json = null;
238
+ }
239
+ async function request(method, url, {
240
+ body,
241
+ timeoutMs = DEFAULT_TIMEOUT_MS,
242
+ sleep = defaultSleep,
243
+ now = Date.now,
244
+ maxTotalWaitMs = MAX_TOTAL_WAIT_MS
245
+ } = {}) {
246
+ let waitedMs = 0;
247
+ let retries = 0;
248
+ for (; ; ) {
249
+ const { res, text } = await fetchOnce(method, url, body, timeoutMs);
250
+ const json = parseBody(text);
251
+ if (res.ok) {
252
+ if (json === null) {
253
+ throw new SkillError("HTTP_BAD_JSON", "Endpoint returned a non-JSON body.", {
254
+ url: redactUrl(url),
255
+ snippet: redact(text).slice(0, 400)
256
+ });
257
+ }
258
+ return json;
259
+ }
260
+ const headerWaitMs = parseRetryAfter(res.headers?.get?.("retry-after"), now());
261
+ const hintMs = headerWaitMs ?? bodyRetryAfterMs(json);
262
+ const retryable = res.status === 429 || res.status === 503 && hintMs !== null;
263
+ if (retryable && retries < MAX_RETRIES) {
264
+ const waitMs = hintMs ?? FALLBACK_BACKOFF_MS[retries];
265
+ if (waitedMs + waitMs <= Math.min(maxTotalWaitMs, MAX_TOTAL_WAIT_MS)) {
266
+ retries += 1;
267
+ waitedMs += waitMs;
268
+ await sleep(waitMs);
269
+ continue;
270
+ }
192
271
  }
193
- }
194
- if (!res.ok) {
195
272
  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}`;
196
273
  const message = json?.message || (typeof json?.error === "string" ? json.error : json?.error?.message) || `HTTP ${res.status}`;
274
+ const rateLimit = rateLimitInfo(res);
197
275
  throw new SkillError(code, redact(String(message)), {
198
276
  httpStatus: res.status,
199
277
  url: redactUrl(url),
200
- body: json ?? redact(text).slice(0, 800)
278
+ body: json ?? redact(text).slice(0, 800),
279
+ ...retryable ? {
280
+ retries,
281
+ retryAfterSeconds: hintMs === null ? null : Math.ceil(hintMs / 1e3)
282
+ } : {},
283
+ ...rateLimit ? { rateLimit } : {}
201
284
  });
202
285
  }
203
- if (json === null) {
204
- throw new SkillError("HTTP_BAD_JSON", "Endpoint returned a non-JSON body.", {
205
- url: redactUrl(url),
206
- snippet: redact(text).slice(0, 400)
207
- });
208
- }
209
- return json;
210
286
  }
211
287
  function redactUrl(url) {
212
288
  try {
@@ -221,17 +297,6 @@ function postJson(url, body, opts) {
221
297
  }
222
298
 
223
299
  // scripts/src/lib/api.mjs
224
- var PKG_VERSION = (() => {
225
- const requireFrom = createRequire(import.meta.url);
226
- for (const candidate of ["../../package.json", "../../../package.json"]) {
227
- try {
228
- const pkg = requireFrom(candidate);
229
- if (pkg?.name === "@rozoai/checkout" && pkg.version) return pkg.version;
230
- } catch {
231
- }
232
- }
233
- return "0.0.0";
234
- })();
235
300
  var MPP_BASE = process.env.ROZO_CHECKOUT_MPP_BASE || "https://apiserver.mpprouter.dev/v1/services/rozo-agent-api";
236
301
  var CLIENT_LABEL = `rozo-checkout-cli/${PKG_VERSION}`;
237
302
  var ATTRIBUTION_CLIENT = `rozo-checkout-skill/${PKG_VERSION}`;
@@ -300,6 +365,8 @@ var MARGINS_MS = {
300
365
  };
301
366
  var BOLT11_MIN_VALIDITY_MS = 10 * MINUTE;
302
367
  var DEFAULT_MARGIN_MS = 10 * MINUTE;
368
+ var DEFAULT_WATCH_MS = 10 * MINUTE;
369
+ var LIGHTNING_WATCH_CAP_MS = 60 * MINUTE;
303
370
  function parseDeadline(value) {
304
371
  if (value === null || value === void 0 || value === "") return null;
305
372
  if (typeof value === "number" && Number.isFinite(value)) {
@@ -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]);
@@ -34,6 +36,7 @@ function redact(text) {
34
36
  return `${scheme}://${host}${hadMore ? "/<redacted>" : ""}`;
35
37
  });
36
38
  s = s.replace(/\b(bearer)\s+[A-Za-z0-9._~+/=-]{8,}/gi, "$1 <redacted>");
39
+ s = s.replace(/\bak_[A-Za-z0-9_-]{8,}/g, "ak_<redacted>");
37
40
  s = s.replace(
38
41
  /\b(api[-_]?key|apikey|access[-_]?token|auth[-_]?token|secret|token|password|passwd|pwd)\b(\s*[:=]\s*)("?)[A-Za-z0-9._~+/=-]{6,}\3/gi,
39
42
  "$1$2<redacted>"
@@ -138,18 +141,66 @@ function maskAddress(address) {
138
141
  return `${s.slice(0, 6)}...${s.slice(-4)}`;
139
142
  }
140
143
 
141
- // scripts/src/lib/api.mjs
144
+ // scripts/src/lib/version.mjs
142
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
+ })();
143
157
 
144
158
  // scripts/src/lib/http.mjs
145
159
  var DEFAULT_TIMEOUT_MS = 2e4;
146
- var USER_AGENT = "rozo-checkout-skill/1.0";
147
- async function request(method, url, { body, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
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) {
148
200
  const controller = new AbortController();
149
201
  const timer = setTimeout(() => controller.abort(), timeoutMs);
150
- let res;
151
202
  try {
152
- res = await fetch(url, {
203
+ const res = await fetch(url, {
153
204
  method,
154
205
  headers: {
155
206
  accept: "application/json",
@@ -159,6 +210,8 @@ async function request(method, url, { body, timeoutMs = DEFAULT_TIMEOUT_MS } = {
159
210
  body: body !== void 0 ? JSON.stringify(body) : void 0,
160
211
  signal: controller.signal
161
212
  });
213
+ const text = await res.text();
214
+ return { res, text };
162
215
  } catch (err) {
163
216
  throw new SkillError(
164
217
  err?.name === "AbortError" ? "HTTP_TIMEOUT" : "HTTP_UNREACHABLE",
@@ -168,31 +221,54 @@ async function request(method, url, { body, timeoutMs = DEFAULT_TIMEOUT_MS } = {
168
221
  } finally {
169
222
  clearTimeout(timer);
170
223
  }
171
- const text = await res.text();
172
- let json = null;
173
- if (text) {
174
- try {
175
- json = JSON.parse(text);
176
- } catch {
177
- json = null;
224
+ }
225
+ async function request(method, url, {
226
+ body,
227
+ timeoutMs = DEFAULT_TIMEOUT_MS,
228
+ sleep = 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 sleep(waitMs);
255
+ continue;
256
+ }
178
257
  }
179
- }
180
- if (!res.ok) {
181
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}`;
182
259
  const message = json?.message || (typeof json?.error === "string" ? json.error : json?.error?.message) || `HTTP ${res.status}`;
260
+ const rateLimit = rateLimitInfo(res);
183
261
  throw new SkillError(code, redact(String(message)), {
184
262
  httpStatus: res.status,
185
263
  url: redactUrl(url),
186
- body: json ?? redact(text).slice(0, 800)
187
- });
188
- }
189
- if (json === null) {
190
- throw new SkillError("HTTP_BAD_JSON", "Endpoint returned a non-JSON body.", {
191
- url: redactUrl(url),
192
- snippet: redact(text).slice(0, 400)
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 } : {}
193
270
  });
194
271
  }
195
- return json;
196
272
  }
197
273
  function redactUrl(url) {
198
274
  try {
@@ -258,17 +334,6 @@ function findByLinkId(linkId) {
258
334
  }
259
335
 
260
336
  // scripts/src/lib/api.mjs
261
- var PKG_VERSION = (() => {
262
- const requireFrom = createRequire(import.meta.url);
263
- for (const candidate of ["../../package.json", "../../../package.json"]) {
264
- try {
265
- const pkg = requireFrom(candidate);
266
- if (pkg?.name === "@rozoai/checkout" && pkg.version) return pkg.version;
267
- } catch {
268
- }
269
- }
270
- return "0.0.0";
271
- })();
272
337
  var MPP_BASE = process.env.ROZO_CHECKOUT_MPP_BASE || "https://apiserver.mpprouter.dev/v1/services/rozo-agent-api";
273
338
  var CLIENT_LABEL = `rozo-checkout-cli/${PKG_VERSION}`;
274
339
  var ATTRIBUTION_CLIENT = `rozo-checkout-skill/${PKG_VERSION}`;
@@ -619,6 +684,8 @@ var MARGINS_MS = {
619
684
  };
620
685
  var BOLT11_MIN_VALIDITY_MS = 10 * MINUTE;
621
686
  var DEFAULT_MARGIN_MS = 10 * MINUTE;
687
+ var DEFAULT_WATCH_MS = 10 * MINUTE;
688
+ var LIGHTNING_WATCH_CAP_MS = 60 * MINUTE;
622
689
  function formatRemaining(ms) {
623
690
  if (!Number.isFinite(ms)) return "unknown";
624
691
  if (ms <= 0) return "expired";