@rozoai/checkout 0.1.14 → 0.1.16

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.
@@ -0,0 +1,1195 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire as __rozoCreateRequire } from 'node:module';
3
+ import { fileURLToPath as __rozoFileURLToPath } from 'node:url';
4
+ import { dirname as __rozoDirname } from 'node:path';
5
+ const require = __rozoCreateRequire(import.meta.url);
6
+ const __filename = __rozoFileURLToPath(import.meta.url);
7
+ const __dirname = __rozoDirname(__filename);
8
+
9
+ // scripts/src/lib/support.mjs
10
+ var SUPPORT = Object.freeze({
11
+ email: "hi@rozo.ai",
12
+ x: "https://x.com/ROZOai",
13
+ discord: "https://discord.gg/EfWejgTbuU"
14
+ });
15
+ var SUPPORT_TEXT = `Need help? Email ${SUPPORT.email}, or reach ROZO on X ${SUPPORT.x} or Discord ${SUPPORT.discord}.`;
16
+
17
+ // scripts/src/lib/output.mjs
18
+ var PUBLIC_SUPPORT_URLS = /* @__PURE__ */ new Set([SUPPORT.x, SUPPORT.discord]);
19
+ var EXIT_OK = 0;
20
+ var EXIT_ERROR = 1;
21
+ var EXIT_USAGE = 2;
22
+ function redact(text) {
23
+ if (text === null || text === void 0) return text;
24
+ let s = typeof text === "string" ? text : String(text);
25
+ s = s.replace(/0x[0-9a-fA-F]{64}/g, "0x<redacted>");
26
+ s = s.replace(/\b[0-9a-fA-F]{64}\b/g, "<redacted>");
27
+ s = s.replace(/\b[1-9A-HJ-NP-Za-km-z]{80,90}\b/g, "<redacted>");
28
+ s = s.replace(/\[(?:\s*\d{1,3}\s*,){40,}\s*\d{1,3}\s*\]/g, "[<redacted>]");
29
+ s = s.replace(/\b([a-zA-Z][a-zA-Z0-9+.-]*):\/\/([^\s"'<>]+)/g, (m, scheme, rest) => {
30
+ if (PUBLIC_SUPPORT_URLS.has(m.replace(/[.,;:!?)]+$/, ""))) return m;
31
+ const withoutUserinfo = rest.includes("@") ? rest.slice(rest.indexOf("@") + 1) : rest;
32
+ const host = withoutUserinfo.split(/[/?#]/)[0];
33
+ const hadMore = withoutUserinfo.length > host.length;
34
+ return `${scheme}://${host}${hadMore ? "/<redacted>" : ""}`;
35
+ });
36
+ s = s.replace(/\b(bearer)\s+[A-Za-z0-9._~+/=-]{8,}/gi, "$1 <redacted>");
37
+ s = s.replace(
38
+ /\b(api[-_]?key|apikey|access[-_]?token|auth[-_]?token|secret|token|password|passwd|pwd)\b(\s*[:=]\s*)("?)[A-Za-z0-9._~+/=-]{6,}\3/gi,
39
+ "$1$2<redacted>"
40
+ );
41
+ return s;
42
+ }
43
+ var PublicHash = class {
44
+ constructor(value) {
45
+ this.value = value;
46
+ }
47
+ toJSON() {
48
+ return this.value;
49
+ }
50
+ };
51
+ var TX_HASH_VALUE = /^(0x[0-9a-fA-F]{64}|[1-9A-HJ-NP-Za-km-z]{86,88})$/;
52
+ function publicHash(value) {
53
+ return typeof value === "string" && TX_HASH_VALUE.test(value) ? new PublicHash(value) : value;
54
+ }
55
+ function redactDeep(value) {
56
+ if (value instanceof PublicHash) return value.value;
57
+ if (typeof value === "string") return redact(value);
58
+ if (Array.isArray(value)) return value.map(redactDeep);
59
+ if (value && typeof value === "object") {
60
+ const out = {};
61
+ for (const [k, v] of Object.entries(value)) {
62
+ if (/priv(ate)?[-_]?key|secret|mnemonic|seed/i.test(k)) {
63
+ out[k] = "<redacted>";
64
+ continue;
65
+ }
66
+ out[k] = redactDeep(v);
67
+ }
68
+ return out;
69
+ }
70
+ return value;
71
+ }
72
+ var capturing = false;
73
+ var EmitSignal = class extends Error {
74
+ constructor(payload, exitCode) {
75
+ super("emit");
76
+ this.payload = payload;
77
+ this.exitCode = exitCode;
78
+ }
79
+ };
80
+ function formatFailure(err, fallbackCode = "RUNTIME_ERROR") {
81
+ const code = err && err.code || fallbackCode;
82
+ const message = redact(err && err.message || String(err));
83
+ const payload = { success: false, error: { code, message } };
84
+ if (err && err.details) payload.error.details = redactDeep(err.details);
85
+ return payload;
86
+ }
87
+ function emit(payload, exitCode = EXIT_OK) {
88
+ const redacted = redactDeep(payload);
89
+ if (capturing) throw new EmitSignal(redacted, exitCode);
90
+ process.stdout.write(JSON.stringify(redacted, null, 2) + "\n");
91
+ process.exit(exitCode);
92
+ }
93
+ function fail(err, fallbackCode = "RUNTIME_ERROR", exitCode = EXIT_ERROR) {
94
+ const payload = formatFailure(err, fallbackCode);
95
+ if (capturing) throw new EmitSignal(payload, exitCode);
96
+ process.stdout.write(JSON.stringify(payload, null, 2) + "\n");
97
+ process.exit(exitCode);
98
+ }
99
+ function usage(message) {
100
+ fail({ code: "USAGE", message }, "USAGE", EXIT_USAGE);
101
+ }
102
+ function parseArgs(argv) {
103
+ const out = { _: [] };
104
+ for (let i = 0; i < argv.length; i++) {
105
+ const a = argv[i];
106
+ if (a.startsWith("--")) {
107
+ const key = a.slice(2);
108
+ const next = argv[i + 1];
109
+ if (next === void 0 || next.startsWith("--")) {
110
+ out[key] = true;
111
+ } else {
112
+ out[key] = next;
113
+ i++;
114
+ }
115
+ } else {
116
+ out._.push(a);
117
+ }
118
+ }
119
+ return out;
120
+ }
121
+ var SkillError = class extends Error {
122
+ constructor(code, message, details) {
123
+ super(message);
124
+ this.code = code;
125
+ if (details) this.details = details;
126
+ }
127
+ };
128
+
129
+ // scripts/src/lib/ids.mjs
130
+ var UUID_RE = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
131
+ function isRozoPaymentId(value) {
132
+ return UUID_RE.test(String(value || "").trim());
133
+ }
134
+ function maskAddress(address) {
135
+ const s = String(address ?? "").trim();
136
+ if (!s) return "(none)";
137
+ if (s.length <= 12) return s;
138
+ return `${s.slice(0, 6)}...${s.slice(-4)}`;
139
+ }
140
+
141
+ // scripts/src/lib/api.mjs
142
+ import { createRequire } from "node:module";
143
+
144
+ // scripts/src/lib/http.mjs
145
+ 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 } = {}) {
148
+ const controller = new AbortController();
149
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
150
+ let res;
151
+ try {
152
+ res = await fetch(url, {
153
+ method,
154
+ headers: {
155
+ accept: "application/json",
156
+ "user-agent": USER_AGENT,
157
+ ...body !== void 0 ? { "content-type": "application/json" } : {}
158
+ },
159
+ body: body !== void 0 ? JSON.stringify(body) : void 0,
160
+ signal: controller.signal
161
+ });
162
+ } catch (err) {
163
+ throw new SkillError(
164
+ err?.name === "AbortError" ? "HTTP_TIMEOUT" : "HTTP_UNREACHABLE",
165
+ `${method} request failed: ${redact(err?.message || String(err))}`,
166
+ { url: redactUrl(url) }
167
+ );
168
+ } finally {
169
+ clearTimeout(timer);
170
+ }
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;
178
+ }
179
+ }
180
+ if (!res.ok) {
181
+ 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
+ const message = json?.message || (typeof json?.error === "string" ? json.error : json?.error?.message) || `HTTP ${res.status}`;
183
+ throw new SkillError(code, redact(String(message)), {
184
+ httpStatus: res.status,
185
+ 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)
193
+ });
194
+ }
195
+ return json;
196
+ }
197
+ function redactUrl(url) {
198
+ try {
199
+ const u = new URL(url);
200
+ return `${u.origin}${u.pathname}`;
201
+ } catch {
202
+ return String(url);
203
+ }
204
+ }
205
+ function getJson(url, opts) {
206
+ return request("GET", url, opts);
207
+ }
208
+
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"]) {
213
+ try {
214
+ const pkg = requireFrom(candidate);
215
+ if (pkg?.name === "@rozoai/checkout" && pkg.version) return pkg.version;
216
+ } catch {
217
+ }
218
+ }
219
+ return "0.0.0";
220
+ })();
221
+ var MPP_BASE = process.env.ROZO_CHECKOUT_MPP_BASE || "https://apiserver.mpprouter.dev/v1/services/rozo-agent-api";
222
+ var CLIENT_LABEL = `rozo-checkout-cli/${PKG_VERSION}`;
223
+ var ATTRIBUTION_CLIENT = `rozo-checkout-skill/${PKG_VERSION}`;
224
+ var INTENTS_BASE = process.env.ROZO_CHECKOUT_INTENTS_BASE || "https://intentapiv4.rozo.ai/functions/v1/payment-api";
225
+ async function invoiceStatus({ linkId, rozoPaymentId }) {
226
+ const qs = new URLSearchParams();
227
+ if (linkId) qs.set("payment_id", linkId);
228
+ if (rozoPaymentId) qs.set("rozo_payment_id", rozoPaymentId);
229
+ if (![...qs.keys()].length) {
230
+ throw new SkillError("USAGE", "invoiceStatus needs linkId or rozoPaymentId.");
231
+ }
232
+ return getJson(`${MPP_BASE}/invoice-status?${qs.toString()}`);
233
+ }
234
+ async function getPayment(rozoPaymentId) {
235
+ return getJson(`${INTENTS_BASE}/payments/${encodeURIComponent(rozoPaymentId)}`);
236
+ }
237
+
238
+ // scripts/src/lib/amounts.mjs
239
+ var CHAIN_NAMES = {
240
+ 1: "Ethereum",
241
+ 56: "BNB Chain",
242
+ 137: "Polygon",
243
+ 8453: "Base",
244
+ 900: "Solana",
245
+ 1500: "Stellar",
246
+ lightning: "Bitcoin Lightning"
247
+ };
248
+ var DECIMALS = {
249
+ "1:USDC": 6,
250
+ "1:USDT": 6,
251
+ // BNB Chain: BEP-20 USDT and USDC are both 18-decimals.
252
+ "56:USDC": 18,
253
+ "56:USDT": 18,
254
+ "137:USDC": 6,
255
+ "137:USDT": 6,
256
+ "8453:USDC": 6,
257
+ "900:USDC": 6,
258
+ "900:USDT": 6,
259
+ // Stellar classic assets carry 7 decimal places.
260
+ "1500:USDC": 7
261
+ };
262
+ var AmountError = class extends Error {
263
+ constructor(code, message) {
264
+ super(message);
265
+ this.code = code;
266
+ }
267
+ };
268
+ function decimalsKey(chainId, tokenSymbol) {
269
+ return `${String(chainId).trim()}:${String(tokenSymbol || "").trim().toUpperCase()}`;
270
+ }
271
+ function decimalsFor(chainId, tokenSymbol) {
272
+ const key = decimalsKey(chainId, tokenSymbol);
273
+ const d = DECIMALS[key];
274
+ if (d === void 0) {
275
+ throw new AmountError(
276
+ "UNKNOWN_DECIMALS",
277
+ `No decimals known for ${key}; refusing to guess.`
278
+ );
279
+ }
280
+ return d;
281
+ }
282
+ function isSatsUnit(amountUnit) {
283
+ return String(amountUnit || "").trim().toLowerCase() === "sats";
284
+ }
285
+ function toAtomic(decimalString, decimals) {
286
+ if (typeof decimalString === "number") decimalString = String(decimalString);
287
+ if (typeof decimalString !== "string") {
288
+ throw new AmountError("BAD_AMOUNT", "Amount must be a string or number.");
289
+ }
290
+ const s = decimalString.trim();
291
+ if (!/^\d+(\.\d+)?$/.test(s)) {
292
+ throw new AmountError("BAD_AMOUNT", `Not a plain decimal amount: ${s}`);
293
+ }
294
+ if (!Number.isInteger(decimals) || decimals < 0 || decimals > 36) {
295
+ throw new AmountError("BAD_DECIMALS", `Invalid decimals: ${decimals}`);
296
+ }
297
+ const [whole, frac = ""] = s.split(".");
298
+ if (frac.length > decimals) {
299
+ const excess = frac.slice(decimals);
300
+ if (/[^0]/.test(excess)) {
301
+ throw new AmountError(
302
+ "AMOUNT_PRECISION",
303
+ `Amount ${s} has more precision than ${decimals} decimals allow.`
304
+ );
305
+ }
306
+ }
307
+ const padded = (frac + "0".repeat(decimals)).slice(0, decimals);
308
+ return BigInt(whole + (decimals > 0 ? padded : ""));
309
+ }
310
+ function satsToAtomic(value) {
311
+ const s = typeof value === "number" ? String(value) : String(value ?? "").trim();
312
+ if (!/^\d+$/.test(s)) {
313
+ throw new AmountError("BAD_SATS", `Lightning amount must be integer sats: ${s}`);
314
+ }
315
+ return BigInt(s);
316
+ }
317
+ function sourceAtomic(source, field) {
318
+ const raw = source?.[field];
319
+ if (raw === null || raw === void 0 || raw === "") return null;
320
+ if (isSatsUnit(source?.amountUnit)) return satsToAtomic(raw);
321
+ return toAtomic(raw, decimalsFor(source?.chainId, source?.tokenSymbol));
322
+ }
323
+ function comparePayment(source) {
324
+ const expected = sourceAtomic(source, "amount");
325
+ if (expected === null) {
326
+ throw new AmountError("MISSING_EXPECTED_AMOUNT", "source.amount is missing.");
327
+ }
328
+ const received = sourceAtomic(source, "amountReceived");
329
+ if (received === null || received === 0n) {
330
+ return {
331
+ state: "none",
332
+ expectedAtomic: expected.toString(),
333
+ receivedAtomic: (received ?? 0n).toString(),
334
+ deltaAtomic: (0n - expected).toString()
335
+ };
336
+ }
337
+ const delta = received - expected;
338
+ return {
339
+ state: delta === 0n ? "exact" : delta < 0n ? "underpaid" : "overpaid",
340
+ expectedAtomic: expected.toString(),
341
+ receivedAtomic: received.toString(),
342
+ deltaAtomic: delta.toString()
343
+ };
344
+ }
345
+ function formatAmount(source) {
346
+ const amount = source?.amount;
347
+ if (isSatsUnit(source?.amountUnit)) return `${amount} sats`;
348
+ return `${amount} ${source?.tokenSymbol ?? ""}`.trim();
349
+ }
350
+ function chainName(chainId) {
351
+ return CHAIN_NAMES[String(chainId)] || CHAIN_NAMES[chainId] || `chain ${chainId}`;
352
+ }
353
+
354
+ // scripts/src/lib/guards.mjs
355
+ function receiptSignal(source) {
356
+ const raw = source?.amountReceived;
357
+ if (raw === null || raw === void 0 || raw === "") {
358
+ return { money: false, receipt: null, unparsable: false };
359
+ }
360
+ try {
361
+ const receipt = comparePayment(source);
362
+ return { money: receipt.state !== "none", receipt, unparsable: false };
363
+ } catch {
364
+ return { money: true, receipt: null, unparsable: true };
365
+ }
366
+ }
367
+ function checkPayable(statusResponse, now = Date.now()) {
368
+ const cb = statusResponse?.coinbase;
369
+ if (!cb) {
370
+ return {
371
+ ok: false,
372
+ code: "LINK_NO_LONGER_PAYABLE",
373
+ reason: "invoice-status returned no Coinbase state; cannot prove the link is still payable.",
374
+ derived: null
375
+ };
376
+ }
377
+ const protocolVersion = cb.protocolVersion ?? statusResponse?.protocolVersion ?? null;
378
+ const derived = {
379
+ protocolVersion,
380
+ status: cb.status ?? null,
381
+ settled: cb.settled ?? null,
382
+ usageCount: cb.usageCount ?? null,
383
+ maxUsage: cb.maxUsage ?? null,
384
+ preApprovalExpiry: cb.preApprovalExpiry ?? null
385
+ };
386
+ if (cb.settled === true) {
387
+ return {
388
+ ok: false,
389
+ code: "LINK_NO_LONGER_PAYABLE",
390
+ reason: "The Coinbase resource is already settled \u2014 someone has paid it.",
391
+ derived
392
+ };
393
+ }
394
+ if (protocolVersion === "v3") {
395
+ if (!cb.status) {
396
+ return {
397
+ ok: false,
398
+ code: "LINK_PAYABILITY_UNKNOWN",
399
+ reason: "The Payment Session response carries no status; cannot prove it is still payable.",
400
+ derived
401
+ };
402
+ }
403
+ if (cb.status !== "PAYMENT_SESSION_STATUS_CREATED") {
404
+ return {
405
+ ok: false,
406
+ code: "LINK_NO_LONGER_PAYABLE",
407
+ reason: `Payment Session status is ${cb.status}; only PAYMENT_SESSION_STATUS_CREATED is payable.`,
408
+ derived
409
+ };
410
+ }
411
+ return { ok: true, code: null, reason: null, derived };
412
+ }
413
+ const usage2 = Number(cb.usageCount);
414
+ const max = Number(cb.maxUsage);
415
+ if (cb.usageCount === null || cb.usageCount === void 0 || cb.maxUsage === null || cb.maxUsage === void 0 || !Number.isFinite(usage2) || !Number.isFinite(max)) {
416
+ return {
417
+ ok: false,
418
+ code: "LINK_PAYABILITY_UNKNOWN",
419
+ reason: "The payment link response is missing usageCount/maxUsage; cannot prove it has not already been used.",
420
+ derived
421
+ };
422
+ }
423
+ if (usage2 >= max) {
424
+ return {
425
+ ok: false,
426
+ code: "LINK_NO_LONGER_PAYABLE",
427
+ reason: `Payment link already used (${usage2}/${max}).`,
428
+ derived
429
+ };
430
+ }
431
+ return { ok: true, code: null, reason: null, derived };
432
+ }
433
+ function classifyStatus({
434
+ payment,
435
+ routerState,
436
+ coinbase,
437
+ now = Date.now(),
438
+ viewsFailed = false,
439
+ provider = "coinbase"
440
+ }) {
441
+ const bitrefill = provider === "bitrefill";
442
+ const source = payment?.source || {};
443
+ const hasTx = Boolean(source.txHash);
444
+ const confirmed = Boolean(source.confirmedAt);
445
+ const signal = receiptSignal(source);
446
+ const receipt = signal.receipt;
447
+ const moneyDetected = hasTx || confirmed || signal.money;
448
+ const routerStatus = routerState?.status ?? null;
449
+ const mk = (state, detail, opts = {}) => ({
450
+ state,
451
+ moneyDetected: Boolean(moneyDetected),
452
+ terminal: Boolean(opts.terminal),
453
+ escalate: Boolean(opts.escalate),
454
+ unknown: Boolean(opts.unknown),
455
+ detail,
456
+ receipt,
457
+ receiptUnparsable: signal.unparsable,
458
+ routerStatus
459
+ });
460
+ if (viewsFailed || !payment?.status && !routerStatus && !coinbase) {
461
+ return mk(
462
+ "unknown",
463
+ "Could not read the order state from the backend. This is NOT evidence that nothing has been paid \u2014 do not act on it.",
464
+ { unknown: true }
465
+ );
466
+ }
467
+ if (signal.unparsable) {
468
+ return mk(
469
+ "stuck_after_payment",
470
+ "The order reports an amountReceived that cannot be read. Treating it as funded until a human confirms otherwise.",
471
+ { escalate: true }
472
+ );
473
+ }
474
+ if (routerStatus === "failed_pay_invoice" || routerStatus === "failed_insufficient_balance") {
475
+ return mk(
476
+ "stuck_after_payment",
477
+ `Fulfillment failed (${routerStatus}) after the pay-in. Do not pay again \u2014 escalate for manual reconciliation.`,
478
+ { terminal: false, escalate: true }
479
+ );
480
+ }
481
+ if (!bitrefill && (routerStatus === "paid" || coinbase?.settled === true)) {
482
+ return mk("settled", "Coinbase invoice settled by the funder wallet.", { terminal: true });
483
+ }
484
+ if (moneyDetected && receipt && receipt.state === "underpaid") {
485
+ return mk(
486
+ "underpaid",
487
+ "Less arrived than the order requires. Do NOT send a top-up to the same address \u2014 escalate.",
488
+ { escalate: true }
489
+ );
490
+ }
491
+ if (moneyDetected && receipt && receipt.state === "overpaid") {
492
+ return mk("payin_detected", "More arrived than required; escalate for operator follow-up.", {
493
+ escalate: true
494
+ });
495
+ }
496
+ if (bitrefill) {
497
+ const payoutTx = Boolean(payment?.destination?.txHash);
498
+ if (payment?.status === "payment_payout_completed" || payment?.status === "payment_completed" && payoutTx) {
499
+ return mk("settled", "USDC delivered to the Bitrefill invoice address on Base.", {
500
+ terminal: true
501
+ });
502
+ }
503
+ if (payment?.status === "payment_completed") {
504
+ return mk("bridging", "Payment completed but no payout transaction is visible yet. Keep polling.");
505
+ }
506
+ }
507
+ switch (payment?.status) {
508
+ case "payment_unpaid": {
509
+ if (moneyDetected) {
510
+ return mk("payin_detected", "Pay-in seen on chain, waiting for confirmations.");
511
+ }
512
+ const exp = payment?.expiresAt ? Date.parse(payment.expiresAt) : NaN;
513
+ if (Number.isFinite(exp) && exp < now) {
514
+ return mk("expired_unfunded", "The order expired before any funds arrived. This order can no longer be paid.", {
515
+ terminal: true
516
+ });
517
+ }
518
+ return mk("awaiting_deposit", "Waiting for the deposit.");
519
+ }
520
+ case "payment_started":
521
+ return mk("payin_detected", "Pay-in seen, waiting for confirmations.");
522
+ case "payment_payin_completed":
523
+ return mk("payin_confirmed", "Pay-in confirmed; fulfillment can start.");
524
+ case "payment_bridging":
525
+ case "payment_payout_started":
526
+ return mk("bridging", "Bridging the pay-in toward the funder.");
527
+ case "payment_payout_completed":
528
+ return mk(
529
+ routerStatus === "paying" ? "paying_coinbase" : "bridging",
530
+ routerStatus === "paying" ? "Funder is paying the Coinbase invoice." : "Payout landed; waiting on Coinbase settlement."
531
+ );
532
+ case "payment_completed":
533
+ return mk(
534
+ "paying_coinbase",
535
+ "The bridge leg completed, but Coinbase settlement is not yet confirmed. Keep polling."
536
+ );
537
+ case "payment_expired":
538
+ return moneyDetected ? mk("stuck_after_payment", "Order expired AFTER funds arrived \u2014 escalate immediately.", {
539
+ escalate: true
540
+ }) : mk("expired_unfunded", "Order expired unfunded. This order can no longer be paid.", { terminal: true });
541
+ case "payment_bounced":
542
+ case "payment_refunded":
543
+ return mk("stuck_after_payment", `Order ended as ${payment.status} \u2014 escalate.`, {
544
+ escalate: true
545
+ });
546
+ default:
547
+ break;
548
+ }
549
+ if (routerStatus === "payin_seen") return mk("payin_confirmed", "Router saw the pay-in.");
550
+ if (routerStatus === "paying") return mk("paying_coinbase", "Funder is paying Coinbase.");
551
+ return mk(
552
+ moneyDetected ? "stuck_after_payment" : "unknown",
553
+ `Unrecognized backend status "${payment?.status ?? "unknown"}"; not assuming anything about it.`,
554
+ { escalate: Boolean(moneyDetected), unknown: !moneyDetected }
555
+ );
556
+ }
557
+
558
+ // scripts/src/lib/expiry.mjs
559
+ var MINUTE = 6e4;
560
+ var MARGINS_MS = {
561
+ 1: 10 * MINUTE,
562
+ 56: 10 * MINUTE,
563
+ 137: 10 * MINUTE,
564
+ 8453: 10 * MINUTE,
565
+ 900: 5 * MINUTE,
566
+ 1500: 10 * MINUTE,
567
+ lightning: 10 * MINUTE
568
+ };
569
+ var BOLT11_MIN_VALIDITY_MS = 10 * MINUTE;
570
+ var DEFAULT_MARGIN_MS = 10 * MINUTE;
571
+ function formatRemaining(ms) {
572
+ if (!Number.isFinite(ms)) return "unknown";
573
+ if (ms <= 0) return "expired";
574
+ const totalMinutes = Math.floor(ms / 6e4);
575
+ if (totalMinutes < 1) return `${Math.floor(ms / 1e3)}s`;
576
+ if (totalMinutes < 60) return `${totalMinutes}m`;
577
+ const h = Math.floor(totalMinutes / 60);
578
+ const m = totalMinutes % 60;
579
+ return m ? `${h}h ${m}m` : `${h}h`;
580
+ }
581
+ function marginFor(chainId) {
582
+ const key = String(chainId);
583
+ return MARGINS_MS[key] ?? MARGINS_MS[chainId] ?? DEFAULT_MARGIN_MS;
584
+ }
585
+ function parseDeadline(value) {
586
+ if (value === null || value === void 0 || value === "") return null;
587
+ if (typeof value === "number" && Number.isFinite(value)) {
588
+ return value < 1e12 ? Math.round(value * 1e3) : Math.round(value);
589
+ }
590
+ const s = String(value).trim();
591
+ if (/^\d+$/.test(s)) {
592
+ const n = Number(s);
593
+ return n < 1e12 ? n * 1e3 : n;
594
+ }
595
+ const t = Date.parse(s);
596
+ return Number.isFinite(t) ? t : null;
597
+ }
598
+ function checkExpiry({
599
+ now,
600
+ chainId,
601
+ intentExpiresAt,
602
+ coinbaseExpiry,
603
+ bolt11ExpiresAt = void 0
604
+ }) {
605
+ const marginMs = marginFor(chainId);
606
+ const intentMs = parseDeadline(intentExpiresAt);
607
+ const coinbaseMs = parseDeadline(coinbaseExpiry);
608
+ const bolt11Ms = bolt11ExpiresAt === void 0 ? void 0 : parseDeadline(bolt11ExpiresAt);
609
+ const deadlines = { intentMs, coinbaseMs, bolt11Ms: bolt11Ms ?? null };
610
+ const base = {
611
+ marginMs,
612
+ effectiveDeadlineMs: null,
613
+ msRemaining: null,
614
+ msOfSlack: null,
615
+ deadlines
616
+ };
617
+ if (intentMs === null) {
618
+ return {
619
+ ...base,
620
+ ok: false,
621
+ code: "EXPIRY_UNPARSABLE",
622
+ reason: "Intent expiresAt is missing or unparsable."
623
+ };
624
+ }
625
+ if (coinbaseMs === null) {
626
+ return {
627
+ ...base,
628
+ ok: false,
629
+ code: "EXPIRY_UNPARSABLE",
630
+ reason: "Coinbase preApprovalExpiry is missing or unparsable."
631
+ };
632
+ }
633
+ const effective = Math.min(intentMs, coinbaseMs);
634
+ const msRemaining = effective - now;
635
+ const msOfSlack = msRemaining - marginMs;
636
+ const withDeadline = {
637
+ ...base,
638
+ effectiveDeadlineMs: effective,
639
+ msRemaining,
640
+ msOfSlack
641
+ };
642
+ if (msRemaining <= 0) {
643
+ return {
644
+ ...withDeadline,
645
+ ok: false,
646
+ code: "EXPIRED",
647
+ reason: "The order or the Coinbase link has already expired."
648
+ };
649
+ }
650
+ if (msOfSlack <= 0) {
651
+ return {
652
+ ...withDeadline,
653
+ ok: false,
654
+ code: "EXPIRY_MARGIN",
655
+ reason: `Only ${Math.floor(msRemaining / 1e3)}s left before the earliest deadline; this chain needs a ${Math.floor(marginMs / 6e4)} min safety margin.`
656
+ };
657
+ }
658
+ if (bolt11ExpiresAt !== void 0) {
659
+ if (bolt11Ms === null) {
660
+ return {
661
+ ...withDeadline,
662
+ ok: false,
663
+ code: "EXPIRY_UNPARSABLE",
664
+ reason: "BOLT11 invoice expiry is missing or unparsable."
665
+ };
666
+ }
667
+ const bolt11Remaining = bolt11Ms - now;
668
+ if (bolt11Remaining < BOLT11_MIN_VALIDITY_MS) {
669
+ return {
670
+ ...withDeadline,
671
+ ok: false,
672
+ code: "BOLT11_TOO_SHORT",
673
+ reason: `BOLT11 invoice has ${Math.max(0, Math.floor(bolt11Remaining / 1e3))}s of validity left; at least 10 min is required. Request a fresh invoice.`
674
+ };
675
+ }
676
+ }
677
+ return { ...withDeadline, ok: true, code: null, reason: null };
678
+ }
679
+
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
+ // scripts/src/lib/bitrefill.mjs
732
+ var PROVIDER_BITREFILL = "bitrefill";
733
+ var BITREFILL_DESTINATION = Object.freeze({ chainId: "8453", tokenSymbol: "USDC" });
734
+ var BITREFILL_MIN_EXPIRY_MS = 5 * 60 * 1e3;
735
+ var BITREFILL_MIN_PAY_WINDOW_MS = 2 * 60 * 1e3;
736
+ var BITREFILL_MAX_EXPIRY_AHEAD_MS = 30 * 60 * 1e3;
737
+ function providerFromPayment(payment) {
738
+ if (!payment) return null;
739
+ if (String(payment.orderId ?? payment.order_id ?? "").startsWith("bitrefill_")) return PROVIDER_BITREFILL;
740
+ const meta = payment.metadata?.provider;
741
+ if (meta) return String(meta).toLowerCase();
742
+ return null;
743
+ }
744
+ function earliestExpiry(...values) {
745
+ const ms = values.map((v) => parseDeadline(v)).filter((v) => v !== null);
746
+ return ms.length ? new Date(Math.min(...ms)).toISOString() : null;
747
+ }
748
+ function intentBitrefillExpiry(payment) {
749
+ return payment?.metadata?.bitrefillExpiresAt ?? null;
750
+ }
751
+
752
+ // scripts/src/lib/receipt.mjs
753
+ var SCHEMA_VERSION = 1;
754
+ var PAYMENT_OUTCOMES = Object.freeze([
755
+ "awaiting_payment",
756
+ "processing",
757
+ "settled",
758
+ "expired_unfunded",
759
+ "needs_attention",
760
+ "unknown"
761
+ ]);
762
+ var NEXT_ACTION_TYPES = Object.freeze([
763
+ "pay",
764
+ "check_status",
765
+ "choose_method",
766
+ "request_new_invoice",
767
+ "contact_support",
768
+ "none"
769
+ ]);
770
+ var STATE_TO_OUTCOME = Object.freeze({
771
+ awaiting_deposit: "awaiting_payment",
772
+ payin_detected: "processing",
773
+ payin_confirmed: "processing",
774
+ bridging: "processing",
775
+ paying_coinbase: "processing",
776
+ settled: "settled",
777
+ expired_unfunded: "expired_unfunded",
778
+ invoice_expired: "expired_unfunded",
779
+ deadline_unknown: "needs_attention",
780
+ underpaid: "needs_attention",
781
+ stuck_after_payment: "needs_attention",
782
+ unknown: "unknown"
783
+ });
784
+ var SERVICE_DELIVERY_NOTE = {
785
+ coinbase: "The invoice is paid. Arrival of the OpenRouter credits (or other goods) is not independently verified by Rozo; check the merchant account.",
786
+ bitrefill: "USDC reached the Bitrefill invoice address. Delivery of the Bitrefill order is not independently verified by Rozo; check the Bitrefill order page."
787
+ };
788
+ function merchantSettlementProved(result) {
789
+ if (result?.provider === "bitrefill") return result?.state === "settled";
790
+ return result?.backend?.coinbaseSettled === true;
791
+ }
792
+ function hasLocalSend(result) {
793
+ return Boolean(result?.localSend && result.localSend.status);
794
+ }
795
+ function paymentOutcomeFor(result) {
796
+ if (!result) return "unknown";
797
+ if (result.escalate) return "needs_attention";
798
+ if (result.unknown) return "unknown";
799
+ let outcome = Object.prototype.hasOwnProperty.call(STATE_TO_OUTCOME, result.state) ? STATE_TO_OUTCOME[result.state] : "unknown";
800
+ if (outcome === "settled" && !merchantSettlementProved(result)) outcome = "processing";
801
+ if (outcome === "settled" && result.provider !== "bitrefill") {
802
+ const router = result.backend?.routerStatus ?? null;
803
+ if (router && router.startsWith("failed_")) return "needs_attention";
804
+ if (router && router !== "paid") outcome = "processing";
805
+ }
806
+ if (!result.authoritativeView && (outcome === "awaiting_payment" || outcome === "expired_unfunded")) {
807
+ return "unknown";
808
+ }
809
+ if ((result.moneyDetected || hasLocalSend(result)) && outcome === "expired_unfunded") {
810
+ return "needs_attention";
811
+ }
812
+ if (result.moneyDetected && outcome === "awaiting_payment") return "processing";
813
+ return outcome;
814
+ }
815
+ function nextActionFor(result, outcome = paymentOutcomeFor(result)) {
816
+ const locked = Boolean(result?.moneyDetected) || hasLocalSend(result);
817
+ const base = { canRetryQuery: true, canCreateOrder: false, canSend: false };
818
+ const bitrefill = result?.provider === "bitrefill";
819
+ switch (outcome) {
820
+ case "awaiting_payment":
821
+ if (locked) {
822
+ return {
823
+ ...base,
824
+ type: "check_status",
825
+ message: "A send was already recorded for this order. Do NOT pay again; poll status until the pay-in shows up."
826
+ };
827
+ }
828
+ if (!result?.sendWindow?.ok) {
829
+ const code = result?.sendWindow?.code ?? "SEND_WINDOW_UNKNOWN";
830
+ if (code === "LINK_NO_LONGER_PAYABLE") {
831
+ return {
832
+ ...base,
833
+ type: "request_new_invoice",
834
+ reason: code,
835
+ message: "The merchant link no longer accepts payment. Do NOT fund this order; get a new payment link."
836
+ };
837
+ }
838
+ if (code === "EXPIRED" || code === "EXPIRY_MARGIN") {
839
+ return {
840
+ ...base,
841
+ type: "check_status",
842
+ reason: code,
843
+ message: "Too little time is left to pay this order safely. Do NOT fund it. Once it shows expired_unfunded, run pay again on the same link for a new order."
844
+ };
845
+ }
846
+ return {
847
+ ...base,
848
+ type: "check_status",
849
+ reason: code,
850
+ message: "Whether this order can still be paid could not be proved. Do NOT fund it on a guess; retry."
851
+ };
852
+ }
853
+ return {
854
+ ...base,
855
+ type: "pay",
856
+ canSend: true,
857
+ message: "Send the exact amount from the order deposit block, once."
858
+ };
859
+ case "processing":
860
+ return {
861
+ ...base,
862
+ type: "check_status",
863
+ message: "Funds are moving. Do NOT pay again; poll status."
864
+ };
865
+ case "settled":
866
+ return { ...base, type: "none", canRetryQuery: false, message: "Nothing to do." };
867
+ case "expired_unfunded":
868
+ if (bitrefill) {
869
+ return {
870
+ ...base,
871
+ type: "request_new_invoice",
872
+ message: "Nothing was funded. Create a fresh Bitrefill invoice and pay that one."
873
+ };
874
+ }
875
+ return {
876
+ ...base,
877
+ type: "choose_method",
878
+ canCreateOrder: true,
879
+ message: "Nothing arrived for this order. If your own wallet shows no send to the old deposit address and no pending Lightning payment, run pay again on the same link: it creates a new order, and you may pick a different coin. Never fund the old deposit address."
880
+ };
881
+ case "needs_attention":
882
+ if (result?.state === "deadline_unknown" && !locked) {
883
+ return {
884
+ ...base,
885
+ type: "request_new_invoice",
886
+ message: "The invoice deadline is unknown here, so the order is not payable. Re-run pay with the invoice details, or create a fresh invoice."
887
+ };
888
+ }
889
+ return {
890
+ ...base,
891
+ type: "contact_support",
892
+ message: "Do NOT pay again and do NOT create a new order. Keep linkId, rozoPaymentId and every tx hash, and contact Rozo support."
893
+ };
894
+ default:
895
+ return {
896
+ ...base,
897
+ type: "check_status",
898
+ message: "The order state could not be established. This is not evidence that nothing was paid. Retry with the rozoPaymentId before doing anything else."
899
+ };
900
+ }
901
+ }
902
+ function sourcePaymentLayer(result) {
903
+ const payin = result?.payin || {};
904
+ let status;
905
+ if (result?.unknown || !result?.authoritativeView) status = "unknown";
906
+ else if (payin.confirmedAt) status = "confirmed";
907
+ else if (result?.moneyDetected) status = "detected";
908
+ else status = "none";
909
+ return {
910
+ status,
911
+ evidence: status === "unknown" ? null : "rozo-intents",
912
+ chain: payin.chain ?? null,
913
+ expected: payin.expected ?? null,
914
+ received: payin.received ?? null,
915
+ txHash: payin.txHash ?? null,
916
+ confirmedAt: payin.confirmedAt ?? null,
917
+ localSend: result?.localSend ? {
918
+ status: result.localSend.status,
919
+ txHash: result.localSend.txHash ?? null,
920
+ claimedAt: result.localSend.claimedAt ?? null,
921
+ // A wallet broadcast proves the payer sent, not that Rozo received.
922
+ evidence: "local-wallet"
923
+ } : null
924
+ };
925
+ }
926
+ function merchantSettlementLayer(result) {
927
+ if (result?.provider === "bitrefill") {
928
+ const proved = result?.state === "settled";
929
+ return {
930
+ status: proved ? "confirmed" : result?.unknown ? "unknown" : "pending",
931
+ evidence: proved ? "rozo-payout" : null,
932
+ provider: "bitrefill",
933
+ txHash: result?.payout?.txHash ?? null,
934
+ confirmedAt: result?.payout?.confirmedAt ?? null
935
+ };
936
+ }
937
+ const backend = result?.backend || {};
938
+ let status;
939
+ if (backend.routerStatus && backend.routerStatus.startsWith("failed_")) status = "unknown";
940
+ else if (backend.coinbaseSettled === true) status = "confirmed";
941
+ else if (backend.coinbaseSettled === null || backend.coinbaseSettled === void 0) {
942
+ status = backend.routerStatus === "paid" || result?.unknown ? "unknown" : "pending";
943
+ } else status = "pending";
944
+ return {
945
+ status,
946
+ evidence: status === "confirmed" ? "coinbase" : null,
947
+ provider: "coinbase",
948
+ coinbaseStatus: backend.coinbaseStatus ?? null,
949
+ routerStatus: backend.routerStatus ?? null,
950
+ // The router reporting `paid` is recorded, but it is not settlement proof.
951
+ routerReportsPaid: backend.routerStatus === "paid",
952
+ // Coinbase may read settled because someone else paid the link.
953
+ routerFailed: Boolean(backend.routerStatus && backend.routerStatus.startsWith("failed_")),
954
+ payoutTxHash: result?.payout?.txHash ?? null
955
+ };
956
+ }
957
+ function serviceDeliveryLayer(result, merchant) {
958
+ return {
959
+ status: "unknown",
960
+ evidence: null,
961
+ note: merchant.status === "confirmed" ? SERVICE_DELIVERY_NOTE[result?.provider === "bitrefill" ? "bitrefill" : "coinbase"] : "Not applicable until the merchant invoice is settled."
962
+ };
963
+ }
964
+ function buildReceipt(result, { observedAt = (/* @__PURE__ */ new Date()).toISOString() } = {}) {
965
+ const paymentOutcome = paymentOutcomeFor(result);
966
+ const merchantSettlement = merchantSettlementLayer(result);
967
+ return {
968
+ schemaVersion: SCHEMA_VERSION,
969
+ orderId: result?.rozoPaymentId ?? null,
970
+ linkId: result?.linkId ?? null,
971
+ provider: result?.provider ?? null,
972
+ observedAt,
973
+ paymentOutcome,
974
+ state: result?.state ?? "unknown",
975
+ sourcePayment: sourcePaymentLayer(result),
976
+ merchantSettlement,
977
+ serviceDelivery: serviceDeliveryLayer(result, merchantSettlement),
978
+ nextAction: nextActionFor(result, paymentOutcome)
979
+ };
980
+ }
981
+ function receiptExitCode(outcome) {
982
+ if (outcome === "settled") return 0;
983
+ if (outcome === "expired_unfunded" || outcome === "needs_attention") return 1;
984
+ return 3;
985
+ }
986
+
987
+ // scripts/src/status.mjs
988
+ function resolveProvider(explicit, rozoPaymentId) {
989
+ if (explicit) return String(explicit).toLowerCase();
990
+ if (rozoPaymentId && isRozoPaymentId(rozoPaymentId)) {
991
+ try {
992
+ return readState(rozoPaymentId)?.provider ?? null;
993
+ } catch {
994
+ return null;
995
+ }
996
+ }
997
+ return null;
998
+ }
999
+ function localSendFor(id, read = readState) {
1000
+ if (!id || !isRozoPaymentId(id)) return null;
1001
+ let state;
1002
+ try {
1003
+ state = read(id);
1004
+ } catch (err) {
1005
+ return { status: "unreadable", txHash: null, claimedAt: null, error: err?.code ?? "STATE_UNREADABLE" };
1006
+ }
1007
+ const send = state?.send;
1008
+ if (!send || !send.status) return null;
1009
+ return { status: send.status, txHash: publicHash(send.txHash ?? null), claimedAt: send.claimedAt ?? null };
1010
+ }
1011
+ function sendWindowFor({ provider, chainId, payment, status, bitrefillExpiry, now = Date.now() }) {
1012
+ if (provider === "bitrefill") {
1013
+ const deadline = bitrefillExpiry ?? null;
1014
+ const expiry2 = checkExpiry({
1015
+ now,
1016
+ chainId,
1017
+ intentExpiresAt: payment?.expiresAt ?? deadline,
1018
+ coinbaseExpiry: deadline
1019
+ });
1020
+ return expiry2.ok ? { ok: true, code: null, minutesOfSlack: Math.floor(expiry2.msOfSlack / 6e4) } : { ok: false, code: expiry2.code, reason: expiry2.reason };
1021
+ }
1022
+ if (!status) {
1023
+ return { ok: false, code: "LINK_PAYABILITY_UNKNOWN", reason: "The Coinbase link state could not be read." };
1024
+ }
1025
+ const payable = checkPayable(status, now);
1026
+ if (!payable.ok) return { ok: false, code: payable.code, reason: payable.reason };
1027
+ const expiry = checkExpiry({
1028
+ now,
1029
+ chainId,
1030
+ intentExpiresAt: payment?.expiresAt ?? status?.rozoPayment?.expiresAt,
1031
+ coinbaseExpiry: status?.coinbase?.preApprovalExpiry
1032
+ });
1033
+ return expiry.ok ? { ok: true, code: null, minutesOfSlack: Math.floor(expiry.msOfSlack / 6e4) } : { ok: false, code: expiry.code, reason: expiry.reason };
1034
+ }
1035
+ async function snapshot({ rozoPaymentId, linkId, provider: explicitProvider }) {
1036
+ let provider = resolveProvider(explicitProvider, rozoPaymentId);
1037
+ let prefetched = null;
1038
+ if (!provider && rozoPaymentId && isRozoPaymentId(rozoPaymentId)) {
1039
+ try {
1040
+ prefetched = await getPayment(rozoPaymentId);
1041
+ provider = providerFromPayment(prefetched);
1042
+ } catch {
1043
+ prefetched = null;
1044
+ }
1045
+ }
1046
+ provider = provider === "bitrefill" ? "bitrefill" : "coinbase";
1047
+ let status = null;
1048
+ let statusError = null;
1049
+ if (provider !== "bitrefill") {
1050
+ try {
1051
+ status = await invoiceStatus({ linkId, rozoPaymentId });
1052
+ } catch (err) {
1053
+ statusError = { code: err.code, message: err.message };
1054
+ }
1055
+ }
1056
+ let id = rozoPaymentId || status?.rozo_payment_id || null;
1057
+ let idSource = rozoPaymentId ? "argument" : status?.rozo_payment_id ? "invoice-status" : null;
1058
+ if (!id && linkId) {
1059
+ const local = findByLinkId(linkId);
1060
+ if (local) {
1061
+ id = local.rozoPaymentId;
1062
+ idSource = "local state";
1063
+ }
1064
+ }
1065
+ let payment = null;
1066
+ let paymentError = null;
1067
+ if (prefetched && id === rozoPaymentId) {
1068
+ payment = prefetched;
1069
+ } else if (id && isRozoPaymentId(id)) {
1070
+ try {
1071
+ payment = await getPayment(id);
1072
+ } catch (err) {
1073
+ paymentError = { code: err.code, message: err.message };
1074
+ }
1075
+ }
1076
+ const viewsFailed = provider === "bitrefill" ? !payment : Boolean(statusError) && !payment;
1077
+ const verdict = classifyStatus({
1078
+ payment: payment || status?.rozoPayment || {},
1079
+ routerState: status?.routerState,
1080
+ coinbase: status?.coinbase,
1081
+ viewsFailed,
1082
+ provider
1083
+ });
1084
+ const source = payment?.source || status?.rozoPayment?.source || {};
1085
+ let bitrefillExpiry = null;
1086
+ let state = verdict.state;
1087
+ let terminal = verdict.terminal;
1088
+ let detail = verdict.detail;
1089
+ if (provider === "bitrefill") {
1090
+ let local = null;
1091
+ try {
1092
+ local = id && isRozoPaymentId(id) ? readState(id) : null;
1093
+ } catch {
1094
+ local = null;
1095
+ }
1096
+ bitrefillExpiry = earliestExpiry(local?.bitrefill?.expiresAt, intentBitrefillExpiry(payment));
1097
+ if (!verdict.moneyDetected && ["awaiting_deposit", "expired_unfunded"].includes(verdict.state)) {
1098
+ const ms = bitrefillExpiry ? Date.parse(bitrefillExpiry) : NaN;
1099
+ if (Number.isFinite(ms) && ms <= Date.now()) {
1100
+ state = "invoice_expired";
1101
+ terminal = true;
1102
+ detail = "The Bitrefill invoice has expired. Do NOT fund this order; create a fresh invoice.";
1103
+ } else if (!bitrefillExpiry && verdict.state === "awaiting_deposit") {
1104
+ state = "deadline_unknown";
1105
+ detail = "The Bitrefill invoice deadline is unknown on this machine, so this order is NOT shown as payable. Re-run pay with the invoice details, or create a fresh invoice.";
1106
+ }
1107
+ }
1108
+ }
1109
+ return {
1110
+ provider,
1111
+ rozoPaymentId: id,
1112
+ rozoPaymentIdSource: idSource,
1113
+ // Without the authoritative payment object we are reading a partial view.
1114
+ authoritativeView: Boolean(payment),
1115
+ linkId: linkId || status?.pl_id || null,
1116
+ state,
1117
+ unknown: verdict.unknown,
1118
+ moneyDetected: verdict.moneyDetected,
1119
+ terminal,
1120
+ escalate: verdict.escalate,
1121
+ detail,
1122
+ backend: {
1123
+ paymentStatus: payment?.status ?? status?.rozoPayment?.status ?? null,
1124
+ routerStatus: verdict.routerStatus,
1125
+ coinbaseSettled: status?.coinbase?.settled ?? null,
1126
+ coinbaseStatus: status?.coinbase?.status ?? null
1127
+ },
1128
+ payin: {
1129
+ expected: source.amount ? formatAmount(source) : null,
1130
+ received: source.amountReceived ?? null,
1131
+ receipt: verdict.receipt,
1132
+ txHash: publicHash(source.txHash ?? null),
1133
+ confirmedAt: source.confirmedAt ?? null,
1134
+ senderAddressMasked: source.senderAddress ? maskAddress(source.senderAddress) : null,
1135
+ chain: source.chainId ? chainName(source.chainId) : null
1136
+ },
1137
+ expiry: (() => {
1138
+ if (provider === "bitrefill" && !bitrefillExpiry) {
1139
+ return { expiresAt: null, expiresIn: null, msRemaining: null, deadlineUnknown: true };
1140
+ }
1141
+ const iso = provider === "bitrefill" ? bitrefillExpiry : payment?.expiresAt ?? status?.rozoPayment?.expiresAt ?? null;
1142
+ if (!iso) return { expiresAt: null, expiresIn: null, msRemaining: null };
1143
+ const ms = Date.parse(iso) - Date.now();
1144
+ return {
1145
+ expiresAt: iso,
1146
+ expiresIn: formatRemaining(ms),
1147
+ msRemaining: Number.isFinite(ms) ? ms : null
1148
+ };
1149
+ })(),
1150
+ payout: {
1151
+ txHash: publicHash(payment?.destination?.txHash ?? status?.rozoPayment?.destination?.txHash ?? null),
1152
+ confirmedAt: payment?.destination?.confirmedAt ?? status?.rozoPayment?.destination?.confirmedAt ?? null
1153
+ },
1154
+ localSend: localSendFor(id),
1155
+ sendWindow: state === "awaiting_deposit" ? sendWindowFor({ provider, chainId: source.chainId, payment, status, bitrefillExpiry }) : null,
1156
+ errors: [statusError, paymentError].filter(Boolean)
1157
+ };
1158
+ }
1159
+
1160
+ // scripts/src/receipt.mjs
1161
+ async function main(argv) {
1162
+ const args = parseArgs(argv);
1163
+ const rozoPaymentId = args["rozo-payment-id"] || (isRozoPaymentId(args._[0]) ? args._[0] : null);
1164
+ const linkId = args["link-id"] || (!rozoPaymentId ? args._[0] : null);
1165
+ const provider = args.provider ? String(args.provider) : null;
1166
+ if (provider && provider !== "bitrefill" && provider !== "coinbase") {
1167
+ usage("--provider must be coinbase or bitrefill");
1168
+ }
1169
+ if (provider === "bitrefill" && !rozoPaymentId) {
1170
+ usage("A Bitrefill order is tracked by --rozo-payment-id <uuid>.");
1171
+ }
1172
+ if (!rozoPaymentId && !linkId) {
1173
+ usage("Required: --rozo-payment-id <uuid> and/or --link-id <pl_* | paymentSession_*>");
1174
+ }
1175
+ const result = await snapshot({ rozoPaymentId, linkId, provider });
1176
+ const receipt = buildReceipt(result);
1177
+ const exitCode = receiptExitCode(receipt.paymentOutcome);
1178
+ emit(
1179
+ {
1180
+ success: receipt.paymentOutcome === "settled",
1181
+ operationOk: !result.unknown,
1182
+ step: "receipt",
1183
+ receipt,
1184
+ errors: result.errors,
1185
+ ...exitCode === 0 ? {} : { support: SUPPORT }
1186
+ },
1187
+ exitCode
1188
+ );
1189
+ }
1190
+ async function run(argv = process.argv.slice(2)) {
1191
+ return main(argv);
1192
+ }
1193
+
1194
+ // scripts/src/bin/receipt.mjs
1195
+ run().catch((err) => fail(err));