@rozoai/checkout 0.1.11 → 0.1.14

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.
@@ -6,7 +6,42 @@ const require = __rozoCreateRequire(import.meta.url);
6
6
  const __filename = __rozoFileURLToPath(import.meta.url);
7
7
  const __dirname = __rozoDirname(__filename);
8
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
+ var CONTACT_EMAIL_MAX_LENGTH = 254;
17
+ var EMAIL_RE = /^[a-z0-9][^\s@]*@[^\s@.]+(\.[^\s@.]+)+$/;
18
+ function normalizeContactEmail(raw) {
19
+ if (raw === void 0 || raw === null) return null;
20
+ if (typeof raw !== "string") return void 0;
21
+ const email = raw.trim().toLowerCase();
22
+ if (email.length === 0) return null;
23
+ if (email.length > CONTACT_EMAIL_MAX_LENGTH) return void 0;
24
+ if (/[\x00-\x1f\x7f]/.test(email) || !EMAIL_RE.test(email)) return void 0;
25
+ return email;
26
+ }
27
+ function contactEmailAttached(email, reused) {
28
+ return Boolean(email) && !reused;
29
+ }
30
+ function contactEmailFromArgs(args) {
31
+ if (args.email === void 0) return null;
32
+ const email = args.email === true ? void 0 : normalizeContactEmail(String(args.email));
33
+ if (email === void 0) {
34
+ const err = new Error(
35
+ "--email must be a valid email address, for example name@example.com. It is optional: leave it out to continue without one."
36
+ );
37
+ err.code = "INVALID_EMAIL";
38
+ throw err;
39
+ }
40
+ return email;
41
+ }
42
+
9
43
  // scripts/src/lib/output.mjs
44
+ var PUBLIC_SUPPORT_URLS = /* @__PURE__ */ new Set([SUPPORT.x, SUPPORT.discord]);
10
45
  var EXIT_OK = 0;
11
46
  var EXIT_ERROR = 1;
12
47
  var EXIT_USAGE = 2;
@@ -17,7 +52,8 @@ function redact(text) {
17
52
  s = s.replace(/\b[0-9a-fA-F]{64}\b/g, "<redacted>");
18
53
  s = s.replace(/\b[1-9A-HJ-NP-Za-km-z]{80,90}\b/g, "<redacted>");
19
54
  s = s.replace(/\[(?:\s*\d{1,3}\s*,){40,}\s*\d{1,3}\s*\]/g, "[<redacted>]");
20
- s = s.replace(/\b([a-zA-Z][a-zA-Z0-9+.-]*):\/\/([^\s"'<>]+)/g, (_m, scheme, rest) => {
55
+ s = s.replace(/\b([a-zA-Z][a-zA-Z0-9+.-]*):\/\/([^\s"'<>]+)/g, (m, scheme, rest) => {
56
+ if (PUBLIC_SUPPORT_URLS.has(m.replace(/[.,;:!?)]+$/, ""))) return m;
21
57
  const withoutUserinfo = rest.includes("@") ? rest.slice(rest.indexOf("@") + 1) : rest;
22
58
  const host = withoutUserinfo.split(/[/?#]/)[0];
23
59
  const hadMore = withoutUserinfo.length > host.length;
@@ -193,7 +229,7 @@ async function request(method, url, { body, timeoutMs = DEFAULT_TIMEOUT_MS } = {
193
229
  }
194
230
  }
195
231
  if (!res.ok) {
196
- const code = json?.code || json?.error?.code || (typeof json?.error === "string" ? null : null) || `HTTP_${res.status}`;
232
+ 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}`;
197
233
  const message = json?.message || (typeof json?.error === "string" ? json.error : json?.error?.message) || `HTTP ${res.status}`;
198
234
  throw new SkillError(code, redact(String(message)), {
199
235
  httpStatus: res.status,
@@ -224,6 +260,14 @@ function postJson(url, body, opts) {
224
260
  return request("POST", url, { ...opts, body });
225
261
  }
226
262
 
263
+ // scripts/src/lib/utm.mjs
264
+ function normalizeUtmSource(value) {
265
+ if (typeof value !== "string") return null;
266
+ const s = value.trim().toLowerCase();
267
+ if (!s || s.length > 100 || !/^[a-z0-9][a-z0-9._-]*$/.test(s)) return null;
268
+ return s;
269
+ }
270
+
227
271
  // scripts/src/lib/api.mjs
228
272
  var PKG_VERSION = (() => {
229
273
  const requireFrom = createRequire(import.meta.url);
@@ -239,18 +283,26 @@ var PKG_VERSION = (() => {
239
283
  var MPP_BASE = process.env.ROZO_CHECKOUT_MPP_BASE || "https://apiserver.mpprouter.dev/v1/services/rozo-agent-api";
240
284
  var CLIENT_LABEL = `rozo-checkout-cli/${PKG_VERSION}`;
241
285
  var ATTRIBUTION_CLIENT = `rozo-checkout-skill/${PKG_VERSION}`;
286
+ function buildAttribution({ utmSource } = {}) {
287
+ const raw = utmSource !== void 0 ? utmSource : process.env.ROZO_CHECKOUT_UTM_SOURCE;
288
+ const utm_source = normalizeUtmSource(raw);
289
+ return { client: ATTRIBUTION_CLIENT, ...utm_source ? { utm_source } : {} };
290
+ }
242
291
  var INTENTS_BASE = process.env.ROZO_CHECKOUT_INTENTS_BASE || "https://intentapiv4.rozo.ai/functions/v1/payment-api";
243
292
  async function quoteInvoice({ url, linkId }) {
244
293
  const body = url ? { url } : { payment_id: linkId };
245
294
  return postJson(`${MPP_BASE}/quote-invoice`, body);
246
295
  }
247
- async function createInvoice({ url, linkId, source, quoteReceipt }) {
296
+ async function createInvoice({ url, linkId, source, quoteReceipt, utmSource, email }) {
248
297
  const body = {
249
298
  ...url ? { url } : { payment_id: linkId },
250
299
  source: { chainId: String(source.chainId), tokenSymbol: source.tokenSymbol },
251
300
  ...quoteReceipt ? { quoteReceipt } : {},
252
301
  client: CLIENT_LABEL,
253
- attribution: { client: ATTRIBUTION_CLIENT }
302
+ attribution: buildAttribution({ utmSource }),
303
+ // Optional payer contact email (validated by the caller). Omitted, never
304
+ // sent empty, when the user gave none.
305
+ ...email ? { email } : {}
254
306
  };
255
307
  return postJson(`${MPP_BASE}/create-invoice`, body);
256
308
  }
@@ -1058,12 +1110,23 @@ function readState(rozoPaymentId) {
1058
1110
  function createOrderRecord(record) {
1059
1111
  return withLock(() => createOrderRecordUnlocked(record));
1060
1112
  }
1113
+ function mergeBitrefill(prev, next) {
1114
+ if (!next) return prev ?? null;
1115
+ if (!prev?.expiresAt || !next.expiresAt) return { ...next, expiresAt: next.expiresAt ?? prev?.expiresAt ?? null };
1116
+ const a = Date.parse(prev.expiresAt);
1117
+ const b = Date.parse(next.expiresAt);
1118
+ return { ...next, expiresAt: Number.isFinite(a) && (!Number.isFinite(b) || a < b) ? prev.expiresAt : next.expiresAt };
1119
+ }
1061
1120
  function createOrderRecordUnlocked(record) {
1062
1121
  const { rozoPaymentId } = record;
1063
1122
  const existing = readState(rozoPaymentId);
1064
1123
  const next = {
1065
1124
  version: 1,
1066
1125
  rozoPaymentId,
1126
+ // 'coinbase' (default, historical records) or 'bitrefill'. Drives which
1127
+ // payability checks presend and status run.
1128
+ provider: record.provider ?? existing?.provider ?? "coinbase",
1129
+ bitrefill: mergeBitrefill(existing?.bitrefill, record.bitrefill),
1067
1130
  linkId: record.linkId,
1068
1131
  paymentLink: record.paymentLink ?? null,
1069
1132
  merchant: record.merchant ?? null,
@@ -1142,6 +1205,7 @@ async function main(argv) {
1142
1205
  }
1143
1206
  const requested = { chainId, tokenSymbol };
1144
1207
  const confirmed = Boolean(args.confirm);
1208
+ const email = contactEmailFromArgs(args);
1145
1209
  let blacklist;
1146
1210
  try {
1147
1211
  blacklist = loadBlacklist();
@@ -1158,7 +1222,9 @@ async function main(argv) {
1158
1222
  const created = await createInvoice({
1159
1223
  url: String(url),
1160
1224
  source: requested,
1161
- quoteReceipt
1225
+ quoteReceipt,
1226
+ utmSource: typeof args["utm-source"] === "string" ? args["utm-source"] : void 0,
1227
+ email
1162
1228
  });
1163
1229
  if (!created?.rozoPaymentId) {
1164
1230
  throw new SkillError("CREATE_FAILED", "create-invoice returned no rozoPaymentId.", {
@@ -1315,6 +1381,13 @@ async function main(argv) {
1315
1381
  success: true,
1316
1382
  step: "create-order",
1317
1383
  confirmed,
1384
+ // True only when an email went out with a NEW order. A reused order keeps
1385
+ // whatever email it was first created with (the router never rebinds), so
1386
+ // reporting true there would be a false promise. The address itself is
1387
+ // never echoed. Note the --confirm run always sees the order as reused;
1388
+ // the CLI carries the first run's value forward.
1389
+ contactEmailProvided: contactEmailAttached(email, created.reused),
1390
+ support: SUPPORT,
1318
1391
  reused: Boolean(created.reused),
1319
1392
  reusedNote: created.reused ? `An existing unpaid order for this link was reused (${rozoPaymentId}), valid for another ${formatRemaining(expiry.msRemaining)}. Nothing new was created.` : null,
1320
1393
  orderCost: "Creating an order moves no money. An order you never fund simply expires and costs nothing.",
@@ -6,7 +6,16 @@ const require = __rozoCreateRequire(import.meta.url);
6
6
  const __filename = __rozoFileURLToPath(import.meta.url);
7
7
  const __dirname = __rozoDirname(__filename);
8
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
+
9
17
  // scripts/src/lib/output.mjs
18
+ var PUBLIC_SUPPORT_URLS = /* @__PURE__ */ new Set([SUPPORT.x, SUPPORT.discord]);
10
19
  var EXIT_OK = 0;
11
20
  var EXIT_ERROR = 1;
12
21
  var EXIT_USAGE = 2;
@@ -17,7 +26,8 @@ function redact(text) {
17
26
  s = s.replace(/\b[0-9a-fA-F]{64}\b/g, "<redacted>");
18
27
  s = s.replace(/\b[1-9A-HJ-NP-Za-km-z]{80,90}\b/g, "<redacted>");
19
28
  s = s.replace(/\[(?:\s*\d{1,3}\s*,){40,}\s*\d{1,3}\s*\]/g, "[<redacted>]");
20
- s = s.replace(/\b([a-zA-Z][a-zA-Z0-9+.-]*):\/\/([^\s"'<>]+)/g, (_m, scheme, rest) => {
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;
21
31
  const withoutUserinfo = rest.includes("@") ? rest.slice(rest.indexOf("@") + 1) : rest;
22
32
  const host = withoutUserinfo.split(/[/?#]/)[0];
23
33
  const hadMore = withoutUserinfo.length > host.length;
@@ -173,7 +183,7 @@ async function request(method, url, { body, timeoutMs = DEFAULT_TIMEOUT_MS } = {
173
183
  }
174
184
  }
175
185
  if (!res.ok) {
176
- const code = json?.code || json?.error?.code || (typeof json?.error === "string" ? null : null) || `HTTP_${res.status}`;
186
+ 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}`;
177
187
  const message = json?.message || (typeof json?.error === "string" ? json.error : json?.error?.message) || `HTTP ${res.status}`;
178
188
  throw new SkillError(code, redact(String(message)), {
179
189
  httpStatus: res.status,
@@ -9972,7 +9972,16 @@ var init_call = __esm({
9972
9972
  }
9973
9973
  });
9974
9974
 
9975
+ // scripts/src/lib/support.mjs
9976
+ var SUPPORT = Object.freeze({
9977
+ email: "hi@rozo.ai",
9978
+ x: "https://x.com/ROZOai",
9979
+ discord: "https://discord.gg/EfWejgTbuU"
9980
+ });
9981
+ var SUPPORT_TEXT = `Need help? Email ${SUPPORT.email}, or reach ROZO on X ${SUPPORT.x} or Discord ${SUPPORT.discord}.`;
9982
+
9975
9983
  // scripts/src/lib/output.mjs
9984
+ var PUBLIC_SUPPORT_URLS = /* @__PURE__ */ new Set([SUPPORT.x, SUPPORT.discord]);
9976
9985
  var EXIT_OK = 0;
9977
9986
  var EXIT_ERROR = 1;
9978
9987
  var EXIT_UNCONFIRMED = 3;
@@ -9983,7 +9992,8 @@ function redact(text) {
9983
9992
  s = s.replace(/\b[0-9a-fA-F]{64}\b/g, "<redacted>");
9984
9993
  s = s.replace(/\b[1-9A-HJ-NP-Za-km-z]{80,90}\b/g, "<redacted>");
9985
9994
  s = s.replace(/\[(?:\s*\d{1,3}\s*,){40,}\s*\d{1,3}\s*\]/g, "[<redacted>]");
9986
- s = s.replace(/\b([a-zA-Z][a-zA-Z0-9+.-]*):\/\/([^\s"'<>]+)/g, (_m, scheme, rest) => {
9995
+ s = s.replace(/\b([a-zA-Z][a-zA-Z0-9+.-]*):\/\/([^\s"'<>]+)/g, (m, scheme, rest) => {
9996
+ if (PUBLIC_SUPPORT_URLS.has(m.replace(/[.,;:!?)]+$/, ""))) return m;
9987
9997
  const withoutUserinfo = rest.includes("@") ? rest.slice(rest.indexOf("@") + 1) : rest;
9988
9998
  const host = withoutUserinfo.split(/[/?#]/)[0];
9989
9999
  const hadMore = withoutUserinfo.length > host.length;
@@ -21857,7 +21867,7 @@ async function request(method, url, { body, timeoutMs = DEFAULT_TIMEOUT_MS } = {
21857
21867
  }
21858
21868
  }
21859
21869
  if (!res.ok) {
21860
- const code = json?.code || json?.error?.code || (typeof json?.error === "string" ? null : null) || `HTTP_${res.status}`;
21870
+ 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}`;
21861
21871
  const message = json?.message || (typeof json?.error === "string" ? json.error : json?.error?.message) || `HTTP ${res.status}`;
21862
21872
  throw new SkillError(code, redact(String(message)), {
21863
21873
  httpStatus: res.status,
@@ -22045,6 +22055,15 @@ function reuseGuard({ payment, requested, reused = false }) {
22045
22055
  }
22046
22056
  return { ok: true, code: null, reason: null, moneyDetected: false, evidence, deposit };
22047
22057
  }
22058
+ function normalizeDecimal(v) {
22059
+ if (v === null || v === void 0) return null;
22060
+ const s = String(v).trim();
22061
+ if (!/^\d+(\.\d+)?$/.test(s)) return s;
22062
+ const [w, f = ""] = s.split(".");
22063
+ const frac = f.replace(/0+$/, "");
22064
+ const whole = w.replace(/^0+(?=\d)/, "");
22065
+ return frac ? `${whole}.${frac}` : whole;
22066
+ }
22048
22067
  function checkPayable(statusResponse, now = Date.now()) {
22049
22068
  const cb = statusResponse?.coinbase;
22050
22069
  if (!cb) {
@@ -22540,7 +22559,67 @@ function assertPaymentLimit(invoiceUsd) {
22540
22559
  return { usd, limitUsd: MAX_PAYMENT_USD };
22541
22560
  }
22542
22561
 
22562
+ // scripts/src/lib/bitrefill.mjs
22563
+ var BITREFILL_DESTINATION = Object.freeze({ chainId: "8453", tokenSymbol: "USDC" });
22564
+ var BITREFILL_MIN_EXPIRY_MS = 5 * 60 * 1e3;
22565
+ var BITREFILL_MIN_PAY_WINDOW_MS = 2 * 60 * 1e3;
22566
+ var BITREFILL_MAX_EXPIRY_AHEAD_MS = 30 * 60 * 1e3;
22567
+ function checkBitrefillExpiry(expiresAt, now = Date.now(), minMs = BITREFILL_MIN_PAY_WINDOW_MS) {
22568
+ const ms = parseDeadline(expiresAt);
22569
+ const base = { marginMs: minMs, deadlines: { invoiceMs: ms } };
22570
+ if (ms === null) {
22571
+ return { ...base, ok: false, code: "EXPIRY_UNPARSABLE", reason: "The Bitrefill invoice expiry is missing or unreadable.", effectiveDeadlineMs: null, msRemaining: null, msOfSlack: null };
22572
+ }
22573
+ const msRemaining = ms - now;
22574
+ const out = { ...base, effectiveDeadlineMs: ms, msRemaining, msOfSlack: msRemaining - minMs };
22575
+ if (msRemaining <= 0) return { ...out, ok: false, code: "EXPIRED", reason: "The Bitrefill invoice has expired." };
22576
+ if (msRemaining < minMs) {
22577
+ return {
22578
+ ...out,
22579
+ ok: false,
22580
+ code: "INVOICE_EXPIRING",
22581
+ reason: `Only ${Math.floor(msRemaining / 1e3)}s left on the Bitrefill invoice; at least ${Math.round(minMs / 6e4)} min is required. Create a fresh invoice.`
22582
+ };
22583
+ }
22584
+ return { ...out, ok: true, code: null, reason: null };
22585
+ }
22586
+ var sameAddress = (a, b) => typeof a === "string" && typeof b === "string" && a.trim().toLowerCase() === b.trim().toLowerCase();
22587
+ function verifyBitrefillDestination({ requested, payment }) {
22588
+ const dest = payment?.destination || {};
22589
+ const drift = [];
22590
+ if (String(dest.chainId ?? "") !== BITREFILL_DESTINATION.chainId) {
22591
+ drift.push({ field: "destination.chainId", requested: "8453", live: dest.chainId ?? null });
22592
+ }
22593
+ if (String(dest.tokenSymbol ?? "").toUpperCase() !== BITREFILL_DESTINATION.tokenSymbol) {
22594
+ drift.push({ field: "destination.tokenSymbol", requested: "USDC", live: dest.tokenSymbol ?? null });
22595
+ }
22596
+ if (normalizeDecimal(dest.amount) !== normalizeDecimal(requested.amount)) {
22597
+ drift.push({ field: "destination.amount", requested: requested.amount, live: dest.amount ?? null });
22598
+ }
22599
+ const liveAddress = dest.receiverAddress ?? dest.address ?? null;
22600
+ if (!sameAddress(liveAddress, requested.address)) {
22601
+ drift.push({ field: "destination.address", requested: requested.address, live: liveAddress });
22602
+ }
22603
+ return drift.length ? {
22604
+ ok: false,
22605
+ code: "BITREFILL_ECHO_MISMATCH",
22606
+ reason: "The live order does not deliver to the Bitrefill invoice. Refusing to continue.",
22607
+ drift,
22608
+ addressVerified: false
22609
+ } : { ok: true, code: null, reason: null, drift, addressVerified: true };
22610
+ }
22611
+
22543
22612
  // scripts/src/lib/presend.mjs
22613
+ function assertBitrefillLive(state, payment) {
22614
+ if (!state.bitrefill) {
22615
+ throw new SkillError("BITREFILL_ECHO_MISMATCH", "This Bitrefill order has no recorded invoice. Refusing to send.");
22616
+ }
22617
+ const dest = verifyBitrefillDestination({ requested: state.bitrefill, payment });
22618
+ if (!dest.ok) throw new SkillError(dest.code, dest.reason, { drift: dest.drift });
22619
+ const expiry = checkBitrefillExpiry(state.bitrefill.expiresAt, Date.now());
22620
+ if (!expiry.ok) throw new SkillError(expiry.code, expiry.reason, expiry);
22621
+ return expiry;
22622
+ }
22544
22623
  async function preflight({
22545
22624
  rozoPaymentId,
22546
22625
  expectFamily,
@@ -22626,16 +22705,22 @@ async function preflight({
22626
22705
  if ((source.receiverMemo ?? null) !== (state.receiverMemo ?? null)) {
22627
22706
  throw new SkillError("DEPOSIT_CHANGED", "The live deposit memo differs from the recorded one.");
22628
22707
  }
22629
- const statusNow = await invoiceStatus({ linkId: state.linkId });
22630
- const expiry = checkExpiry({
22631
- now: Date.now(),
22632
- chainId: source.chainId,
22633
- intentExpiresAt: payment?.expiresAt,
22634
- coinbaseExpiry: statusNow?.coinbase?.preApprovalExpiry
22635
- });
22636
- if (!expiry.ok) throw new SkillError(expiry.code, expiry.reason, expiry);
22637
- const payable = checkPayable(statusNow, Date.now());
22638
- if (!payable.ok) throw new SkillError(payable.code, payable.reason, payable.derived);
22708
+ let statusNow = null;
22709
+ let expiry;
22710
+ if (state.provider === "bitrefill") {
22711
+ expiry = assertBitrefillLive(state, payment);
22712
+ } else {
22713
+ statusNow = await invoiceStatus({ linkId: state.linkId });
22714
+ expiry = checkExpiry({
22715
+ now: Date.now(),
22716
+ chainId: source.chainId,
22717
+ intentExpiresAt: payment?.expiresAt,
22718
+ coinbaseExpiry: statusNow?.coinbase?.preApprovalExpiry
22719
+ });
22720
+ if (!expiry.ok) throw new SkillError(expiry.code, expiry.reason, expiry);
22721
+ const payable = checkPayable(statusNow, Date.now());
22722
+ if (!payable.ok) throw new SkillError(payable.code, payable.reason, payable.derived);
22723
+ }
22639
22724
  assertNotBlacklisted(
22640
22725
  [
22641
22726
  { address: source.receiverAddress, family, role: "deposit address" },
@@ -22654,7 +22739,17 @@ async function preflight({
22654
22739
  statusNow
22655
22740
  };
22656
22741
  }
22657
- async function finalPayabilityCheck({ linkId, chainId, intentExpiresAt }) {
22742
+ async function finalPayabilityCheck({ linkId, chainId, intentExpiresAt, state = null, rozoPaymentId = null }) {
22743
+ if (state?.provider === "bitrefill") {
22744
+ const id = rozoPaymentId ?? state.rozoPaymentId;
22745
+ const payment = await getPayment(id);
22746
+ const guard = reuseGuard({ payment, requested: state.source });
22747
+ if (!guard.ok) {
22748
+ throw new SkillError(guard.code, guard.reason, { ...guard.evidence, moneyDetected: guard.moneyDetected });
22749
+ }
22750
+ const expiry2 = assertBitrefillLive(state, payment);
22751
+ return { statusNow: null, payable: { ok: true }, expiry: expiry2 };
22752
+ }
22658
22753
  const statusNow = await invoiceStatus({ linkId });
22659
22754
  const payable = checkPayable(statusNow, Date.now());
22660
22755
  if (!payable.ok) throw new SkillError(payable.code, payable.reason, payable.derived);
@@ -23018,7 +23113,9 @@ async function main(argv) {
23018
23113
  await finalPayabilityCheck({
23019
23114
  linkId: state.linkId,
23020
23115
  chainId: source.chainId,
23021
- intentExpiresAt: payment?.expiresAt
23116
+ intentExpiresAt: payment?.expiresAt,
23117
+ state,
23118
+ rozoPaymentId
23022
23119
  });
23023
23120
  claimSend(
23024
23121
  rozoPaymentId,
@@ -32143,7 +32143,16 @@ Message: ${transactionMessage}.
32143
32143
  }
32144
32144
  });
32145
32145
 
32146
+ // scripts/src/lib/support.mjs
32147
+ var SUPPORT = Object.freeze({
32148
+ email: "hi@rozo.ai",
32149
+ x: "https://x.com/ROZOai",
32150
+ discord: "https://discord.gg/EfWejgTbuU"
32151
+ });
32152
+ var SUPPORT_TEXT = `Need help? Email ${SUPPORT.email}, or reach ROZO on X ${SUPPORT.x} or Discord ${SUPPORT.discord}.`;
32153
+
32146
32154
  // scripts/src/lib/output.mjs
32155
+ var PUBLIC_SUPPORT_URLS = /* @__PURE__ */ new Set([SUPPORT.x, SUPPORT.discord]);
32147
32156
  var EXIT_OK = 0;
32148
32157
  var EXIT_ERROR = 1;
32149
32158
  var EXIT_UNCONFIRMED = 3;
@@ -32154,7 +32163,8 @@ function redact(text) {
32154
32163
  s = s.replace(/\b[0-9a-fA-F]{64}\b/g, "<redacted>");
32155
32164
  s = s.replace(/\b[1-9A-HJ-NP-Za-km-z]{80,90}\b/g, "<redacted>");
32156
32165
  s = s.replace(/\[(?:\s*\d{1,3}\s*,){40,}\s*\d{1,3}\s*\]/g, "[<redacted>]");
32157
- s = s.replace(/\b([a-zA-Z][a-zA-Z0-9+.-]*):\/\/([^\s"'<>]+)/g, (_m, scheme, rest) => {
32166
+ s = s.replace(/\b([a-zA-Z][a-zA-Z0-9+.-]*):\/\/([^\s"'<>]+)/g, (m, scheme, rest) => {
32167
+ if (PUBLIC_SUPPORT_URLS.has(m.replace(/[.,;:!?)]+$/, ""))) return m;
32158
32168
  const withoutUserinfo = rest.includes("@") ? rest.slice(rest.indexOf("@") + 1) : rest;
32159
32169
  const host = withoutUserinfo.split(/[/?#]/)[0];
32160
32170
  const hadMore = withoutUserinfo.length > host.length;
@@ -33043,7 +33053,7 @@ async function request(method, url, { body, timeoutMs = DEFAULT_TIMEOUT_MS } = {
33043
33053
  }
33044
33054
  }
33045
33055
  if (!res.ok) {
33046
- const code = json?.code || json?.error?.code || (typeof json?.error === "string" ? null : null) || `HTTP_${res.status}`;
33056
+ 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}`;
33047
33057
  const message = json?.message || (typeof json?.error === "string" ? json.error : json?.error?.message) || `HTTP ${res.status}`;
33048
33058
  throw new SkillError(code, redact(String(message)), {
33049
33059
  httpStatus: res.status,
@@ -33231,6 +33241,15 @@ function reuseGuard({ payment, requested, reused = false }) {
33231
33241
  }
33232
33242
  return { ok: true, code: null, reason: null, moneyDetected: false, evidence, deposit };
33233
33243
  }
33244
+ function normalizeDecimal(v) {
33245
+ if (v === null || v === void 0) return null;
33246
+ const s = String(v).trim();
33247
+ if (!/^\d+(\.\d+)?$/.test(s)) return s;
33248
+ const [w, f = ""] = s.split(".");
33249
+ const frac = f.replace(/0+$/, "");
33250
+ const whole = w.replace(/^0+(?=\d)/, "");
33251
+ return frac ? `${whole}.${frac}` : whole;
33252
+ }
33234
33253
  function checkPayable(statusResponse, now = Date.now()) {
33235
33254
  const cb = statusResponse?.coinbase;
33236
33255
  if (!cb) {
@@ -33726,7 +33745,67 @@ function assertPaymentLimit(invoiceUsd) {
33726
33745
  return { usd, limitUsd: MAX_PAYMENT_USD };
33727
33746
  }
33728
33747
 
33748
+ // scripts/src/lib/bitrefill.mjs
33749
+ var BITREFILL_DESTINATION = Object.freeze({ chainId: "8453", tokenSymbol: "USDC" });
33750
+ var BITREFILL_MIN_EXPIRY_MS = 5 * 60 * 1e3;
33751
+ var BITREFILL_MIN_PAY_WINDOW_MS = 2 * 60 * 1e3;
33752
+ var BITREFILL_MAX_EXPIRY_AHEAD_MS = 30 * 60 * 1e3;
33753
+ function checkBitrefillExpiry(expiresAt, now = Date.now(), minMs = BITREFILL_MIN_PAY_WINDOW_MS) {
33754
+ const ms = parseDeadline(expiresAt);
33755
+ const base = { marginMs: minMs, deadlines: { invoiceMs: ms } };
33756
+ if (ms === null) {
33757
+ return { ...base, ok: false, code: "EXPIRY_UNPARSABLE", reason: "The Bitrefill invoice expiry is missing or unreadable.", effectiveDeadlineMs: null, msRemaining: null, msOfSlack: null };
33758
+ }
33759
+ const msRemaining = ms - now;
33760
+ const out = { ...base, effectiveDeadlineMs: ms, msRemaining, msOfSlack: msRemaining - minMs };
33761
+ if (msRemaining <= 0) return { ...out, ok: false, code: "EXPIRED", reason: "The Bitrefill invoice has expired." };
33762
+ if (msRemaining < minMs) {
33763
+ return {
33764
+ ...out,
33765
+ ok: false,
33766
+ code: "INVOICE_EXPIRING",
33767
+ reason: `Only ${Math.floor(msRemaining / 1e3)}s left on the Bitrefill invoice; at least ${Math.round(minMs / 6e4)} min is required. Create a fresh invoice.`
33768
+ };
33769
+ }
33770
+ return { ...out, ok: true, code: null, reason: null };
33771
+ }
33772
+ var sameAddress = (a, b) => typeof a === "string" && typeof b === "string" && a.trim().toLowerCase() === b.trim().toLowerCase();
33773
+ function verifyBitrefillDestination({ requested, payment }) {
33774
+ const dest = payment?.destination || {};
33775
+ const drift = [];
33776
+ if (String(dest.chainId ?? "") !== BITREFILL_DESTINATION.chainId) {
33777
+ drift.push({ field: "destination.chainId", requested: "8453", live: dest.chainId ?? null });
33778
+ }
33779
+ if (String(dest.tokenSymbol ?? "").toUpperCase() !== BITREFILL_DESTINATION.tokenSymbol) {
33780
+ drift.push({ field: "destination.tokenSymbol", requested: "USDC", live: dest.tokenSymbol ?? null });
33781
+ }
33782
+ if (normalizeDecimal(dest.amount) !== normalizeDecimal(requested.amount)) {
33783
+ drift.push({ field: "destination.amount", requested: requested.amount, live: dest.amount ?? null });
33784
+ }
33785
+ const liveAddress = dest.receiverAddress ?? dest.address ?? null;
33786
+ if (!sameAddress(liveAddress, requested.address)) {
33787
+ drift.push({ field: "destination.address", requested: requested.address, live: liveAddress });
33788
+ }
33789
+ return drift.length ? {
33790
+ ok: false,
33791
+ code: "BITREFILL_ECHO_MISMATCH",
33792
+ reason: "The live order does not deliver to the Bitrefill invoice. Refusing to continue.",
33793
+ drift,
33794
+ addressVerified: false
33795
+ } : { ok: true, code: null, reason: null, drift, addressVerified: true };
33796
+ }
33797
+
33729
33798
  // scripts/src/lib/presend.mjs
33799
+ function assertBitrefillLive(state, payment) {
33800
+ if (!state.bitrefill) {
33801
+ throw new SkillError("BITREFILL_ECHO_MISMATCH", "This Bitrefill order has no recorded invoice. Refusing to send.");
33802
+ }
33803
+ const dest = verifyBitrefillDestination({ requested: state.bitrefill, payment });
33804
+ if (!dest.ok) throw new SkillError(dest.code, dest.reason, { drift: dest.drift });
33805
+ const expiry = checkBitrefillExpiry(state.bitrefill.expiresAt, Date.now());
33806
+ if (!expiry.ok) throw new SkillError(expiry.code, expiry.reason, expiry);
33807
+ return expiry;
33808
+ }
33730
33809
  async function preflight({
33731
33810
  rozoPaymentId,
33732
33811
  expectFamily,
@@ -33812,16 +33891,22 @@ async function preflight({
33812
33891
  if ((source.receiverMemo ?? null) !== (state.receiverMemo ?? null)) {
33813
33892
  throw new SkillError("DEPOSIT_CHANGED", "The live deposit memo differs from the recorded one.");
33814
33893
  }
33815
- const statusNow = await invoiceStatus({ linkId: state.linkId });
33816
- const expiry = checkExpiry({
33817
- now: Date.now(),
33818
- chainId: source.chainId,
33819
- intentExpiresAt: payment?.expiresAt,
33820
- coinbaseExpiry: statusNow?.coinbase?.preApprovalExpiry
33821
- });
33822
- if (!expiry.ok) throw new SkillError(expiry.code, expiry.reason, expiry);
33823
- const payable = checkPayable(statusNow, Date.now());
33824
- if (!payable.ok) throw new SkillError(payable.code, payable.reason, payable.derived);
33894
+ let statusNow = null;
33895
+ let expiry;
33896
+ if (state.provider === "bitrefill") {
33897
+ expiry = assertBitrefillLive(state, payment);
33898
+ } else {
33899
+ statusNow = await invoiceStatus({ linkId: state.linkId });
33900
+ expiry = checkExpiry({
33901
+ now: Date.now(),
33902
+ chainId: source.chainId,
33903
+ intentExpiresAt: payment?.expiresAt,
33904
+ coinbaseExpiry: statusNow?.coinbase?.preApprovalExpiry
33905
+ });
33906
+ if (!expiry.ok) throw new SkillError(expiry.code, expiry.reason, expiry);
33907
+ const payable = checkPayable(statusNow, Date.now());
33908
+ if (!payable.ok) throw new SkillError(payable.code, payable.reason, payable.derived);
33909
+ }
33825
33910
  assertNotBlacklisted(
33826
33911
  [
33827
33912
  { address: source.receiverAddress, family, role: "deposit address" },
@@ -33840,7 +33925,17 @@ async function preflight({
33840
33925
  statusNow
33841
33926
  };
33842
33927
  }
33843
- async function finalPayabilityCheck({ linkId, chainId, intentExpiresAt }) {
33928
+ async function finalPayabilityCheck({ linkId, chainId, intentExpiresAt, state = null, rozoPaymentId = null }) {
33929
+ if (state?.provider === "bitrefill") {
33930
+ const id = rozoPaymentId ?? state.rozoPaymentId;
33931
+ const payment = await getPayment(id);
33932
+ const guard = reuseGuard({ payment, requested: state.source });
33933
+ if (!guard.ok) {
33934
+ throw new SkillError(guard.code, guard.reason, { ...guard.evidence, moneyDetected: guard.moneyDetected });
33935
+ }
33936
+ const expiry2 = assertBitrefillLive(state, payment);
33937
+ return { statusNow: null, payable: { ok: true }, expiry: expiry2 };
33938
+ }
33844
33939
  const statusNow = await invoiceStatus({ linkId });
33845
33940
  const payable = checkPayable(statusNow, Date.now());
33846
33941
  if (!payable.ok) throw new SkillError(payable.code, payable.reason, payable.derived);
@@ -34109,7 +34204,9 @@ async function main(argv) {
34109
34204
  await finalPayabilityCheck({
34110
34205
  linkId: state.linkId,
34111
34206
  chainId: source.chainId,
34112
- intentExpiresAt: payment?.expiresAt
34207
+ intentExpiresAt: payment?.expiresAt,
34208
+ state,
34209
+ rozoPaymentId
34113
34210
  });
34114
34211
  claimSend(
34115
34212
  rozoPaymentId,