@rozoai/checkout 0.1.16 → 0.1.17

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.
@@ -277,6 +277,310 @@ function normalizeUtmSource(value) {
277
277
  return s;
278
278
  }
279
279
 
280
+ // scripts/src/lib/identity.mjs
281
+ import crypto2 from "node:crypto";
282
+
283
+ // scripts/src/lib/prefs.mjs
284
+ import fs2 from "node:fs";
285
+ import path2 from "node:path";
286
+ import os2 from "node:os";
287
+
288
+ // scripts/src/lib/state.mjs
289
+ import fs from "node:fs";
290
+ import path from "node:path";
291
+ import os from "node:os";
292
+ import crypto from "node:crypto";
293
+ var LOCK_STALE_MS = 6e4;
294
+ var LOCK_WAIT_MS = 1e4;
295
+ var LOCK_POLL_MS = 25;
296
+ function lockPath() {
297
+ return path.join(stateRoot(), ".send.lock");
298
+ }
299
+ function sleepSync(ms) {
300
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
301
+ }
302
+ function tryAcquire(file) {
303
+ try {
304
+ const fd = fs.openSync(file, "wx", 384);
305
+ fs.writeFileSync(fd, JSON.stringify({ pid: process.pid, at: (/* @__PURE__ */ new Date()).toISOString() }));
306
+ fs.closeSync(fd);
307
+ return true;
308
+ } catch (err) {
309
+ if (err.code !== "EEXIST") throw err;
310
+ return false;
311
+ }
312
+ }
313
+ function withLock(fn) {
314
+ const file = lockPath();
315
+ fs.mkdirSync(path.dirname(file), { recursive: true, mode: 448 });
316
+ const deadline = Date.now() + LOCK_WAIT_MS;
317
+ for (; ; ) {
318
+ if (tryAcquire(file)) break;
319
+ try {
320
+ const age = Date.now() - fs.statSync(file).mtimeMs;
321
+ if (age > LOCK_STALE_MS) {
322
+ const stolen = `${file}.stale.${crypto.randomBytes(4).toString("hex")}`;
323
+ try {
324
+ fs.renameSync(file, stolen);
325
+ fs.unlinkSync(stolen);
326
+ } catch {
327
+ }
328
+ continue;
329
+ }
330
+ } catch (err) {
331
+ if (err.code !== "ENOENT") throw err;
332
+ continue;
333
+ }
334
+ if (Date.now() > deadline) {
335
+ throw new SkillError(
336
+ "LOCK_TIMEOUT",
337
+ "Another rozo-checkout process is holding the send lock. Refusing to proceed rather than risk a concurrent second send."
338
+ );
339
+ }
340
+ sleepSync(LOCK_POLL_MS);
341
+ }
342
+ try {
343
+ return fn();
344
+ } finally {
345
+ try {
346
+ fs.unlinkSync(file);
347
+ } catch {
348
+ }
349
+ }
350
+ }
351
+ function stateRoot() {
352
+ return process.env.ROZO_CHECKOUT_STATE_DIR || path.join(os.homedir(), ".rozo-checkout", "state");
353
+ }
354
+ function statePath(rozoPaymentId) {
355
+ if (!/^[A-Za-z0-9-]{8,64}$/.test(String(rozoPaymentId || ""))) {
356
+ throw new SkillError("BAD_ROZO_PAYMENT_ID", "Refusing to build a state path from that id.");
357
+ }
358
+ return path.join(stateRoot(), `${rozoPaymentId}.json`);
359
+ }
360
+ function writeAtomic(file, data) {
361
+ const dir = path.dirname(file);
362
+ fs.mkdirSync(dir, { recursive: true, mode: 448 });
363
+ const tmp = path.join(dir, `.${path.basename(file)}.${crypto.randomBytes(6).toString("hex")}.tmp`);
364
+ const fd = fs.openSync(tmp, "wx", 384);
365
+ try {
366
+ fs.writeFileSync(fd, JSON.stringify(data, null, 2) + "\n", "utf8");
367
+ fs.fsyncSync(fd);
368
+ } finally {
369
+ fs.closeSync(fd);
370
+ }
371
+ fs.renameSync(tmp, file);
372
+ }
373
+ function readState(rozoPaymentId) {
374
+ const file = statePath(rozoPaymentId);
375
+ let raw;
376
+ try {
377
+ raw = fs.readFileSync(file, "utf8");
378
+ } catch (err) {
379
+ if (err.code === "ENOENT") return null;
380
+ throw new SkillError("STATE_UNREADABLE", `Cannot read local state: ${err.code}`);
381
+ }
382
+ try {
383
+ return JSON.parse(raw);
384
+ } catch {
385
+ throw new SkillError(
386
+ "STATE_CORRUPT",
387
+ "The local state file for this order is corrupt. Refusing to act; inspect it manually."
388
+ );
389
+ }
390
+ }
391
+ function createOrderRecord(record) {
392
+ return withLock(() => createOrderRecordUnlocked(record));
393
+ }
394
+ function mergeBitrefill(prev, next) {
395
+ if (!next) return prev ?? null;
396
+ if (!prev?.expiresAt || !next.expiresAt) return { ...next, expiresAt: next.expiresAt ?? prev?.expiresAt ?? null };
397
+ const a = Date.parse(prev.expiresAt);
398
+ const b = Date.parse(next.expiresAt);
399
+ return { ...next, expiresAt: Number.isFinite(a) && (!Number.isFinite(b) || a < b) ? prev.expiresAt : next.expiresAt };
400
+ }
401
+ function createOrderRecordUnlocked(record) {
402
+ const { rozoPaymentId } = record;
403
+ const existing = readState(rozoPaymentId);
404
+ const next = {
405
+ version: 1,
406
+ rozoPaymentId,
407
+ // 'coinbase' (default, historical records) or 'bitrefill'. Drives which
408
+ // payability checks presend and status run.
409
+ provider: record.provider ?? existing?.provider ?? "coinbase",
410
+ bitrefill: mergeBitrefill(existing?.bitrefill, record.bitrefill),
411
+ linkId: record.linkId,
412
+ paymentLink: record.paymentLink ?? null,
413
+ merchant: record.merchant ?? null,
414
+ invoiceAmount: record.invoiceAmount ?? null,
415
+ source: record.source,
416
+ receiverAddress: record.receiverAddress,
417
+ receiverMemo: record.receiverMemo ?? null,
418
+ amount: record.amount,
419
+ amountUnit: record.amountUnit ?? null,
420
+ expiresAt: record.expiresAt ?? null,
421
+ createdAt: existing?.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
422
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
423
+ confirmation: existing?.confirmation ?? null,
424
+ send: existing?.send ?? null
425
+ };
426
+ writeAtomic(statePath(rozoPaymentId), next);
427
+ return next;
428
+ }
429
+ function depositDigest(source) {
430
+ const canonical = JSON.stringify({
431
+ chainId: String(source?.chainId ?? ""),
432
+ tokenSymbol: String(source?.tokenSymbol ?? "").toUpperCase(),
433
+ tokenAddress: String(source?.tokenAddress ?? ""),
434
+ receiverAddress: String(source?.receiverAddress ?? ""),
435
+ receiverMemo: source?.receiverMemo ?? null,
436
+ amount: String(source?.amount ?? ""),
437
+ amountUnit: source?.amountUnit ?? null,
438
+ lnInvoice: source?.lnInvoice ?? null
439
+ });
440
+ return crypto.createHash("sha256").update(canonical, "utf8").digest("hex");
441
+ }
442
+ function recordConfirmation(rozoPaymentId, { source, invoiceAmount, tier }) {
443
+ return withLock(() => {
444
+ const state = readState(rozoPaymentId);
445
+ if (!state) {
446
+ throw new SkillError("NO_ORDER_STATE", "Cannot confirm an order with no local record.");
447
+ }
448
+ const next = {
449
+ ...state,
450
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
451
+ confirmation: {
452
+ confirmedAt: (/* @__PURE__ */ new Date()).toISOString(),
453
+ depositDigest: depositDigest(source),
454
+ invoiceAmount: invoiceAmount ?? state.invoiceAmount ?? null,
455
+ tier: tier ?? null
456
+ }
457
+ };
458
+ writeAtomic(statePath(rozoPaymentId), next);
459
+ return next;
460
+ });
461
+ }
462
+
463
+ // scripts/src/lib/prefs.mjs
464
+ function prefsPath() {
465
+ const root = process.env.ROZO_CHECKOUT_STATE_DIR ? path2.dirname(stateRoot()) : path2.join(os2.homedir(), ".rozo-checkout");
466
+ return path2.join(root, "prefs.json");
467
+ }
468
+ var ALLOWED = ["lastPayerAddress", "lastAddressFamily", "lastPreset", "installId", "updatedAt"];
469
+ var INSTALL_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
470
+ var LOCK_WAIT_MS2 = 2e3;
471
+ var LOCK_STALE_MS2 = 1e4;
472
+ function sleepSync2(ms) {
473
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
474
+ }
475
+ function withPrefsLock(fn) {
476
+ const lock = `${prefsPath()}.lock`;
477
+ fs2.mkdirSync(path2.dirname(lock), { recursive: true, mode: 448 });
478
+ const deadline = Date.now() + LOCK_WAIT_MS2;
479
+ for (; ; ) {
480
+ try {
481
+ fs2.closeSync(fs2.openSync(lock, "wx", 384));
482
+ break;
483
+ } catch (err) {
484
+ if (err.code !== "EEXIST") throw err;
485
+ }
486
+ try {
487
+ if (Date.now() - fs2.statSync(lock).mtimeMs > LOCK_STALE_MS2) {
488
+ fs2.rmSync(lock, { force: true });
489
+ continue;
490
+ }
491
+ } catch {
492
+ continue;
493
+ }
494
+ if (Date.now() > deadline) return void 0;
495
+ sleepSync2(10);
496
+ }
497
+ try {
498
+ return fn();
499
+ } finally {
500
+ fs2.rmSync(lock, { force: true });
501
+ }
502
+ }
503
+ var USER_FIELDS = ["lastPayerAddress", "lastAddressFamily", "lastPreset"];
504
+ function readPrefs() {
505
+ let raw;
506
+ try {
507
+ raw = fs2.readFileSync(prefsPath(), "utf8");
508
+ } catch {
509
+ return null;
510
+ }
511
+ let doc;
512
+ try {
513
+ doc = JSON.parse(raw);
514
+ } catch {
515
+ return null;
516
+ }
517
+ if (!doc || typeof doc !== "object" || Array.isArray(doc)) return null;
518
+ const out = {};
519
+ for (const k of ALLOWED) {
520
+ const v = doc[k];
521
+ if (typeof v === "string" && v.trim()) out[k] = v.trim();
522
+ }
523
+ return Object.keys(out).length ? out : null;
524
+ }
525
+ function savePrefs(update, { installIdIfAbsent } = {}) {
526
+ try {
527
+ const saved = withPrefsLock(() => {
528
+ const existing = readPrefs() || {};
529
+ const next = { ...existing };
530
+ for (const k of USER_FIELDS) {
531
+ const v = update?.[k];
532
+ if (typeof v === "string" && v.trim()) next[k] = v.trim();
533
+ }
534
+ if (!INSTALL_ID_RE.test(String(existing.installId ?? "")) && INSTALL_ID_RE.test(String(installIdIfAbsent ?? ""))) {
535
+ next.installId = installIdIfAbsent;
536
+ }
537
+ next.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
538
+ writeAtomic(prefsPath(), next);
539
+ return next;
540
+ });
541
+ return saved ?? null;
542
+ } catch {
543
+ return null;
544
+ }
545
+ }
546
+
547
+ // scripts/src/lib/identity.mjs
548
+ var ACCOUNT_HASH_PREFIX = "rozo-acct-v1:";
549
+ function isValidInstallId(value) {
550
+ return typeof value === "string" && INSTALL_ID_RE.test(value);
551
+ }
552
+ function attributionDisabled(env = process.env) {
553
+ const v = String(env.ROZO_CHECKOUT_ANON_ID ?? "").trim().toLowerCase();
554
+ return ["off", "0", "false", "no"].includes(v);
555
+ }
556
+ function getOrCreateInstallId() {
557
+ const existing = readPrefs()?.installId;
558
+ if (isValidInstallId(existing)) return existing;
559
+ const saved = savePrefs({}, { installIdIfAbsent: crypto2.randomUUID().toLowerCase() });
560
+ if (isValidInstallId(saved?.installId)) return saved.installId;
561
+ const onDisk = readPrefs()?.installId;
562
+ return isValidInstallId(onDisk) ? onDisk : crypto2.randomUUID().toLowerCase();
563
+ }
564
+ function hashAccount(raw) {
565
+ return crypto2.createHash("sha256").update(`${ACCOUNT_HASH_PREFIX}${raw}`, "utf8").digest("hex");
566
+ }
567
+ function rawApiKey(env) {
568
+ const v = typeof env.OPENROUTER_API_KEY === "string" ? env.OPENROUTER_API_KEY.trim() : "";
569
+ return v && v.length <= 1024 ? v : null;
570
+ }
571
+ function attributionIdentity(env = process.env) {
572
+ if (attributionDisabled(env)) return {};
573
+ const out = {};
574
+ try {
575
+ const id = getOrCreateInstallId();
576
+ if (isValidInstallId(id)) out.install_id = id;
577
+ } catch {
578
+ }
579
+ const raw = rawApiKey(env);
580
+ if (raw) out.account_hash = hashAccount(raw);
581
+ return out;
582
+ }
583
+
280
584
  // scripts/src/lib/api.mjs
281
585
  var PKG_VERSION = (() => {
282
586
  const requireFrom = createRequire(import.meta.url);
@@ -292,10 +596,16 @@ var PKG_VERSION = (() => {
292
596
  var MPP_BASE = process.env.ROZO_CHECKOUT_MPP_BASE || "https://apiserver.mpprouter.dev/v1/services/rozo-agent-api";
293
597
  var CLIENT_LABEL = `rozo-checkout-cli/${PKG_VERSION}`;
294
598
  var ATTRIBUTION_CLIENT = `rozo-checkout-skill/${PKG_VERSION}`;
295
- function buildAttribution({ utmSource } = {}) {
599
+ function buildAttribution({ utmSource, identity } = {}) {
296
600
  const raw = utmSource !== void 0 ? utmSource : process.env.ROZO_CHECKOUT_UTM_SOURCE;
297
601
  const utm_source = normalizeUtmSource(raw);
298
- return { client: ATTRIBUTION_CLIENT, ...utm_source ? { utm_source } : {} };
602
+ const ids = identity ?? attributionIdentity();
603
+ return {
604
+ client: ATTRIBUTION_CLIENT,
605
+ ...utm_source ? { utm_source } : {},
606
+ ...ids.install_id ? { install_id: ids.install_id } : {},
607
+ ...ids.account_hash ? { account_hash: ids.account_hash } : {}
608
+ };
299
609
  }
300
610
  var INTENTS_BASE = process.env.ROZO_CHECKOUT_INTENTS_BASE || "https://intentapiv4.rozo.ai/functions/v1/payment-api";
301
611
  async function quoteInvoice({ url, linkId }) {
@@ -891,9 +1201,9 @@ function checkPayable(statusResponse, now = Date.now()) {
891
1201
  }
892
1202
 
893
1203
  // scripts/src/lib/blacklist.mjs
894
- import fs from "node:fs";
895
- import path from "node:path";
896
- import crypto from "node:crypto";
1204
+ import fs3 from "node:fs";
1205
+ import path3 from "node:path";
1206
+ import crypto3 from "node:crypto";
897
1207
  import { fileURLToPath } from "node:url";
898
1208
  var BlacklistError = class extends Error {
899
1209
  constructor(code, message) {
@@ -928,7 +1238,7 @@ function parseBlacklist(doc) {
928
1238
  }
929
1239
  addresses.push(e.address);
930
1240
  }
931
- const digest = crypto.createHash("sha256").update(JSON.stringify(addresses), "utf8").digest("hex");
1241
+ const digest = crypto3.createHash("sha256").update(JSON.stringify(addresses), "utf8").digest("hex");
932
1242
  if (typeof provenance.addressesSha256 !== "string" || !provenance.addressesSha256) {
933
1243
  throw new BlacklistError("BLACKLIST_UNAVAILABLE", "Blacklist provenance digest is missing.");
934
1244
  }
@@ -951,11 +1261,11 @@ function parseBlacklist(doc) {
951
1261
  return { entries, index, provenance, digest };
952
1262
  }
953
1263
  function candidatePaths(moduleUrl) {
954
- const here = path.dirname(fileURLToPath(moduleUrl));
1264
+ const here = path3.dirname(fileURLToPath(moduleUrl));
955
1265
  return [
956
- path.join(here, "blacklist.json"),
957
- path.join(here, "..", "src", "lib", "blacklist.json"),
958
- path.join(here, "..", "..", "src", "lib", "blacklist.json")
1266
+ path3.join(here, "blacklist.json"),
1267
+ path3.join(here, "..", "src", "lib", "blacklist.json"),
1268
+ path3.join(here, "..", "..", "src", "lib", "blacklist.json")
959
1269
  ];
960
1270
  }
961
1271
  var cached = null;
@@ -966,7 +1276,7 @@ function loadBlacklist(explicitPath) {
966
1276
  for (const p of paths) {
967
1277
  let raw;
968
1278
  try {
969
- raw = fs.readFileSync(p, "utf8");
1279
+ raw = fs3.readFileSync(p, "utf8");
970
1280
  } catch (err) {
971
1281
  lastErr = err;
972
1282
  continue;
@@ -1013,181 +1323,6 @@ function assertNotBlacklisted(targets, blacklist) {
1013
1323
  return true;
1014
1324
  }
1015
1325
 
1016
- // scripts/src/lib/state.mjs
1017
- import fs2 from "node:fs";
1018
- import path2 from "node:path";
1019
- import os from "node:os";
1020
- import crypto2 from "node:crypto";
1021
- var LOCK_STALE_MS = 6e4;
1022
- var LOCK_WAIT_MS = 1e4;
1023
- var LOCK_POLL_MS = 25;
1024
- function lockPath() {
1025
- return path2.join(stateRoot(), ".send.lock");
1026
- }
1027
- function sleepSync(ms) {
1028
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
1029
- }
1030
- function tryAcquire(file) {
1031
- try {
1032
- const fd = fs2.openSync(file, "wx", 384);
1033
- fs2.writeFileSync(fd, JSON.stringify({ pid: process.pid, at: (/* @__PURE__ */ new Date()).toISOString() }));
1034
- fs2.closeSync(fd);
1035
- return true;
1036
- } catch (err) {
1037
- if (err.code !== "EEXIST") throw err;
1038
- return false;
1039
- }
1040
- }
1041
- function withLock(fn) {
1042
- const file = lockPath();
1043
- fs2.mkdirSync(path2.dirname(file), { recursive: true, mode: 448 });
1044
- const deadline = Date.now() + LOCK_WAIT_MS;
1045
- for (; ; ) {
1046
- if (tryAcquire(file)) break;
1047
- try {
1048
- const age = Date.now() - fs2.statSync(file).mtimeMs;
1049
- if (age > LOCK_STALE_MS) {
1050
- const stolen = `${file}.stale.${crypto2.randomBytes(4).toString("hex")}`;
1051
- try {
1052
- fs2.renameSync(file, stolen);
1053
- fs2.unlinkSync(stolen);
1054
- } catch {
1055
- }
1056
- continue;
1057
- }
1058
- } catch (err) {
1059
- if (err.code !== "ENOENT") throw err;
1060
- continue;
1061
- }
1062
- if (Date.now() > deadline) {
1063
- throw new SkillError(
1064
- "LOCK_TIMEOUT",
1065
- "Another rozo-checkout process is holding the send lock. Refusing to proceed rather than risk a concurrent second send."
1066
- );
1067
- }
1068
- sleepSync(LOCK_POLL_MS);
1069
- }
1070
- try {
1071
- return fn();
1072
- } finally {
1073
- try {
1074
- fs2.unlinkSync(file);
1075
- } catch {
1076
- }
1077
- }
1078
- }
1079
- function stateRoot() {
1080
- return process.env.ROZO_CHECKOUT_STATE_DIR || path2.join(os.homedir(), ".rozo-checkout", "state");
1081
- }
1082
- function statePath(rozoPaymentId) {
1083
- if (!/^[A-Za-z0-9-]{8,64}$/.test(String(rozoPaymentId || ""))) {
1084
- throw new SkillError("BAD_ROZO_PAYMENT_ID", "Refusing to build a state path from that id.");
1085
- }
1086
- return path2.join(stateRoot(), `${rozoPaymentId}.json`);
1087
- }
1088
- function writeAtomic(file, data) {
1089
- const dir = path2.dirname(file);
1090
- fs2.mkdirSync(dir, { recursive: true, mode: 448 });
1091
- const tmp = path2.join(dir, `.${path2.basename(file)}.${crypto2.randomBytes(6).toString("hex")}.tmp`);
1092
- const fd = fs2.openSync(tmp, "wx", 384);
1093
- try {
1094
- fs2.writeFileSync(fd, JSON.stringify(data, null, 2) + "\n", "utf8");
1095
- fs2.fsyncSync(fd);
1096
- } finally {
1097
- fs2.closeSync(fd);
1098
- }
1099
- fs2.renameSync(tmp, file);
1100
- }
1101
- function readState(rozoPaymentId) {
1102
- const file = statePath(rozoPaymentId);
1103
- let raw;
1104
- try {
1105
- raw = fs2.readFileSync(file, "utf8");
1106
- } catch (err) {
1107
- if (err.code === "ENOENT") return null;
1108
- throw new SkillError("STATE_UNREADABLE", `Cannot read local state: ${err.code}`);
1109
- }
1110
- try {
1111
- return JSON.parse(raw);
1112
- } catch {
1113
- throw new SkillError(
1114
- "STATE_CORRUPT",
1115
- "The local state file for this order is corrupt. Refusing to act; inspect it manually."
1116
- );
1117
- }
1118
- }
1119
- function createOrderRecord(record) {
1120
- return withLock(() => createOrderRecordUnlocked(record));
1121
- }
1122
- function mergeBitrefill(prev, next) {
1123
- if (!next) return prev ?? null;
1124
- if (!prev?.expiresAt || !next.expiresAt) return { ...next, expiresAt: next.expiresAt ?? prev?.expiresAt ?? null };
1125
- const a = Date.parse(prev.expiresAt);
1126
- const b = Date.parse(next.expiresAt);
1127
- return { ...next, expiresAt: Number.isFinite(a) && (!Number.isFinite(b) || a < b) ? prev.expiresAt : next.expiresAt };
1128
- }
1129
- function createOrderRecordUnlocked(record) {
1130
- const { rozoPaymentId } = record;
1131
- const existing = readState(rozoPaymentId);
1132
- const next = {
1133
- version: 1,
1134
- rozoPaymentId,
1135
- // 'coinbase' (default, historical records) or 'bitrefill'. Drives which
1136
- // payability checks presend and status run.
1137
- provider: record.provider ?? existing?.provider ?? "coinbase",
1138
- bitrefill: mergeBitrefill(existing?.bitrefill, record.bitrefill),
1139
- linkId: record.linkId,
1140
- paymentLink: record.paymentLink ?? null,
1141
- merchant: record.merchant ?? null,
1142
- invoiceAmount: record.invoiceAmount ?? null,
1143
- source: record.source,
1144
- receiverAddress: record.receiverAddress,
1145
- receiverMemo: record.receiverMemo ?? null,
1146
- amount: record.amount,
1147
- amountUnit: record.amountUnit ?? null,
1148
- expiresAt: record.expiresAt ?? null,
1149
- createdAt: existing?.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
1150
- updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1151
- confirmation: existing?.confirmation ?? null,
1152
- send: existing?.send ?? null
1153
- };
1154
- writeAtomic(statePath(rozoPaymentId), next);
1155
- return next;
1156
- }
1157
- function depositDigest(source) {
1158
- const canonical = JSON.stringify({
1159
- chainId: String(source?.chainId ?? ""),
1160
- tokenSymbol: String(source?.tokenSymbol ?? "").toUpperCase(),
1161
- tokenAddress: String(source?.tokenAddress ?? ""),
1162
- receiverAddress: String(source?.receiverAddress ?? ""),
1163
- receiverMemo: source?.receiverMemo ?? null,
1164
- amount: String(source?.amount ?? ""),
1165
- amountUnit: source?.amountUnit ?? null,
1166
- lnInvoice: source?.lnInvoice ?? null
1167
- });
1168
- return crypto2.createHash("sha256").update(canonical, "utf8").digest("hex");
1169
- }
1170
- function recordConfirmation(rozoPaymentId, { source, invoiceAmount, tier }) {
1171
- return withLock(() => {
1172
- const state = readState(rozoPaymentId);
1173
- if (!state) {
1174
- throw new SkillError("NO_ORDER_STATE", "Cannot confirm an order with no local record.");
1175
- }
1176
- const next = {
1177
- ...state,
1178
- updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1179
- confirmation: {
1180
- confirmedAt: (/* @__PURE__ */ new Date()).toISOString(),
1181
- depositDigest: depositDigest(source),
1182
- invoiceAmount: invoiceAmount ?? state.invoiceAmount ?? null,
1183
- tier: tier ?? null
1184
- }
1185
- };
1186
- writeAtomic(statePath(rozoPaymentId), next);
1187
- return next;
1188
- });
1189
- }
1190
-
1191
1326
  // scripts/src/create-order.mjs
1192
1327
  function confirmTier(usdAmount) {
1193
1328
  const usd = Number(usdAmount);
@@ -206,6 +206,57 @@ function getJson(url, opts) {
206
206
  return request("GET", url, opts);
207
207
  }
208
208
 
209
+ // scripts/src/lib/state.mjs
210
+ import fs from "node:fs";
211
+ import path from "node:path";
212
+ import os from "node:os";
213
+ function stateRoot() {
214
+ return process.env.ROZO_CHECKOUT_STATE_DIR || path.join(os.homedir(), ".rozo-checkout", "state");
215
+ }
216
+ function statePath(rozoPaymentId) {
217
+ if (!/^[A-Za-z0-9-]{8,64}$/.test(String(rozoPaymentId || ""))) {
218
+ throw new SkillError("BAD_ROZO_PAYMENT_ID", "Refusing to build a state path from that id.");
219
+ }
220
+ return path.join(stateRoot(), `${rozoPaymentId}.json`);
221
+ }
222
+ function readState(rozoPaymentId) {
223
+ const file = statePath(rozoPaymentId);
224
+ let raw;
225
+ try {
226
+ raw = fs.readFileSync(file, "utf8");
227
+ } catch (err) {
228
+ if (err.code === "ENOENT") return null;
229
+ throw new SkillError("STATE_UNREADABLE", `Cannot read local state: ${err.code}`);
230
+ }
231
+ try {
232
+ return JSON.parse(raw);
233
+ } catch {
234
+ throw new SkillError(
235
+ "STATE_CORRUPT",
236
+ "The local state file for this order is corrupt. Refusing to act; inspect it manually."
237
+ );
238
+ }
239
+ }
240
+ function findByLinkId(linkId) {
241
+ const dir = stateRoot();
242
+ let files = [];
243
+ try {
244
+ files = fs.readdirSync(dir).filter((f) => f.endsWith(".json"));
245
+ } catch {
246
+ return null;
247
+ }
248
+ let best = null;
249
+ for (const f of files) {
250
+ try {
251
+ const s = JSON.parse(fs.readFileSync(path.join(dir, f), "utf8"));
252
+ if (s?.linkId !== linkId) continue;
253
+ if (!best || String(s.createdAt) > String(best.createdAt)) best = s;
254
+ } catch {
255
+ }
256
+ }
257
+ return best;
258
+ }
259
+
209
260
  // scripts/src/lib/api.mjs
210
261
  var PKG_VERSION = (() => {
211
262
  const requireFrom = createRequire(import.meta.url);
@@ -677,57 +728,6 @@ function checkExpiry({
677
728
  return { ...withDeadline, ok: true, code: null, reason: null };
678
729
  }
679
730
 
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
731
  // scripts/src/lib/bitrefill.mjs
732
732
  var PROVIDER_BITREFILL = "bitrefill";
733
733
  var BITREFILL_DESTINATION = Object.freeze({ chainId: "8453", tokenSymbol: "USDC" });
@@ -1017,7 +1017,7 @@ function sendWindowFor({ provider, chainId, payment, status, bitrefillExpiry, no
1017
1017
  intentExpiresAt: payment?.expiresAt ?? deadline,
1018
1018
  coinbaseExpiry: deadline
1019
1019
  });
1020
- return expiry2.ok ? { ok: true, code: null, minutesOfSlack: Math.floor(expiry2.msOfSlack / 6e4) } : { ok: false, code: expiry2.code, reason: expiry2.reason };
1020
+ 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
1021
  }
1022
1022
  if (!status) {
1023
1023
  return { ok: false, code: "LINK_PAYABILITY_UNKNOWN", reason: "The Coinbase link state could not be read." };
@@ -1030,7 +1030,7 @@ function sendWindowFor({ provider, chainId, payment, status, bitrefillExpiry, no
1030
1030
  intentExpiresAt: payment?.expiresAt ?? status?.rozoPayment?.expiresAt,
1031
1031
  coinbaseExpiry: status?.coinbase?.preApprovalExpiry
1032
1032
  });
1033
- return expiry.ok ? { ok: true, code: null, minutesOfSlack: Math.floor(expiry.msOfSlack / 6e4) } : { ok: false, code: expiry.code, reason: expiry.reason };
1033
+ 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
1034
  }
1035
1035
  async function snapshot({ rozoPaymentId, linkId, provider: explicitProvider }) {
1036
1036
  let provider = resolveProvider(explicitProvider, rozoPaymentId);