@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.
@@ -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]);
@@ -21849,18 +21851,66 @@ function promptPassphrase(prompt = " Keystore passphrase: ") {
21849
21851
  });
21850
21852
  }
21851
21853
 
21852
- // scripts/src/lib/api.mjs
21854
+ // scripts/src/lib/version.mjs
21853
21855
  import { createRequire } from "node:module";
21856
+ var PKG_VERSION = (() => {
21857
+ const requireFrom = createRequire(import.meta.url);
21858
+ for (const candidate of ["../../package.json", "../../../package.json"]) {
21859
+ try {
21860
+ const pkg = requireFrom(candidate);
21861
+ if (pkg?.name === "@rozoai/checkout" && pkg.version) return pkg.version;
21862
+ } catch {
21863
+ }
21864
+ }
21865
+ return "0.0.0";
21866
+ })();
21854
21867
 
21855
21868
  // scripts/src/lib/http.mjs
21856
21869
  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 } = {}) {
21870
+ var USER_AGENT = `rozo-checkout-skill/${PKG_VERSION}`;
21871
+ var MAX_RETRIES = 3;
21872
+ var MAX_TOTAL_WAIT_MS = 12e4;
21873
+ var FALLBACK_BACKOFF_MS = [2e3, 4e3, 8e3];
21874
+ var defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
21875
+ function parseRetryAfter(value, now = Date.now()) {
21876
+ if (value === null || value === void 0) return null;
21877
+ const s = String(value).trim();
21878
+ if (!s) return null;
21879
+ if (/^\d+(\.\d+)?$/.test(s)) return Math.round(Number(s) * 1e3);
21880
+ const at = Date.parse(s);
21881
+ if (!Number.isFinite(at)) return null;
21882
+ return Math.max(0, at - now);
21883
+ }
21884
+ function bodyRetryAfterMs(json) {
21885
+ const raw = json?.retryAfterSeconds ?? json?.error?.retryAfterSeconds;
21886
+ if (raw === null || raw === void 0 || raw === "") return null;
21887
+ const n = Number(raw);
21888
+ return Number.isFinite(n) && n >= 0 ? Math.round(n * 1e3) : null;
21889
+ }
21890
+ function rateLimitInfo(res) {
21891
+ const pick = (name) => res.headers?.get?.(name) ?? null;
21892
+ const info = {
21893
+ limit: pick("x-ratelimit-limit"),
21894
+ remaining: pick("x-ratelimit-remaining"),
21895
+ tier: pick("x-ratelimit-tier"),
21896
+ scope: pick("x-ratelimit-scope")
21897
+ };
21898
+ const present = Object.fromEntries(Object.entries(info).filter(([, v]) => v !== null));
21899
+ return Object.keys(present).length ? present : null;
21900
+ }
21901
+ function parseBody(text) {
21902
+ if (!text) return null;
21903
+ try {
21904
+ return JSON.parse(text);
21905
+ } catch {
21906
+ return null;
21907
+ }
21908
+ }
21909
+ async function fetchOnce(method, url, body, timeoutMs) {
21859
21910
  const controller = new AbortController();
21860
21911
  const timer = setTimeout(() => controller.abort(), timeoutMs);
21861
- let res;
21862
21912
  try {
21863
- res = await fetch(url, {
21913
+ const res = await fetch(url, {
21864
21914
  method,
21865
21915
  headers: {
21866
21916
  accept: "application/json",
@@ -21870,6 +21920,8 @@ async function request(method, url, { body, timeoutMs = DEFAULT_TIMEOUT_MS } = {
21870
21920
  body: body !== void 0 ? JSON.stringify(body) : void 0,
21871
21921
  signal: controller.signal
21872
21922
  });
21923
+ const text = await res.text();
21924
+ return { res, text };
21873
21925
  } catch (err) {
21874
21926
  throw new SkillError(
21875
21927
  err?.name === "AbortError" ? "HTTP_TIMEOUT" : "HTTP_UNREACHABLE",
@@ -21879,31 +21931,54 @@ async function request(method, url, { body, timeoutMs = DEFAULT_TIMEOUT_MS } = {
21879
21931
  } finally {
21880
21932
  clearTimeout(timer);
21881
21933
  }
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;
21934
+ }
21935
+ async function request(method, url, {
21936
+ body,
21937
+ timeoutMs = DEFAULT_TIMEOUT_MS,
21938
+ sleep = defaultSleep,
21939
+ now = Date.now,
21940
+ maxTotalWaitMs = MAX_TOTAL_WAIT_MS
21941
+ } = {}) {
21942
+ let waitedMs = 0;
21943
+ let retries = 0;
21944
+ for (; ; ) {
21945
+ const { res, text } = await fetchOnce(method, url, body, timeoutMs);
21946
+ const json = parseBody(text);
21947
+ if (res.ok) {
21948
+ if (json === null) {
21949
+ throw new SkillError("HTTP_BAD_JSON", "Endpoint returned a non-JSON body.", {
21950
+ url: redactUrl(url),
21951
+ snippet: redact(text).slice(0, 400)
21952
+ });
21953
+ }
21954
+ return json;
21955
+ }
21956
+ const headerWaitMs = parseRetryAfter(res.headers?.get?.("retry-after"), now());
21957
+ const hintMs = headerWaitMs ?? bodyRetryAfterMs(json);
21958
+ const retryable = res.status === 429 || res.status === 503 && hintMs !== null;
21959
+ if (retryable && retries < MAX_RETRIES) {
21960
+ const waitMs = hintMs ?? FALLBACK_BACKOFF_MS[retries];
21961
+ if (waitedMs + waitMs <= Math.min(maxTotalWaitMs, MAX_TOTAL_WAIT_MS)) {
21962
+ retries += 1;
21963
+ waitedMs += waitMs;
21964
+ await sleep(waitMs);
21965
+ continue;
21966
+ }
21889
21967
  }
21890
- }
21891
- if (!res.ok) {
21892
21968
  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
21969
  const message = json?.message || (typeof json?.error === "string" ? json.error : json?.error?.message) || `HTTP ${res.status}`;
21970
+ const rateLimit = rateLimitInfo(res);
21894
21971
  throw new SkillError(code, redact(String(message)), {
21895
21972
  httpStatus: res.status,
21896
21973
  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)
21974
+ body: json ?? redact(text).slice(0, 800),
21975
+ ...retryable ? {
21976
+ retries,
21977
+ retryAfterSeconds: hintMs === null ? null : Math.ceil(hintMs / 1e3)
21978
+ } : {},
21979
+ ...rateLimit ? { rateLimit } : {}
21904
21980
  });
21905
21981
  }
21906
- return json;
21907
21982
  }
21908
21983
  function redactUrl(url) {
21909
21984
  try {
@@ -22111,17 +22186,6 @@ function assertPaymentLimit(invoiceUsd) {
22111
22186
  }
22112
22187
 
22113
22188
  // 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
22189
  var MPP_BASE = process.env.ROZO_CHECKOUT_MPP_BASE || "https://apiserver.mpprouter.dev/v1/services/rozo-agent-api";
22126
22190
  var CLIENT_LABEL = `rozo-checkout-cli/${PKG_VERSION}`;
22127
22191
  var ATTRIBUTION_CLIENT = `rozo-checkout-skill/${PKG_VERSION}`;
@@ -22359,6 +22423,8 @@ var MARGINS_MS = {
22359
22423
  };
22360
22424
  var BOLT11_MIN_VALIDITY_MS = 10 * MINUTE;
22361
22425
  var DEFAULT_MARGIN_MS = 10 * MINUTE;
22426
+ var DEFAULT_WATCH_MS = 10 * MINUTE;
22427
+ var LIGHTNING_WATCH_CAP_MS = 60 * MINUTE;
22362
22428
  function marginFor(chainId) {
22363
22429
  const key = String(chainId);
22364
22430
  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]);
@@ -33026,18 +33028,66 @@ function promptPassphrase(prompt = " Keystore passphrase: ") {
33026
33028
  });
33027
33029
  }
33028
33030
 
33029
- // scripts/src/lib/api.mjs
33031
+ // scripts/src/lib/version.mjs
33030
33032
  import { createRequire } from "node:module";
33033
+ var PKG_VERSION = (() => {
33034
+ const requireFrom = createRequire(import.meta.url);
33035
+ for (const candidate of ["../../package.json", "../../../package.json"]) {
33036
+ try {
33037
+ const pkg = requireFrom(candidate);
33038
+ if (pkg?.name === "@rozoai/checkout" && pkg.version) return pkg.version;
33039
+ } catch {
33040
+ }
33041
+ }
33042
+ return "0.0.0";
33043
+ })();
33031
33044
 
33032
33045
  // scripts/src/lib/http.mjs
33033
33046
  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 } = {}) {
33047
+ var USER_AGENT = `rozo-checkout-skill/${PKG_VERSION}`;
33048
+ var MAX_RETRIES = 3;
33049
+ var MAX_TOTAL_WAIT_MS = 12e4;
33050
+ var FALLBACK_BACKOFF_MS = [2e3, 4e3, 8e3];
33051
+ var defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
33052
+ function parseRetryAfter(value, now = Date.now()) {
33053
+ if (value === null || value === void 0) return null;
33054
+ const s = String(value).trim();
33055
+ if (!s) return null;
33056
+ if (/^\d+(\.\d+)?$/.test(s)) return Math.round(Number(s) * 1e3);
33057
+ const at = Date.parse(s);
33058
+ if (!Number.isFinite(at)) return null;
33059
+ return Math.max(0, at - now);
33060
+ }
33061
+ function bodyRetryAfterMs(json) {
33062
+ const raw = json?.retryAfterSeconds ?? json?.error?.retryAfterSeconds;
33063
+ if (raw === null || raw === void 0 || raw === "") return null;
33064
+ const n = Number(raw);
33065
+ return Number.isFinite(n) && n >= 0 ? Math.round(n * 1e3) : null;
33066
+ }
33067
+ function rateLimitInfo(res) {
33068
+ const pick = (name) => res.headers?.get?.(name) ?? null;
33069
+ const info = {
33070
+ limit: pick("x-ratelimit-limit"),
33071
+ remaining: pick("x-ratelimit-remaining"),
33072
+ tier: pick("x-ratelimit-tier"),
33073
+ scope: pick("x-ratelimit-scope")
33074
+ };
33075
+ const present = Object.fromEntries(Object.entries(info).filter(([, v]) => v !== null));
33076
+ return Object.keys(present).length ? present : null;
33077
+ }
33078
+ function parseBody(text) {
33079
+ if (!text) return null;
33080
+ try {
33081
+ return JSON.parse(text);
33082
+ } catch {
33083
+ return null;
33084
+ }
33085
+ }
33086
+ async function fetchOnce(method, url, body, timeoutMs) {
33036
33087
  const controller = new AbortController();
33037
33088
  const timer = setTimeout(() => controller.abort(), timeoutMs);
33038
- let res;
33039
33089
  try {
33040
- res = await fetch(url, {
33090
+ const res = await fetch(url, {
33041
33091
  method,
33042
33092
  headers: {
33043
33093
  accept: "application/json",
@@ -33047,6 +33097,8 @@ async function request(method, url, { body, timeoutMs = DEFAULT_TIMEOUT_MS } = {
33047
33097
  body: body !== void 0 ? JSON.stringify(body) : void 0,
33048
33098
  signal: controller.signal
33049
33099
  });
33100
+ const text = await res.text();
33101
+ return { res, text };
33050
33102
  } catch (err) {
33051
33103
  throw new SkillError(
33052
33104
  err?.name === "AbortError" ? "HTTP_TIMEOUT" : "HTTP_UNREACHABLE",
@@ -33056,31 +33108,54 @@ async function request(method, url, { body, timeoutMs = DEFAULT_TIMEOUT_MS } = {
33056
33108
  } finally {
33057
33109
  clearTimeout(timer);
33058
33110
  }
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;
33111
+ }
33112
+ async function request(method, url, {
33113
+ body,
33114
+ timeoutMs = DEFAULT_TIMEOUT_MS,
33115
+ sleep = defaultSleep,
33116
+ now = Date.now,
33117
+ maxTotalWaitMs = MAX_TOTAL_WAIT_MS
33118
+ } = {}) {
33119
+ let waitedMs = 0;
33120
+ let retries = 0;
33121
+ for (; ; ) {
33122
+ const { res, text } = await fetchOnce(method, url, body, timeoutMs);
33123
+ const json = parseBody(text);
33124
+ if (res.ok) {
33125
+ if (json === null) {
33126
+ throw new SkillError("HTTP_BAD_JSON", "Endpoint returned a non-JSON body.", {
33127
+ url: redactUrl(url),
33128
+ snippet: redact(text).slice(0, 400)
33129
+ });
33130
+ }
33131
+ return json;
33132
+ }
33133
+ const headerWaitMs = parseRetryAfter(res.headers?.get?.("retry-after"), now());
33134
+ const hintMs = headerWaitMs ?? bodyRetryAfterMs(json);
33135
+ const retryable = res.status === 429 || res.status === 503 && hintMs !== null;
33136
+ if (retryable && retries < MAX_RETRIES) {
33137
+ const waitMs = hintMs ?? FALLBACK_BACKOFF_MS[retries];
33138
+ if (waitedMs + waitMs <= Math.min(maxTotalWaitMs, MAX_TOTAL_WAIT_MS)) {
33139
+ retries += 1;
33140
+ waitedMs += waitMs;
33141
+ await sleep(waitMs);
33142
+ continue;
33143
+ }
33066
33144
  }
33067
- }
33068
- if (!res.ok) {
33069
33145
  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
33146
  const message = json?.message || (typeof json?.error === "string" ? json.error : json?.error?.message) || `HTTP ${res.status}`;
33147
+ const rateLimit = rateLimitInfo(res);
33071
33148
  throw new SkillError(code, redact(String(message)), {
33072
33149
  httpStatus: res.status,
33073
33150
  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)
33151
+ body: json ?? redact(text).slice(0, 800),
33152
+ ...retryable ? {
33153
+ retries,
33154
+ retryAfterSeconds: hintMs === null ? null : Math.ceil(hintMs / 1e3)
33155
+ } : {},
33156
+ ...rateLimit ? { rateLimit } : {}
33081
33157
  });
33082
33158
  }
33083
- return json;
33084
33159
  }
33085
33160
  function redactUrl(url) {
33086
33161
  try {
@@ -33288,17 +33363,6 @@ function assertPaymentLimit(invoiceUsd) {
33288
33363
  }
33289
33364
 
33290
33365
  // 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
33366
  var MPP_BASE = process.env.ROZO_CHECKOUT_MPP_BASE || "https://apiserver.mpprouter.dev/v1/services/rozo-agent-api";
33303
33367
  var CLIENT_LABEL = `rozo-checkout-cli/${PKG_VERSION}`;
33304
33368
  var ATTRIBUTION_CLIENT = `rozo-checkout-skill/${PKG_VERSION}`;
@@ -33536,6 +33600,8 @@ var MARGINS_MS = {
33536
33600
  };
33537
33601
  var BOLT11_MIN_VALIDITY_MS = 10 * MINUTE;
33538
33602
  var DEFAULT_MARGIN_MS = 10 * MINUTE;
33603
+ var DEFAULT_WATCH_MS = 10 * MINUTE;
33604
+ var LIGHTNING_WATCH_CAP_MS = 60 * MINUTE;
33539
33605
  function marginFor(chainId) {
33540
33606
  const key = String(chainId);
33541
33607
  return MARGINS_MS[key] ?? MARGINS_MS[chainId] ?? DEFAULT_MARGIN_MS;
@@ -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/api.mjs
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 = "rozo-checkout-skill/1.0";
148
- 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) {
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
- const text = await res.text();
173
- let json = null;
174
- if (text) {
175
- try {
176
- json = JSON.parse(text);
177
- } catch {
178
- json = null;
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 {
@@ -259,17 +334,6 @@ function findByLinkId(linkId) {
259
334
  }
260
335
 
261
336
  // 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
337
  var MPP_BASE = process.env.ROZO_CHECKOUT_MPP_BASE || "https://apiserver.mpprouter.dev/v1/services/rozo-agent-api";
274
338
  var CLIENT_LABEL = `rozo-checkout-cli/${PKG_VERSION}`;
275
339
  var ATTRIBUTION_CLIENT = `rozo-checkout-skill/${PKG_VERSION}`;
@@ -620,6 +684,15 @@ var MARGINS_MS = {
620
684
  };
621
685
  var BOLT11_MIN_VALIDITY_MS = 10 * MINUTE;
622
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
+ }
623
696
  function formatRemaining(ms) {
624
697
  if (!Number.isFinite(ms)) return "unknown";
625
698
  if (ms <= 0) return "expired";
@@ -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 timeoutMs = Math.max(0, Number(args.timeout ?? 600) * 1e3);
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);