@rozoai/checkout 0.1.17 → 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.
@@ -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]);
@@ -152,18 +154,66 @@ function extractLinkId(urlOrId) {
152
154
  );
153
155
  }
154
156
 
155
- // scripts/src/lib/api.mjs
157
+ // scripts/src/lib/version.mjs
156
158
  import { createRequire } from "node:module";
159
+ var PKG_VERSION = (() => {
160
+ const requireFrom = createRequire(import.meta.url);
161
+ for (const candidate of ["../../package.json", "../../../package.json"]) {
162
+ try {
163
+ const pkg = requireFrom(candidate);
164
+ if (pkg?.name === "@rozoai/checkout" && pkg.version) return pkg.version;
165
+ } catch {
166
+ }
167
+ }
168
+ return "0.0.0";
169
+ })();
157
170
 
158
171
  // scripts/src/lib/http.mjs
159
172
  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 } = {}) {
173
+ var USER_AGENT = `rozo-checkout-skill/${PKG_VERSION}`;
174
+ var MAX_RETRIES = 3;
175
+ var MAX_TOTAL_WAIT_MS = 12e4;
176
+ var FALLBACK_BACKOFF_MS = [2e3, 4e3, 8e3];
177
+ var defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
178
+ function parseRetryAfter(value, now = Date.now()) {
179
+ if (value === null || value === void 0) return null;
180
+ const s = String(value).trim();
181
+ if (!s) return null;
182
+ if (/^\d+(\.\d+)?$/.test(s)) return Math.round(Number(s) * 1e3);
183
+ const at = Date.parse(s);
184
+ if (!Number.isFinite(at)) return null;
185
+ return Math.max(0, at - now);
186
+ }
187
+ function bodyRetryAfterMs(json) {
188
+ const raw = json?.retryAfterSeconds ?? json?.error?.retryAfterSeconds;
189
+ if (raw === null || raw === void 0 || raw === "") return null;
190
+ const n = Number(raw);
191
+ return Number.isFinite(n) && n >= 0 ? Math.round(n * 1e3) : null;
192
+ }
193
+ function rateLimitInfo(res) {
194
+ const pick = (name) => res.headers?.get?.(name) ?? null;
195
+ const info = {
196
+ limit: pick("x-ratelimit-limit"),
197
+ remaining: pick("x-ratelimit-remaining"),
198
+ tier: pick("x-ratelimit-tier"),
199
+ scope: pick("x-ratelimit-scope")
200
+ };
201
+ const present = Object.fromEntries(Object.entries(info).filter(([, v]) => v !== null));
202
+ return Object.keys(present).length ? present : null;
203
+ }
204
+ function parseBody(text) {
205
+ if (!text) return null;
206
+ try {
207
+ return JSON.parse(text);
208
+ } catch {
209
+ return null;
210
+ }
211
+ }
212
+ async function fetchOnce(method, url, body, timeoutMs) {
162
213
  const controller = new AbortController();
163
214
  const timer = setTimeout(() => controller.abort(), timeoutMs);
164
- let res;
165
215
  try {
166
- res = await fetch(url, {
216
+ const res = await fetch(url, {
167
217
  method,
168
218
  headers: {
169
219
  accept: "application/json",
@@ -173,6 +223,8 @@ async function request(method, url, { body, timeoutMs = DEFAULT_TIMEOUT_MS } = {
173
223
  body: body !== void 0 ? JSON.stringify(body) : void 0,
174
224
  signal: controller.signal
175
225
  });
226
+ const text = await res.text();
227
+ return { res, text };
176
228
  } catch (err) {
177
229
  throw new SkillError(
178
230
  err?.name === "AbortError" ? "HTTP_TIMEOUT" : "HTTP_UNREACHABLE",
@@ -182,31 +234,54 @@ async function request(method, url, { body, timeoutMs = DEFAULT_TIMEOUT_MS } = {
182
234
  } finally {
183
235
  clearTimeout(timer);
184
236
  }
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;
237
+ }
238
+ async function request(method, url, {
239
+ body,
240
+ timeoutMs = DEFAULT_TIMEOUT_MS,
241
+ sleep = defaultSleep,
242
+ now = Date.now,
243
+ maxTotalWaitMs = MAX_TOTAL_WAIT_MS
244
+ } = {}) {
245
+ let waitedMs = 0;
246
+ let retries = 0;
247
+ for (; ; ) {
248
+ const { res, text } = await fetchOnce(method, url, body, timeoutMs);
249
+ const json = parseBody(text);
250
+ if (res.ok) {
251
+ if (json === null) {
252
+ throw new SkillError("HTTP_BAD_JSON", "Endpoint returned a non-JSON body.", {
253
+ url: redactUrl(url),
254
+ snippet: redact(text).slice(0, 400)
255
+ });
256
+ }
257
+ return json;
258
+ }
259
+ const headerWaitMs = parseRetryAfter(res.headers?.get?.("retry-after"), now());
260
+ const hintMs = headerWaitMs ?? bodyRetryAfterMs(json);
261
+ const retryable = res.status === 429 || res.status === 503 && hintMs !== null;
262
+ if (retryable && retries < MAX_RETRIES) {
263
+ const waitMs = hintMs ?? FALLBACK_BACKOFF_MS[retries];
264
+ if (waitedMs + waitMs <= Math.min(maxTotalWaitMs, MAX_TOTAL_WAIT_MS)) {
265
+ retries += 1;
266
+ waitedMs += waitMs;
267
+ await sleep(waitMs);
268
+ continue;
269
+ }
192
270
  }
193
- }
194
- if (!res.ok) {
195
271
  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
272
  const message = json?.message || (typeof json?.error === "string" ? json.error : json?.error?.message) || `HTTP ${res.status}`;
273
+ const rateLimit = rateLimitInfo(res);
197
274
  throw new SkillError(code, redact(String(message)), {
198
275
  httpStatus: res.status,
199
276
  url: redactUrl(url),
200
- body: json ?? redact(text).slice(0, 800)
277
+ body: json ?? redact(text).slice(0, 800),
278
+ ...retryable ? {
279
+ retries,
280
+ retryAfterSeconds: hintMs === null ? null : Math.ceil(hintMs / 1e3)
281
+ } : {},
282
+ ...rateLimit ? { rateLimit } : {}
201
283
  });
202
284
  }
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
285
  }
211
286
  function redactUrl(url) {
212
287
  try {
@@ -221,17 +296,6 @@ function postJson(url, body, opts) {
221
296
  }
222
297
 
223
298
  // 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
299
  var MPP_BASE = process.env.ROZO_CHECKOUT_MPP_BASE || "https://apiserver.mpprouter.dev/v1/services/rozo-agent-api";
236
300
  var CLIENT_LABEL = `rozo-checkout-cli/${PKG_VERSION}`;
237
301
  var ATTRIBUTION_CLIENT = `rozo-checkout-skill/${PKG_VERSION}`;
@@ -300,6 +364,8 @@ var MARGINS_MS = {
300
364
  };
301
365
  var BOLT11_MIN_VALIDITY_MS = 10 * MINUTE;
302
366
  var DEFAULT_MARGIN_MS = 10 * MINUTE;
367
+ var DEFAULT_WATCH_MS = 10 * MINUTE;
368
+ var LIGHTNING_WATCH_CAP_MS = 60 * MINUTE;
303
369
  function parseDeadline(value) {
304
370
  if (value === null || value === void 0 || value === "") return null;
305
371
  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]);
@@ -138,18 +140,66 @@ function maskAddress(address) {
138
140
  return `${s.slice(0, 6)}...${s.slice(-4)}`;
139
141
  }
140
142
 
141
- // scripts/src/lib/api.mjs
143
+ // scripts/src/lib/version.mjs
142
144
  import { createRequire } from "node:module";
145
+ var PKG_VERSION = (() => {
146
+ const requireFrom = createRequire(import.meta.url);
147
+ for (const candidate of ["../../package.json", "../../../package.json"]) {
148
+ try {
149
+ const pkg = requireFrom(candidate);
150
+ if (pkg?.name === "@rozoai/checkout" && pkg.version) return pkg.version;
151
+ } catch {
152
+ }
153
+ }
154
+ return "0.0.0";
155
+ })();
143
156
 
144
157
  // scripts/src/lib/http.mjs
145
158
  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 } = {}) {
159
+ var USER_AGENT = `rozo-checkout-skill/${PKG_VERSION}`;
160
+ var MAX_RETRIES = 3;
161
+ var MAX_TOTAL_WAIT_MS = 12e4;
162
+ var FALLBACK_BACKOFF_MS = [2e3, 4e3, 8e3];
163
+ var defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
164
+ function parseRetryAfter(value, now = Date.now()) {
165
+ if (value === null || value === void 0) return null;
166
+ const s = String(value).trim();
167
+ if (!s) return null;
168
+ if (/^\d+(\.\d+)?$/.test(s)) return Math.round(Number(s) * 1e3);
169
+ const at = Date.parse(s);
170
+ if (!Number.isFinite(at)) return null;
171
+ return Math.max(0, at - now);
172
+ }
173
+ function bodyRetryAfterMs(json) {
174
+ const raw = json?.retryAfterSeconds ?? json?.error?.retryAfterSeconds;
175
+ if (raw === null || raw === void 0 || raw === "") return null;
176
+ const n = Number(raw);
177
+ return Number.isFinite(n) && n >= 0 ? Math.round(n * 1e3) : null;
178
+ }
179
+ function rateLimitInfo(res) {
180
+ const pick = (name) => res.headers?.get?.(name) ?? null;
181
+ const info = {
182
+ limit: pick("x-ratelimit-limit"),
183
+ remaining: pick("x-ratelimit-remaining"),
184
+ tier: pick("x-ratelimit-tier"),
185
+ scope: pick("x-ratelimit-scope")
186
+ };
187
+ const present = Object.fromEntries(Object.entries(info).filter(([, v]) => v !== null));
188
+ return Object.keys(present).length ? present : null;
189
+ }
190
+ function parseBody(text) {
191
+ if (!text) return null;
192
+ try {
193
+ return JSON.parse(text);
194
+ } catch {
195
+ return null;
196
+ }
197
+ }
198
+ async function fetchOnce(method, url, body, timeoutMs) {
148
199
  const controller = new AbortController();
149
200
  const timer = setTimeout(() => controller.abort(), timeoutMs);
150
- let res;
151
201
  try {
152
- res = await fetch(url, {
202
+ const res = await fetch(url, {
153
203
  method,
154
204
  headers: {
155
205
  accept: "application/json",
@@ -159,6 +209,8 @@ async function request(method, url, { body, timeoutMs = DEFAULT_TIMEOUT_MS } = {
159
209
  body: body !== void 0 ? JSON.stringify(body) : void 0,
160
210
  signal: controller.signal
161
211
  });
212
+ const text = await res.text();
213
+ return { res, text };
162
214
  } catch (err) {
163
215
  throw new SkillError(
164
216
  err?.name === "AbortError" ? "HTTP_TIMEOUT" : "HTTP_UNREACHABLE",
@@ -168,31 +220,54 @@ async function request(method, url, { body, timeoutMs = DEFAULT_TIMEOUT_MS } = {
168
220
  } finally {
169
221
  clearTimeout(timer);
170
222
  }
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;
223
+ }
224
+ async function request(method, url, {
225
+ body,
226
+ timeoutMs = DEFAULT_TIMEOUT_MS,
227
+ sleep = defaultSleep,
228
+ now = Date.now,
229
+ maxTotalWaitMs = MAX_TOTAL_WAIT_MS
230
+ } = {}) {
231
+ let waitedMs = 0;
232
+ let retries = 0;
233
+ for (; ; ) {
234
+ const { res, text } = await fetchOnce(method, url, body, timeoutMs);
235
+ const json = parseBody(text);
236
+ if (res.ok) {
237
+ if (json === null) {
238
+ throw new SkillError("HTTP_BAD_JSON", "Endpoint returned a non-JSON body.", {
239
+ url: redactUrl(url),
240
+ snippet: redact(text).slice(0, 400)
241
+ });
242
+ }
243
+ return json;
244
+ }
245
+ const headerWaitMs = parseRetryAfter(res.headers?.get?.("retry-after"), now());
246
+ const hintMs = headerWaitMs ?? bodyRetryAfterMs(json);
247
+ const retryable = res.status === 429 || res.status === 503 && hintMs !== null;
248
+ if (retryable && retries < MAX_RETRIES) {
249
+ const waitMs = hintMs ?? FALLBACK_BACKOFF_MS[retries];
250
+ if (waitedMs + waitMs <= Math.min(maxTotalWaitMs, MAX_TOTAL_WAIT_MS)) {
251
+ retries += 1;
252
+ waitedMs += waitMs;
253
+ await sleep(waitMs);
254
+ continue;
255
+ }
178
256
  }
179
- }
180
- if (!res.ok) {
181
257
  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
258
  const message = json?.message || (typeof json?.error === "string" ? json.error : json?.error?.message) || `HTTP ${res.status}`;
259
+ const rateLimit = rateLimitInfo(res);
183
260
  throw new SkillError(code, redact(String(message)), {
184
261
  httpStatus: res.status,
185
262
  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)
263
+ body: json ?? redact(text).slice(0, 800),
264
+ ...retryable ? {
265
+ retries,
266
+ retryAfterSeconds: hintMs === null ? null : Math.ceil(hintMs / 1e3)
267
+ } : {},
268
+ ...rateLimit ? { rateLimit } : {}
193
269
  });
194
270
  }
195
- return json;
196
271
  }
197
272
  function redactUrl(url) {
198
273
  try {
@@ -258,17 +333,6 @@ function findByLinkId(linkId) {
258
333
  }
259
334
 
260
335
  // 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
336
  var MPP_BASE = process.env.ROZO_CHECKOUT_MPP_BASE || "https://apiserver.mpprouter.dev/v1/services/rozo-agent-api";
273
337
  var CLIENT_LABEL = `rozo-checkout-cli/${PKG_VERSION}`;
274
338
  var ATTRIBUTION_CLIENT = `rozo-checkout-skill/${PKG_VERSION}`;
@@ -619,6 +683,8 @@ var MARGINS_MS = {
619
683
  };
620
684
  var BOLT11_MIN_VALIDITY_MS = 10 * MINUTE;
621
685
  var DEFAULT_MARGIN_MS = 10 * MINUTE;
686
+ var DEFAULT_WATCH_MS = 10 * MINUTE;
687
+ var LIGHTNING_WATCH_CAP_MS = 60 * MINUTE;
622
688
  function formatRemaining(ms) {
623
689
  if (!Number.isFinite(ms)) return "unknown";
624
690
  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]);
@@ -160,18 +162,66 @@ function maskMemo(memo) {
160
162
  return `${s.slice(0, 4)}...${s.slice(-3)}`;
161
163
  }
162
164
 
163
- // scripts/src/lib/api.mjs
165
+ // scripts/src/lib/version.mjs
164
166
  import { createRequire } from "node:module";
167
+ var PKG_VERSION = (() => {
168
+ const requireFrom = createRequire(import.meta.url);
169
+ for (const candidate of ["../../package.json", "../../../package.json"]) {
170
+ try {
171
+ const pkg = requireFrom(candidate);
172
+ if (pkg?.name === "@rozoai/checkout" && pkg.version) return pkg.version;
173
+ } catch {
174
+ }
175
+ }
176
+ return "0.0.0";
177
+ })();
165
178
 
166
179
  // scripts/src/lib/http.mjs
167
180
  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 } = {}) {
181
+ var USER_AGENT = `rozo-checkout-skill/${PKG_VERSION}`;
182
+ var MAX_RETRIES = 3;
183
+ var MAX_TOTAL_WAIT_MS = 12e4;
184
+ var FALLBACK_BACKOFF_MS = [2e3, 4e3, 8e3];
185
+ var defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
186
+ function parseRetryAfter(value, now = Date.now()) {
187
+ if (value === null || value === void 0) return null;
188
+ const s = String(value).trim();
189
+ if (!s) return null;
190
+ if (/^\d+(\.\d+)?$/.test(s)) return Math.round(Number(s) * 1e3);
191
+ const at = Date.parse(s);
192
+ if (!Number.isFinite(at)) return null;
193
+ return Math.max(0, at - now);
194
+ }
195
+ function bodyRetryAfterMs(json) {
196
+ const raw = json?.retryAfterSeconds ?? json?.error?.retryAfterSeconds;
197
+ if (raw === null || raw === void 0 || raw === "") return null;
198
+ const n = Number(raw);
199
+ return Number.isFinite(n) && n >= 0 ? Math.round(n * 1e3) : null;
200
+ }
201
+ function rateLimitInfo(res) {
202
+ const pick = (name) => res.headers?.get?.(name) ?? null;
203
+ const info = {
204
+ limit: pick("x-ratelimit-limit"),
205
+ remaining: pick("x-ratelimit-remaining"),
206
+ tier: pick("x-ratelimit-tier"),
207
+ scope: pick("x-ratelimit-scope")
208
+ };
209
+ const present = Object.fromEntries(Object.entries(info).filter(([, v]) => v !== null));
210
+ return Object.keys(present).length ? present : null;
211
+ }
212
+ function parseBody(text) {
213
+ if (!text) return null;
214
+ try {
215
+ return JSON.parse(text);
216
+ } catch {
217
+ return null;
218
+ }
219
+ }
220
+ async function fetchOnce(method, url, body, timeoutMs) {
170
221
  const controller = new AbortController();
171
222
  const timer = setTimeout(() => controller.abort(), timeoutMs);
172
- let res;
173
223
  try {
174
- res = await fetch(url, {
224
+ const res = await fetch(url, {
175
225
  method,
176
226
  headers: {
177
227
  accept: "application/json",
@@ -181,6 +231,8 @@ async function request(method, url, { body, timeoutMs = DEFAULT_TIMEOUT_MS } = {
181
231
  body: body !== void 0 ? JSON.stringify(body) : void 0,
182
232
  signal: controller.signal
183
233
  });
234
+ const text = await res.text();
235
+ return { res, text };
184
236
  } catch (err) {
185
237
  throw new SkillError(
186
238
  err?.name === "AbortError" ? "HTTP_TIMEOUT" : "HTTP_UNREACHABLE",
@@ -190,31 +242,54 @@ async function request(method, url, { body, timeoutMs = DEFAULT_TIMEOUT_MS } = {
190
242
  } finally {
191
243
  clearTimeout(timer);
192
244
  }
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;
245
+ }
246
+ async function request(method, url, {
247
+ body,
248
+ timeoutMs = DEFAULT_TIMEOUT_MS,
249
+ sleep = defaultSleep,
250
+ now = Date.now,
251
+ maxTotalWaitMs = MAX_TOTAL_WAIT_MS
252
+ } = {}) {
253
+ let waitedMs = 0;
254
+ let retries = 0;
255
+ for (; ; ) {
256
+ const { res, text } = await fetchOnce(method, url, body, timeoutMs);
257
+ const json = parseBody(text);
258
+ if (res.ok) {
259
+ if (json === null) {
260
+ throw new SkillError("HTTP_BAD_JSON", "Endpoint returned a non-JSON body.", {
261
+ url: redactUrl(url),
262
+ snippet: redact(text).slice(0, 400)
263
+ });
264
+ }
265
+ return json;
266
+ }
267
+ const headerWaitMs = parseRetryAfter(res.headers?.get?.("retry-after"), now());
268
+ const hintMs = headerWaitMs ?? bodyRetryAfterMs(json);
269
+ const retryable = res.status === 429 || res.status === 503 && hintMs !== null;
270
+ if (retryable && retries < MAX_RETRIES) {
271
+ const waitMs = hintMs ?? FALLBACK_BACKOFF_MS[retries];
272
+ if (waitedMs + waitMs <= Math.min(maxTotalWaitMs, MAX_TOTAL_WAIT_MS)) {
273
+ retries += 1;
274
+ waitedMs += waitMs;
275
+ await sleep(waitMs);
276
+ continue;
277
+ }
200
278
  }
201
- }
202
- if (!res.ok) {
203
279
  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
280
  const message = json?.message || (typeof json?.error === "string" ? json.error : json?.error?.message) || `HTTP ${res.status}`;
281
+ const rateLimit = rateLimitInfo(res);
205
282
  throw new SkillError(code, redact(String(message)), {
206
283
  httpStatus: res.status,
207
284
  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)
285
+ body: json ?? redact(text).slice(0, 800),
286
+ ...retryable ? {
287
+ retries,
288
+ retryAfterSeconds: hintMs === null ? null : Math.ceil(hintMs / 1e3)
289
+ } : {},
290
+ ...rateLimit ? { rateLimit } : {}
215
291
  });
216
292
  }
217
- return json;
218
293
  }
219
294
  function redactUrl(url) {
220
295
  try {
@@ -280,17 +355,6 @@ function findByLinkId(linkId) {
280
355
  }
281
356
 
282
357
  // 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
358
  var MPP_BASE = process.env.ROZO_CHECKOUT_MPP_BASE || "https://apiserver.mpprouter.dev/v1/services/rozo-agent-api";
295
359
  var CLIENT_LABEL = `rozo-checkout-cli/${PKG_VERSION}`;
296
360
  var ATTRIBUTION_CLIENT = `rozo-checkout-skill/${PKG_VERSION}`;
@@ -895,6 +959,8 @@ var MARGINS_MS = {
895
959
  };
896
960
  var BOLT11_MIN_VALIDITY_MS = 10 * MINUTE;
897
961
  var DEFAULT_MARGIN_MS = 10 * MINUTE;
962
+ var DEFAULT_WATCH_MS = 10 * MINUTE;
963
+ var LIGHTNING_WATCH_CAP_MS = 60 * MINUTE;
898
964
  function formatRemaining(ms) {
899
965
  if (!Number.isFinite(ms)) return "unknown";
900
966
  if (ms <= 0) return "expired";