@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]);
@@ -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>"
@@ -160,18 +163,66 @@ function maskMemo(memo) {
160
163
  return `${s.slice(0, 4)}...${s.slice(-3)}`;
161
164
  }
162
165
 
163
- // scripts/src/lib/api.mjs
166
+ // scripts/src/lib/version.mjs
164
167
  import { createRequire } from "node:module";
168
+ var PKG_VERSION = (() => {
169
+ const requireFrom = createRequire(import.meta.url);
170
+ for (const candidate of ["../../package.json", "../../../package.json"]) {
171
+ try {
172
+ const pkg = requireFrom(candidate);
173
+ if (pkg?.name === "@rozoai/checkout" && pkg.version) return pkg.version;
174
+ } catch {
175
+ }
176
+ }
177
+ return "0.0.0";
178
+ })();
165
179
 
166
180
  // scripts/src/lib/http.mjs
167
181
  var DEFAULT_TIMEOUT_MS = 2e4;
168
- var USER_AGENT = "rozo-checkout-skill/1.0";
169
- async function request(method, url, { body, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
182
+ var USER_AGENT = `rozo-checkout-skill/${PKG_VERSION}`;
183
+ var MAX_RETRIES = 3;
184
+ var MAX_TOTAL_WAIT_MS = 12e4;
185
+ var FALLBACK_BACKOFF_MS = [2e3, 4e3, 8e3];
186
+ var defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
187
+ function parseRetryAfter(value, now = Date.now()) {
188
+ if (value === null || value === void 0) return null;
189
+ const s = String(value).trim();
190
+ if (!s) return null;
191
+ if (/^\d+(\.\d+)?$/.test(s)) return Math.round(Number(s) * 1e3);
192
+ const at = Date.parse(s);
193
+ if (!Number.isFinite(at)) return null;
194
+ return Math.max(0, at - now);
195
+ }
196
+ function bodyRetryAfterMs(json) {
197
+ const raw = json?.retryAfterSeconds ?? json?.error?.retryAfterSeconds;
198
+ if (raw === null || raw === void 0 || raw === "") return null;
199
+ const n = Number(raw);
200
+ return Number.isFinite(n) && n >= 0 ? Math.round(n * 1e3) : null;
201
+ }
202
+ function rateLimitInfo(res) {
203
+ const pick = (name) => res.headers?.get?.(name) ?? null;
204
+ const info = {
205
+ limit: pick("x-ratelimit-limit"),
206
+ remaining: pick("x-ratelimit-remaining"),
207
+ tier: pick("x-ratelimit-tier"),
208
+ scope: pick("x-ratelimit-scope")
209
+ };
210
+ const present = Object.fromEntries(Object.entries(info).filter(([, v]) => v !== null));
211
+ return Object.keys(present).length ? present : null;
212
+ }
213
+ function parseBody(text) {
214
+ if (!text) return null;
215
+ try {
216
+ return JSON.parse(text);
217
+ } catch {
218
+ return null;
219
+ }
220
+ }
221
+ async function fetchOnce(method, url, body, timeoutMs) {
170
222
  const controller = new AbortController();
171
223
  const timer = setTimeout(() => controller.abort(), timeoutMs);
172
- let res;
173
224
  try {
174
- res = await fetch(url, {
225
+ const res = await fetch(url, {
175
226
  method,
176
227
  headers: {
177
228
  accept: "application/json",
@@ -181,6 +232,8 @@ async function request(method, url, { body, timeoutMs = DEFAULT_TIMEOUT_MS } = {
181
232
  body: body !== void 0 ? JSON.stringify(body) : void 0,
182
233
  signal: controller.signal
183
234
  });
235
+ const text = await res.text();
236
+ return { res, text };
184
237
  } catch (err) {
185
238
  throw new SkillError(
186
239
  err?.name === "AbortError" ? "HTTP_TIMEOUT" : "HTTP_UNREACHABLE",
@@ -190,31 +243,54 @@ async function request(method, url, { body, timeoutMs = DEFAULT_TIMEOUT_MS } = {
190
243
  } finally {
191
244
  clearTimeout(timer);
192
245
  }
193
- const text = await res.text();
194
- let json = null;
195
- if (text) {
196
- try {
197
- json = JSON.parse(text);
198
- } catch {
199
- json = null;
246
+ }
247
+ async function request(method, url, {
248
+ body,
249
+ timeoutMs = DEFAULT_TIMEOUT_MS,
250
+ sleep = defaultSleep,
251
+ now = Date.now,
252
+ maxTotalWaitMs = MAX_TOTAL_WAIT_MS
253
+ } = {}) {
254
+ let waitedMs = 0;
255
+ let retries = 0;
256
+ for (; ; ) {
257
+ const { res, text } = await fetchOnce(method, url, body, timeoutMs);
258
+ const json = parseBody(text);
259
+ if (res.ok) {
260
+ if (json === null) {
261
+ throw new SkillError("HTTP_BAD_JSON", "Endpoint returned a non-JSON body.", {
262
+ url: redactUrl(url),
263
+ snippet: redact(text).slice(0, 400)
264
+ });
265
+ }
266
+ return json;
267
+ }
268
+ const headerWaitMs = parseRetryAfter(res.headers?.get?.("retry-after"), now());
269
+ const hintMs = headerWaitMs ?? bodyRetryAfterMs(json);
270
+ const retryable = res.status === 429 || res.status === 503 && hintMs !== null;
271
+ if (retryable && retries < MAX_RETRIES) {
272
+ const waitMs = hintMs ?? FALLBACK_BACKOFF_MS[retries];
273
+ if (waitedMs + waitMs <= Math.min(maxTotalWaitMs, MAX_TOTAL_WAIT_MS)) {
274
+ retries += 1;
275
+ waitedMs += waitMs;
276
+ await sleep(waitMs);
277
+ continue;
278
+ }
200
279
  }
201
- }
202
- if (!res.ok) {
203
280
  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}`;
204
281
  const message = json?.message || (typeof json?.error === "string" ? json.error : json?.error?.message) || `HTTP ${res.status}`;
282
+ const rateLimit = rateLimitInfo(res);
205
283
  throw new SkillError(code, redact(String(message)), {
206
284
  httpStatus: res.status,
207
285
  url: redactUrl(url),
208
- body: json ?? redact(text).slice(0, 800)
209
- });
210
- }
211
- if (json === null) {
212
- throw new SkillError("HTTP_BAD_JSON", "Endpoint returned a non-JSON body.", {
213
- url: redactUrl(url),
214
- snippet: redact(text).slice(0, 400)
286
+ body: json ?? redact(text).slice(0, 800),
287
+ ...retryable ? {
288
+ retries,
289
+ retryAfterSeconds: hintMs === null ? null : Math.ceil(hintMs / 1e3)
290
+ } : {},
291
+ ...rateLimit ? { rateLimit } : {}
215
292
  });
216
293
  }
217
- return json;
218
294
  }
219
295
  function redactUrl(url) {
220
296
  try {
@@ -280,17 +356,6 @@ function findByLinkId(linkId) {
280
356
  }
281
357
 
282
358
  // scripts/src/lib/api.mjs
283
- var PKG_VERSION = (() => {
284
- const requireFrom = createRequire(import.meta.url);
285
- for (const candidate of ["../../package.json", "../../../package.json"]) {
286
- try {
287
- const pkg = requireFrom(candidate);
288
- if (pkg?.name === "@rozoai/checkout" && pkg.version) return pkg.version;
289
- } catch {
290
- }
291
- }
292
- return "0.0.0";
293
- })();
294
359
  var MPP_BASE = process.env.ROZO_CHECKOUT_MPP_BASE || "https://apiserver.mpprouter.dev/v1/services/rozo-agent-api";
295
360
  var CLIENT_LABEL = `rozo-checkout-cli/${PKG_VERSION}`;
296
361
  var ATTRIBUTION_CLIENT = `rozo-checkout-skill/${PKG_VERSION}`;
@@ -895,6 +960,8 @@ var MARGINS_MS = {
895
960
  };
896
961
  var BOLT11_MIN_VALIDITY_MS = 10 * MINUTE;
897
962
  var DEFAULT_MARGIN_MS = 10 * MINUTE;
963
+ var DEFAULT_WATCH_MS = 10 * MINUTE;
964
+ var LIGHTNING_WATCH_CAP_MS = 60 * MINUTE;
898
965
  function formatRemaining(ms) {
899
966
  if (!Number.isFinite(ms)) return "unknown";
900
967
  if (ms <= 0) return "expired";
@@ -9982,6 +9982,8 @@ var SUPPORT = Object.freeze({
9982
9982
  discord: "https://discord.gg/EfWejgTbuU"
9983
9983
  });
9984
9984
  var SUPPORT_TEXT = `Need help? Email ${SUPPORT.email}, or reach ROZO on X ${SUPPORT.x} or Discord ${SUPPORT.discord}.`;
9985
+ var BULK_DOCS_URL = "https://docs.rozo.ai/products/checkout/bulk-and-agents";
9986
+ var BULK_HINT_TEXT = `Buying regularly or for others? ${BULK_DOCS_URL}`;
9985
9987
 
9986
9988
  // scripts/src/lib/output.mjs
9987
9989
  var PUBLIC_SUPPORT_URLS = /* @__PURE__ */ new Set([SUPPORT.x, SUPPORT.discord]);
@@ -10003,6 +10005,7 @@ function redact(text) {
10003
10005
  return `${scheme}://${host}${hadMore ? "/<redacted>" : ""}`;
10004
10006
  });
10005
10007
  s = s.replace(/\b(bearer)\s+[A-Za-z0-9._~+/=-]{8,}/gi, "$1 <redacted>");
10008
+ s = s.replace(/\bak_[A-Za-z0-9_-]{8,}/g, "ak_<redacted>");
10006
10009
  s = s.replace(
10007
10010
  /\b(api[-_]?key|apikey|access[-_]?token|auth[-_]?token|secret|token|password|passwd|pwd)\b(\s*[:=]\s*)("?)[A-Za-z0-9._~+/=-]{6,}\3/gi,
10008
10011
  "$1$2<redacted>"
@@ -21849,18 +21852,66 @@ function promptPassphrase(prompt = " Keystore passphrase: ") {
21849
21852
  });
21850
21853
  }
21851
21854
 
21852
- // scripts/src/lib/api.mjs
21855
+ // scripts/src/lib/version.mjs
21853
21856
  import { createRequire } from "node:module";
21857
+ var PKG_VERSION = (() => {
21858
+ const requireFrom = createRequire(import.meta.url);
21859
+ for (const candidate of ["../../package.json", "../../../package.json"]) {
21860
+ try {
21861
+ const pkg = requireFrom(candidate);
21862
+ if (pkg?.name === "@rozoai/checkout" && pkg.version) return pkg.version;
21863
+ } catch {
21864
+ }
21865
+ }
21866
+ return "0.0.0";
21867
+ })();
21854
21868
 
21855
21869
  // scripts/src/lib/http.mjs
21856
21870
  var DEFAULT_TIMEOUT_MS = 2e4;
21857
- var USER_AGENT = "rozo-checkout-skill/1.0";
21858
- async function request(method, url, { body, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
21871
+ var USER_AGENT = `rozo-checkout-skill/${PKG_VERSION}`;
21872
+ var MAX_RETRIES = 3;
21873
+ var MAX_TOTAL_WAIT_MS = 12e4;
21874
+ var FALLBACK_BACKOFF_MS = [2e3, 4e3, 8e3];
21875
+ var defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
21876
+ function parseRetryAfter(value, now = Date.now()) {
21877
+ if (value === null || value === void 0) return null;
21878
+ const s = String(value).trim();
21879
+ if (!s) return null;
21880
+ if (/^\d+(\.\d+)?$/.test(s)) return Math.round(Number(s) * 1e3);
21881
+ const at = Date.parse(s);
21882
+ if (!Number.isFinite(at)) return null;
21883
+ return Math.max(0, at - now);
21884
+ }
21885
+ function bodyRetryAfterMs(json) {
21886
+ const raw = json?.retryAfterSeconds ?? json?.error?.retryAfterSeconds;
21887
+ if (raw === null || raw === void 0 || raw === "") return null;
21888
+ const n = Number(raw);
21889
+ return Number.isFinite(n) && n >= 0 ? Math.round(n * 1e3) : null;
21890
+ }
21891
+ function rateLimitInfo(res) {
21892
+ const pick = (name) => res.headers?.get?.(name) ?? null;
21893
+ const info = {
21894
+ limit: pick("x-ratelimit-limit"),
21895
+ remaining: pick("x-ratelimit-remaining"),
21896
+ tier: pick("x-ratelimit-tier"),
21897
+ scope: pick("x-ratelimit-scope")
21898
+ };
21899
+ const present = Object.fromEntries(Object.entries(info).filter(([, v]) => v !== null));
21900
+ return Object.keys(present).length ? present : null;
21901
+ }
21902
+ function parseBody(text) {
21903
+ if (!text) return null;
21904
+ try {
21905
+ return JSON.parse(text);
21906
+ } catch {
21907
+ return null;
21908
+ }
21909
+ }
21910
+ async function fetchOnce(method, url, body, timeoutMs) {
21859
21911
  const controller = new AbortController();
21860
21912
  const timer = setTimeout(() => controller.abort(), timeoutMs);
21861
- let res;
21862
21913
  try {
21863
- res = await fetch(url, {
21914
+ const res = await fetch(url, {
21864
21915
  method,
21865
21916
  headers: {
21866
21917
  accept: "application/json",
@@ -21870,6 +21921,8 @@ async function request(method, url, { body, timeoutMs = DEFAULT_TIMEOUT_MS } = {
21870
21921
  body: body !== void 0 ? JSON.stringify(body) : void 0,
21871
21922
  signal: controller.signal
21872
21923
  });
21924
+ const text = await res.text();
21925
+ return { res, text };
21873
21926
  } catch (err) {
21874
21927
  throw new SkillError(
21875
21928
  err?.name === "AbortError" ? "HTTP_TIMEOUT" : "HTTP_UNREACHABLE",
@@ -21879,31 +21932,54 @@ async function request(method, url, { body, timeoutMs = DEFAULT_TIMEOUT_MS } = {
21879
21932
  } finally {
21880
21933
  clearTimeout(timer);
21881
21934
  }
21882
- const text = await res.text();
21883
- let json = null;
21884
- if (text) {
21885
- try {
21886
- json = JSON.parse(text);
21887
- } catch {
21888
- json = null;
21935
+ }
21936
+ async function request(method, url, {
21937
+ body,
21938
+ timeoutMs = DEFAULT_TIMEOUT_MS,
21939
+ sleep = defaultSleep,
21940
+ now = Date.now,
21941
+ maxTotalWaitMs = MAX_TOTAL_WAIT_MS
21942
+ } = {}) {
21943
+ let waitedMs = 0;
21944
+ let retries = 0;
21945
+ for (; ; ) {
21946
+ const { res, text } = await fetchOnce(method, url, body, timeoutMs);
21947
+ const json = parseBody(text);
21948
+ if (res.ok) {
21949
+ if (json === null) {
21950
+ throw new SkillError("HTTP_BAD_JSON", "Endpoint returned a non-JSON body.", {
21951
+ url: redactUrl(url),
21952
+ snippet: redact(text).slice(0, 400)
21953
+ });
21954
+ }
21955
+ return json;
21956
+ }
21957
+ const headerWaitMs = parseRetryAfter(res.headers?.get?.("retry-after"), now());
21958
+ const hintMs = headerWaitMs ?? bodyRetryAfterMs(json);
21959
+ const retryable = res.status === 429 || res.status === 503 && hintMs !== null;
21960
+ if (retryable && retries < MAX_RETRIES) {
21961
+ const waitMs = hintMs ?? FALLBACK_BACKOFF_MS[retries];
21962
+ if (waitedMs + waitMs <= Math.min(maxTotalWaitMs, MAX_TOTAL_WAIT_MS)) {
21963
+ retries += 1;
21964
+ waitedMs += waitMs;
21965
+ await sleep(waitMs);
21966
+ continue;
21967
+ }
21889
21968
  }
21890
- }
21891
- if (!res.ok) {
21892
21969
  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}`;
21893
21970
  const message = json?.message || (typeof json?.error === "string" ? json.error : json?.error?.message) || `HTTP ${res.status}`;
21971
+ const rateLimit = rateLimitInfo(res);
21894
21972
  throw new SkillError(code, redact(String(message)), {
21895
21973
  httpStatus: res.status,
21896
21974
  url: redactUrl(url),
21897
- body: json ?? redact(text).slice(0, 800)
21898
- });
21899
- }
21900
- if (json === null) {
21901
- throw new SkillError("HTTP_BAD_JSON", "Endpoint returned a non-JSON body.", {
21902
- url: redactUrl(url),
21903
- snippet: redact(text).slice(0, 400)
21975
+ body: json ?? redact(text).slice(0, 800),
21976
+ ...retryable ? {
21977
+ retries,
21978
+ retryAfterSeconds: hintMs === null ? null : Math.ceil(hintMs / 1e3)
21979
+ } : {},
21980
+ ...rateLimit ? { rateLimit } : {}
21904
21981
  });
21905
21982
  }
21906
- return json;
21907
21983
  }
21908
21984
  function redactUrl(url) {
21909
21985
  try {
@@ -22111,17 +22187,6 @@ function assertPaymentLimit(invoiceUsd) {
22111
22187
  }
22112
22188
 
22113
22189
  // scripts/src/lib/api.mjs
22114
- var PKG_VERSION = (() => {
22115
- const requireFrom = createRequire(import.meta.url);
22116
- for (const candidate of ["../../package.json", "../../../package.json"]) {
22117
- try {
22118
- const pkg = requireFrom(candidate);
22119
- if (pkg?.name === "@rozoai/checkout" && pkg.version) return pkg.version;
22120
- } catch {
22121
- }
22122
- }
22123
- return "0.0.0";
22124
- })();
22125
22190
  var MPP_BASE = process.env.ROZO_CHECKOUT_MPP_BASE || "https://apiserver.mpprouter.dev/v1/services/rozo-agent-api";
22126
22191
  var CLIENT_LABEL = `rozo-checkout-cli/${PKG_VERSION}`;
22127
22192
  var ATTRIBUTION_CLIENT = `rozo-checkout-skill/${PKG_VERSION}`;
@@ -22359,6 +22424,8 @@ var MARGINS_MS = {
22359
22424
  };
22360
22425
  var BOLT11_MIN_VALIDITY_MS = 10 * MINUTE;
22361
22426
  var DEFAULT_MARGIN_MS = 10 * MINUTE;
22427
+ var DEFAULT_WATCH_MS = 10 * MINUTE;
22428
+ var LIGHTNING_WATCH_CAP_MS = 60 * MINUTE;
22362
22429
  function marginFor(chainId) {
22363
22430
  const key = String(chainId);
22364
22431
  return MARGINS_MS[key] ?? MARGINS_MS[chainId] ?? DEFAULT_MARGIN_MS;
@@ -32150,6 +32150,8 @@ var SUPPORT = Object.freeze({
32150
32150
  discord: "https://discord.gg/EfWejgTbuU"
32151
32151
  });
32152
32152
  var SUPPORT_TEXT = `Need help? Email ${SUPPORT.email}, or reach ROZO on X ${SUPPORT.x} or Discord ${SUPPORT.discord}.`;
32153
+ var BULK_DOCS_URL = "https://docs.rozo.ai/products/checkout/bulk-and-agents";
32154
+ var BULK_HINT_TEXT = `Buying regularly or for others? ${BULK_DOCS_URL}`;
32153
32155
 
32154
32156
  // scripts/src/lib/output.mjs
32155
32157
  var PUBLIC_SUPPORT_URLS = /* @__PURE__ */ new Set([SUPPORT.x, SUPPORT.discord]);
@@ -32171,6 +32173,7 @@ function redact(text) {
32171
32173
  return `${scheme}://${host}${hadMore ? "/<redacted>" : ""}`;
32172
32174
  });
32173
32175
  s = s.replace(/\b(bearer)\s+[A-Za-z0-9._~+/=-]{8,}/gi, "$1 <redacted>");
32176
+ s = s.replace(/\bak_[A-Za-z0-9_-]{8,}/g, "ak_<redacted>");
32174
32177
  s = s.replace(
32175
32178
  /\b(api[-_]?key|apikey|access[-_]?token|auth[-_]?token|secret|token|password|passwd|pwd)\b(\s*[:=]\s*)("?)[A-Za-z0-9._~+/=-]{6,}\3/gi,
32176
32179
  "$1$2<redacted>"
@@ -33026,18 +33029,66 @@ function promptPassphrase(prompt = " Keystore passphrase: ") {
33026
33029
  });
33027
33030
  }
33028
33031
 
33029
- // scripts/src/lib/api.mjs
33032
+ // scripts/src/lib/version.mjs
33030
33033
  import { createRequire } from "node:module";
33034
+ var PKG_VERSION = (() => {
33035
+ const requireFrom = createRequire(import.meta.url);
33036
+ for (const candidate of ["../../package.json", "../../../package.json"]) {
33037
+ try {
33038
+ const pkg = requireFrom(candidate);
33039
+ if (pkg?.name === "@rozoai/checkout" && pkg.version) return pkg.version;
33040
+ } catch {
33041
+ }
33042
+ }
33043
+ return "0.0.0";
33044
+ })();
33031
33045
 
33032
33046
  // scripts/src/lib/http.mjs
33033
33047
  var DEFAULT_TIMEOUT_MS = 2e4;
33034
- var USER_AGENT = "rozo-checkout-skill/1.0";
33035
- async function request(method, url, { body, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
33048
+ var USER_AGENT = `rozo-checkout-skill/${PKG_VERSION}`;
33049
+ var MAX_RETRIES = 3;
33050
+ var MAX_TOTAL_WAIT_MS = 12e4;
33051
+ var FALLBACK_BACKOFF_MS = [2e3, 4e3, 8e3];
33052
+ var defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
33053
+ function parseRetryAfter(value, now = Date.now()) {
33054
+ if (value === null || value === void 0) return null;
33055
+ const s = String(value).trim();
33056
+ if (!s) return null;
33057
+ if (/^\d+(\.\d+)?$/.test(s)) return Math.round(Number(s) * 1e3);
33058
+ const at = Date.parse(s);
33059
+ if (!Number.isFinite(at)) return null;
33060
+ return Math.max(0, at - now);
33061
+ }
33062
+ function bodyRetryAfterMs(json) {
33063
+ const raw = json?.retryAfterSeconds ?? json?.error?.retryAfterSeconds;
33064
+ if (raw === null || raw === void 0 || raw === "") return null;
33065
+ const n = Number(raw);
33066
+ return Number.isFinite(n) && n >= 0 ? Math.round(n * 1e3) : null;
33067
+ }
33068
+ function rateLimitInfo(res) {
33069
+ const pick = (name) => res.headers?.get?.(name) ?? null;
33070
+ const info = {
33071
+ limit: pick("x-ratelimit-limit"),
33072
+ remaining: pick("x-ratelimit-remaining"),
33073
+ tier: pick("x-ratelimit-tier"),
33074
+ scope: pick("x-ratelimit-scope")
33075
+ };
33076
+ const present = Object.fromEntries(Object.entries(info).filter(([, v]) => v !== null));
33077
+ return Object.keys(present).length ? present : null;
33078
+ }
33079
+ function parseBody(text) {
33080
+ if (!text) return null;
33081
+ try {
33082
+ return JSON.parse(text);
33083
+ } catch {
33084
+ return null;
33085
+ }
33086
+ }
33087
+ async function fetchOnce(method, url, body, timeoutMs) {
33036
33088
  const controller = new AbortController();
33037
33089
  const timer = setTimeout(() => controller.abort(), timeoutMs);
33038
- let res;
33039
33090
  try {
33040
- res = await fetch(url, {
33091
+ const res = await fetch(url, {
33041
33092
  method,
33042
33093
  headers: {
33043
33094
  accept: "application/json",
@@ -33047,6 +33098,8 @@ async function request(method, url, { body, timeoutMs = DEFAULT_TIMEOUT_MS } = {
33047
33098
  body: body !== void 0 ? JSON.stringify(body) : void 0,
33048
33099
  signal: controller.signal
33049
33100
  });
33101
+ const text = await res.text();
33102
+ return { res, text };
33050
33103
  } catch (err) {
33051
33104
  throw new SkillError(
33052
33105
  err?.name === "AbortError" ? "HTTP_TIMEOUT" : "HTTP_UNREACHABLE",
@@ -33056,31 +33109,54 @@ async function request(method, url, { body, timeoutMs = DEFAULT_TIMEOUT_MS } = {
33056
33109
  } finally {
33057
33110
  clearTimeout(timer);
33058
33111
  }
33059
- const text = await res.text();
33060
- let json = null;
33061
- if (text) {
33062
- try {
33063
- json = JSON.parse(text);
33064
- } catch {
33065
- json = null;
33112
+ }
33113
+ async function request(method, url, {
33114
+ body,
33115
+ timeoutMs = DEFAULT_TIMEOUT_MS,
33116
+ sleep = defaultSleep,
33117
+ now = Date.now,
33118
+ maxTotalWaitMs = MAX_TOTAL_WAIT_MS
33119
+ } = {}) {
33120
+ let waitedMs = 0;
33121
+ let retries = 0;
33122
+ for (; ; ) {
33123
+ const { res, text } = await fetchOnce(method, url, body, timeoutMs);
33124
+ const json = parseBody(text);
33125
+ if (res.ok) {
33126
+ if (json === null) {
33127
+ throw new SkillError("HTTP_BAD_JSON", "Endpoint returned a non-JSON body.", {
33128
+ url: redactUrl(url),
33129
+ snippet: redact(text).slice(0, 400)
33130
+ });
33131
+ }
33132
+ return json;
33133
+ }
33134
+ const headerWaitMs = parseRetryAfter(res.headers?.get?.("retry-after"), now());
33135
+ const hintMs = headerWaitMs ?? bodyRetryAfterMs(json);
33136
+ const retryable = res.status === 429 || res.status === 503 && hintMs !== null;
33137
+ if (retryable && retries < MAX_RETRIES) {
33138
+ const waitMs = hintMs ?? FALLBACK_BACKOFF_MS[retries];
33139
+ if (waitedMs + waitMs <= Math.min(maxTotalWaitMs, MAX_TOTAL_WAIT_MS)) {
33140
+ retries += 1;
33141
+ waitedMs += waitMs;
33142
+ await sleep(waitMs);
33143
+ continue;
33144
+ }
33066
33145
  }
33067
- }
33068
- if (!res.ok) {
33069
33146
  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}`;
33070
33147
  const message = json?.message || (typeof json?.error === "string" ? json.error : json?.error?.message) || `HTTP ${res.status}`;
33148
+ const rateLimit = rateLimitInfo(res);
33071
33149
  throw new SkillError(code, redact(String(message)), {
33072
33150
  httpStatus: res.status,
33073
33151
  url: redactUrl(url),
33074
- body: json ?? redact(text).slice(0, 800)
33075
- });
33076
- }
33077
- if (json === null) {
33078
- throw new SkillError("HTTP_BAD_JSON", "Endpoint returned a non-JSON body.", {
33079
- url: redactUrl(url),
33080
- snippet: redact(text).slice(0, 400)
33152
+ body: json ?? redact(text).slice(0, 800),
33153
+ ...retryable ? {
33154
+ retries,
33155
+ retryAfterSeconds: hintMs === null ? null : Math.ceil(hintMs / 1e3)
33156
+ } : {},
33157
+ ...rateLimit ? { rateLimit } : {}
33081
33158
  });
33082
33159
  }
33083
- return json;
33084
33160
  }
33085
33161
  function redactUrl(url) {
33086
33162
  try {
@@ -33288,17 +33364,6 @@ function assertPaymentLimit(invoiceUsd) {
33288
33364
  }
33289
33365
 
33290
33366
  // scripts/src/lib/api.mjs
33291
- var PKG_VERSION = (() => {
33292
- const requireFrom = createRequire(import.meta.url);
33293
- for (const candidate of ["../../package.json", "../../../package.json"]) {
33294
- try {
33295
- const pkg = requireFrom(candidate);
33296
- if (pkg?.name === "@rozoai/checkout" && pkg.version) return pkg.version;
33297
- } catch {
33298
- }
33299
- }
33300
- return "0.0.0";
33301
- })();
33302
33367
  var MPP_BASE = process.env.ROZO_CHECKOUT_MPP_BASE || "https://apiserver.mpprouter.dev/v1/services/rozo-agent-api";
33303
33368
  var CLIENT_LABEL = `rozo-checkout-cli/${PKG_VERSION}`;
33304
33369
  var ATTRIBUTION_CLIENT = `rozo-checkout-skill/${PKG_VERSION}`;
@@ -33536,6 +33601,8 @@ var MARGINS_MS = {
33536
33601
  };
33537
33602
  var BOLT11_MIN_VALIDITY_MS = 10 * MINUTE;
33538
33603
  var DEFAULT_MARGIN_MS = 10 * MINUTE;
33604
+ var DEFAULT_WATCH_MS = 10 * MINUTE;
33605
+ var LIGHTNING_WATCH_CAP_MS = 60 * MINUTE;
33539
33606
  function marginFor(chainId) {
33540
33607
  const key = String(chainId);
33541
33608
  return MARGINS_MS[key] ?? MARGINS_MS[chainId] ?? DEFAULT_MARGIN_MS;