@rozoai/checkout 0.1.16 → 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)
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 } : {}
187
269
  });
188
270
  }
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)
193
- });
194
- }
195
- return json;
196
271
  }
197
272
  function redactUrl(url) {
198
273
  try {
@@ -206,18 +281,58 @@ function getJson(url, opts) {
206
281
  return request("GET", url, opts);
207
282
  }
208
283
 
209
- // scripts/src/lib/api.mjs
210
- var PKG_VERSION = (() => {
211
- const requireFrom = createRequire(import.meta.url);
212
- for (const candidate of ["../../package.json", "../../../package.json"]) {
284
+ // scripts/src/lib/state.mjs
285
+ import fs from "node:fs";
286
+ import path from "node:path";
287
+ import os from "node:os";
288
+ function stateRoot() {
289
+ return process.env.ROZO_CHECKOUT_STATE_DIR || path.join(os.homedir(), ".rozo-checkout", "state");
290
+ }
291
+ function statePath(rozoPaymentId) {
292
+ if (!/^[A-Za-z0-9-]{8,64}$/.test(String(rozoPaymentId || ""))) {
293
+ throw new SkillError("BAD_ROZO_PAYMENT_ID", "Refusing to build a state path from that id.");
294
+ }
295
+ return path.join(stateRoot(), `${rozoPaymentId}.json`);
296
+ }
297
+ function readState(rozoPaymentId) {
298
+ const file = statePath(rozoPaymentId);
299
+ let raw;
300
+ try {
301
+ raw = fs.readFileSync(file, "utf8");
302
+ } catch (err) {
303
+ if (err.code === "ENOENT") return null;
304
+ throw new SkillError("STATE_UNREADABLE", `Cannot read local state: ${err.code}`);
305
+ }
306
+ try {
307
+ return JSON.parse(raw);
308
+ } catch {
309
+ throw new SkillError(
310
+ "STATE_CORRUPT",
311
+ "The local state file for this order is corrupt. Refusing to act; inspect it manually."
312
+ );
313
+ }
314
+ }
315
+ function findByLinkId(linkId) {
316
+ const dir = stateRoot();
317
+ let files = [];
318
+ try {
319
+ files = fs.readdirSync(dir).filter((f) => f.endsWith(".json"));
320
+ } catch {
321
+ return null;
322
+ }
323
+ let best = null;
324
+ for (const f of files) {
213
325
  try {
214
- const pkg = requireFrom(candidate);
215
- if (pkg?.name === "@rozoai/checkout" && pkg.version) return pkg.version;
326
+ const s = JSON.parse(fs.readFileSync(path.join(dir, f), "utf8"));
327
+ if (s?.linkId !== linkId) continue;
328
+ if (!best || String(s.createdAt) > String(best.createdAt)) best = s;
216
329
  } catch {
217
330
  }
218
331
  }
219
- return "0.0.0";
220
- })();
332
+ return best;
333
+ }
334
+
335
+ // scripts/src/lib/api.mjs
221
336
  var MPP_BASE = process.env.ROZO_CHECKOUT_MPP_BASE || "https://apiserver.mpprouter.dev/v1/services/rozo-agent-api";
222
337
  var CLIENT_LABEL = `rozo-checkout-cli/${PKG_VERSION}`;
223
338
  var ATTRIBUTION_CLIENT = `rozo-checkout-skill/${PKG_VERSION}`;
@@ -568,6 +683,8 @@ var MARGINS_MS = {
568
683
  };
569
684
  var BOLT11_MIN_VALIDITY_MS = 10 * MINUTE;
570
685
  var DEFAULT_MARGIN_MS = 10 * MINUTE;
686
+ var DEFAULT_WATCH_MS = 10 * MINUTE;
687
+ var LIGHTNING_WATCH_CAP_MS = 60 * MINUTE;
571
688
  function formatRemaining(ms) {
572
689
  if (!Number.isFinite(ms)) return "unknown";
573
690
  if (ms <= 0) return "expired";
@@ -677,57 +794,6 @@ function checkExpiry({
677
794
  return { ...withDeadline, ok: true, code: null, reason: null };
678
795
  }
679
796
 
680
- // scripts/src/lib/state.mjs
681
- import fs from "node:fs";
682
- import path from "node:path";
683
- import os from "node:os";
684
- function stateRoot() {
685
- return process.env.ROZO_CHECKOUT_STATE_DIR || path.join(os.homedir(), ".rozo-checkout", "state");
686
- }
687
- function statePath(rozoPaymentId) {
688
- if (!/^[A-Za-z0-9-]{8,64}$/.test(String(rozoPaymentId || ""))) {
689
- throw new SkillError("BAD_ROZO_PAYMENT_ID", "Refusing to build a state path from that id.");
690
- }
691
- return path.join(stateRoot(), `${rozoPaymentId}.json`);
692
- }
693
- function readState(rozoPaymentId) {
694
- const file = statePath(rozoPaymentId);
695
- let raw;
696
- try {
697
- raw = fs.readFileSync(file, "utf8");
698
- } catch (err) {
699
- if (err.code === "ENOENT") return null;
700
- throw new SkillError("STATE_UNREADABLE", `Cannot read local state: ${err.code}`);
701
- }
702
- try {
703
- return JSON.parse(raw);
704
- } catch {
705
- throw new SkillError(
706
- "STATE_CORRUPT",
707
- "The local state file for this order is corrupt. Refusing to act; inspect it manually."
708
- );
709
- }
710
- }
711
- function findByLinkId(linkId) {
712
- const dir = stateRoot();
713
- let files = [];
714
- try {
715
- files = fs.readdirSync(dir).filter((f) => f.endsWith(".json"));
716
- } catch {
717
- return null;
718
- }
719
- let best = null;
720
- for (const f of files) {
721
- try {
722
- const s = JSON.parse(fs.readFileSync(path.join(dir, f), "utf8"));
723
- if (s?.linkId !== linkId) continue;
724
- if (!best || String(s.createdAt) > String(best.createdAt)) best = s;
725
- } catch {
726
- }
727
- }
728
- return best;
729
- }
730
-
731
797
  // scripts/src/lib/bitrefill.mjs
732
798
  var PROVIDER_BITREFILL = "bitrefill";
733
799
  var BITREFILL_DESTINATION = Object.freeze({ chainId: "8453", tokenSymbol: "USDC" });
@@ -1017,7 +1083,7 @@ function sendWindowFor({ provider, chainId, payment, status, bitrefillExpiry, no
1017
1083
  intentExpiresAt: payment?.expiresAt ?? deadline,
1018
1084
  coinbaseExpiry: deadline
1019
1085
  });
1020
- return expiry2.ok ? { ok: true, code: null, minutesOfSlack: Math.floor(expiry2.msOfSlack / 6e4) } : { ok: false, code: expiry2.code, reason: expiry2.reason };
1086
+ return expiry2.ok ? { ok: true, code: null, minutesOfSlack: Math.floor(expiry2.msOfSlack / 6e4), deadlineMs: expiry2.effectiveDeadlineMs } : { ok: false, code: expiry2.code, reason: expiry2.reason };
1021
1087
  }
1022
1088
  if (!status) {
1023
1089
  return { ok: false, code: "LINK_PAYABILITY_UNKNOWN", reason: "The Coinbase link state could not be read." };
@@ -1030,7 +1096,7 @@ function sendWindowFor({ provider, chainId, payment, status, bitrefillExpiry, no
1030
1096
  intentExpiresAt: payment?.expiresAt ?? status?.rozoPayment?.expiresAt,
1031
1097
  coinbaseExpiry: status?.coinbase?.preApprovalExpiry
1032
1098
  });
1033
- return expiry.ok ? { ok: true, code: null, minutesOfSlack: Math.floor(expiry.msOfSlack / 6e4) } : { ok: false, code: expiry.code, reason: expiry.reason };
1099
+ return expiry.ok ? { ok: true, code: null, minutesOfSlack: Math.floor(expiry.msOfSlack / 6e4), deadlineMs: expiry.effectiveDeadlineMs } : { ok: false, code: expiry.code, reason: expiry.reason };
1034
1100
  }
1035
1101
  async function snapshot({ rozoPaymentId, linkId, provider: explicitProvider }) {
1036
1102
  let provider = resolveProvider(explicitProvider, rozoPaymentId);