@rozoai/checkout 0.1.15 → 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.
@@ -66,7 +66,16 @@ function redact(text) {
66
66
  );
67
67
  return s;
68
68
  }
69
+ var PublicHash = class {
70
+ constructor(value) {
71
+ this.value = value;
72
+ }
73
+ toJSON() {
74
+ return this.value;
75
+ }
76
+ };
69
77
  function redactDeep(value) {
78
+ if (value instanceof PublicHash) return value.value;
70
79
  if (typeof value === "string") return redact(value);
71
80
  if (Array.isArray(value)) return value.map(redactDeep);
72
81
  if (value && typeof value === "object") {
@@ -268,6 +277,310 @@ function normalizeUtmSource(value) {
268
277
  return s;
269
278
  }
270
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
+
271
584
  // scripts/src/lib/api.mjs
272
585
  var PKG_VERSION = (() => {
273
586
  const requireFrom = createRequire(import.meta.url);
@@ -283,10 +596,16 @@ var PKG_VERSION = (() => {
283
596
  var MPP_BASE = process.env.ROZO_CHECKOUT_MPP_BASE || "https://apiserver.mpprouter.dev/v1/services/rozo-agent-api";
284
597
  var CLIENT_LABEL = `rozo-checkout-cli/${PKG_VERSION}`;
285
598
  var ATTRIBUTION_CLIENT = `rozo-checkout-skill/${PKG_VERSION}`;
286
- function buildAttribution({ utmSource } = {}) {
599
+ function buildAttribution({ utmSource, identity } = {}) {
287
600
  const raw = utmSource !== void 0 ? utmSource : process.env.ROZO_CHECKOUT_UTM_SOURCE;
288
601
  const utm_source = normalizeUtmSource(raw);
289
- 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
+ };
290
609
  }
291
610
  var INTENTS_BASE = process.env.ROZO_CHECKOUT_INTENTS_BASE || "https://intentapiv4.rozo.ai/functions/v1/payment-api";
292
611
  async function quoteInvoice({ url, linkId }) {
@@ -882,9 +1201,9 @@ function checkPayable(statusResponse, now = Date.now()) {
882
1201
  }
883
1202
 
884
1203
  // scripts/src/lib/blacklist.mjs
885
- import fs from "node:fs";
886
- import path from "node:path";
887
- import crypto from "node:crypto";
1204
+ import fs3 from "node:fs";
1205
+ import path3 from "node:path";
1206
+ import crypto3 from "node:crypto";
888
1207
  import { fileURLToPath } from "node:url";
889
1208
  var BlacklistError = class extends Error {
890
1209
  constructor(code, message) {
@@ -919,7 +1238,7 @@ function parseBlacklist(doc) {
919
1238
  }
920
1239
  addresses.push(e.address);
921
1240
  }
922
- 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");
923
1242
  if (typeof provenance.addressesSha256 !== "string" || !provenance.addressesSha256) {
924
1243
  throw new BlacklistError("BLACKLIST_UNAVAILABLE", "Blacklist provenance digest is missing.");
925
1244
  }
@@ -942,11 +1261,11 @@ function parseBlacklist(doc) {
942
1261
  return { entries, index, provenance, digest };
943
1262
  }
944
1263
  function candidatePaths(moduleUrl) {
945
- const here = path.dirname(fileURLToPath(moduleUrl));
1264
+ const here = path3.dirname(fileURLToPath(moduleUrl));
946
1265
  return [
947
- path.join(here, "blacklist.json"),
948
- path.join(here, "..", "src", "lib", "blacklist.json"),
949
- 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")
950
1269
  ];
951
1270
  }
952
1271
  var cached = null;
@@ -957,7 +1276,7 @@ function loadBlacklist(explicitPath) {
957
1276
  for (const p of paths) {
958
1277
  let raw;
959
1278
  try {
960
- raw = fs.readFileSync(p, "utf8");
1279
+ raw = fs3.readFileSync(p, "utf8");
961
1280
  } catch (err) {
962
1281
  lastErr = err;
963
1282
  continue;
@@ -1004,181 +1323,6 @@ function assertNotBlacklisted(targets, blacklist) {
1004
1323
  return true;
1005
1324
  }
1006
1325
 
1007
- // scripts/src/lib/state.mjs
1008
- import fs2 from "node:fs";
1009
- import path2 from "node:path";
1010
- import os from "node:os";
1011
- import crypto2 from "node:crypto";
1012
- var LOCK_STALE_MS = 6e4;
1013
- var LOCK_WAIT_MS = 1e4;
1014
- var LOCK_POLL_MS = 25;
1015
- function lockPath() {
1016
- return path2.join(stateRoot(), ".send.lock");
1017
- }
1018
- function sleepSync(ms) {
1019
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
1020
- }
1021
- function tryAcquire(file) {
1022
- try {
1023
- const fd = fs2.openSync(file, "wx", 384);
1024
- fs2.writeFileSync(fd, JSON.stringify({ pid: process.pid, at: (/* @__PURE__ */ new Date()).toISOString() }));
1025
- fs2.closeSync(fd);
1026
- return true;
1027
- } catch (err) {
1028
- if (err.code !== "EEXIST") throw err;
1029
- return false;
1030
- }
1031
- }
1032
- function withLock(fn) {
1033
- const file = lockPath();
1034
- fs2.mkdirSync(path2.dirname(file), { recursive: true, mode: 448 });
1035
- const deadline = Date.now() + LOCK_WAIT_MS;
1036
- for (; ; ) {
1037
- if (tryAcquire(file)) break;
1038
- try {
1039
- const age = Date.now() - fs2.statSync(file).mtimeMs;
1040
- if (age > LOCK_STALE_MS) {
1041
- const stolen = `${file}.stale.${crypto2.randomBytes(4).toString("hex")}`;
1042
- try {
1043
- fs2.renameSync(file, stolen);
1044
- fs2.unlinkSync(stolen);
1045
- } catch {
1046
- }
1047
- continue;
1048
- }
1049
- } catch (err) {
1050
- if (err.code !== "ENOENT") throw err;
1051
- continue;
1052
- }
1053
- if (Date.now() > deadline) {
1054
- throw new SkillError(
1055
- "LOCK_TIMEOUT",
1056
- "Another rozo-checkout process is holding the send lock. Refusing to proceed rather than risk a concurrent second send."
1057
- );
1058
- }
1059
- sleepSync(LOCK_POLL_MS);
1060
- }
1061
- try {
1062
- return fn();
1063
- } finally {
1064
- try {
1065
- fs2.unlinkSync(file);
1066
- } catch {
1067
- }
1068
- }
1069
- }
1070
- function stateRoot() {
1071
- return process.env.ROZO_CHECKOUT_STATE_DIR || path2.join(os.homedir(), ".rozo-checkout", "state");
1072
- }
1073
- function statePath(rozoPaymentId) {
1074
- if (!/^[A-Za-z0-9-]{8,64}$/.test(String(rozoPaymentId || ""))) {
1075
- throw new SkillError("BAD_ROZO_PAYMENT_ID", "Refusing to build a state path from that id.");
1076
- }
1077
- return path2.join(stateRoot(), `${rozoPaymentId}.json`);
1078
- }
1079
- function writeAtomic(file, data) {
1080
- const dir = path2.dirname(file);
1081
- fs2.mkdirSync(dir, { recursive: true, mode: 448 });
1082
- const tmp = path2.join(dir, `.${path2.basename(file)}.${crypto2.randomBytes(6).toString("hex")}.tmp`);
1083
- const fd = fs2.openSync(tmp, "wx", 384);
1084
- try {
1085
- fs2.writeFileSync(fd, JSON.stringify(data, null, 2) + "\n", "utf8");
1086
- fs2.fsyncSync(fd);
1087
- } finally {
1088
- fs2.closeSync(fd);
1089
- }
1090
- fs2.renameSync(tmp, file);
1091
- }
1092
- function readState(rozoPaymentId) {
1093
- const file = statePath(rozoPaymentId);
1094
- let raw;
1095
- try {
1096
- raw = fs2.readFileSync(file, "utf8");
1097
- } catch (err) {
1098
- if (err.code === "ENOENT") return null;
1099
- throw new SkillError("STATE_UNREADABLE", `Cannot read local state: ${err.code}`);
1100
- }
1101
- try {
1102
- return JSON.parse(raw);
1103
- } catch {
1104
- throw new SkillError(
1105
- "STATE_CORRUPT",
1106
- "The local state file for this order is corrupt. Refusing to act; inspect it manually."
1107
- );
1108
- }
1109
- }
1110
- function createOrderRecord(record) {
1111
- return withLock(() => createOrderRecordUnlocked(record));
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
- }
1120
- function createOrderRecordUnlocked(record) {
1121
- const { rozoPaymentId } = record;
1122
- const existing = readState(rozoPaymentId);
1123
- const next = {
1124
- version: 1,
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),
1130
- linkId: record.linkId,
1131
- paymentLink: record.paymentLink ?? null,
1132
- merchant: record.merchant ?? null,
1133
- invoiceAmount: record.invoiceAmount ?? null,
1134
- source: record.source,
1135
- receiverAddress: record.receiverAddress,
1136
- receiverMemo: record.receiverMemo ?? null,
1137
- amount: record.amount,
1138
- amountUnit: record.amountUnit ?? null,
1139
- expiresAt: record.expiresAt ?? null,
1140
- createdAt: existing?.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
1141
- updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1142
- confirmation: existing?.confirmation ?? null,
1143
- send: existing?.send ?? null
1144
- };
1145
- writeAtomic(statePath(rozoPaymentId), next);
1146
- return next;
1147
- }
1148
- function depositDigest(source) {
1149
- const canonical = JSON.stringify({
1150
- chainId: String(source?.chainId ?? ""),
1151
- tokenSymbol: String(source?.tokenSymbol ?? "").toUpperCase(),
1152
- tokenAddress: String(source?.tokenAddress ?? ""),
1153
- receiverAddress: String(source?.receiverAddress ?? ""),
1154
- receiverMemo: source?.receiverMemo ?? null,
1155
- amount: String(source?.amount ?? ""),
1156
- amountUnit: source?.amountUnit ?? null,
1157
- lnInvoice: source?.lnInvoice ?? null
1158
- });
1159
- return crypto2.createHash("sha256").update(canonical, "utf8").digest("hex");
1160
- }
1161
- function recordConfirmation(rozoPaymentId, { source, invoiceAmount, tier }) {
1162
- return withLock(() => {
1163
- const state = readState(rozoPaymentId);
1164
- if (!state) {
1165
- throw new SkillError("NO_ORDER_STATE", "Cannot confirm an order with no local record.");
1166
- }
1167
- const next = {
1168
- ...state,
1169
- updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1170
- confirmation: {
1171
- confirmedAt: (/* @__PURE__ */ new Date()).toISOString(),
1172
- depositDigest: depositDigest(source),
1173
- invoiceAmount: invoiceAmount ?? state.invoiceAmount ?? null,
1174
- tier: tier ?? null
1175
- }
1176
- };
1177
- writeAtomic(statePath(rozoPaymentId), next);
1178
- return next;
1179
- });
1180
- }
1181
-
1182
1326
  // scripts/src/create-order.mjs
1183
1327
  function confirmTier(usdAmount) {
1184
1328
  const usd = Number(usdAmount);
@@ -1256,6 +1400,17 @@ async function main(argv) {
1256
1400
  expiresAt: created.expiresAt ?? null,
1257
1401
  expiresIn: Number.isFinite(remainingMs) ? formatRemaining(remainingMs) : null
1258
1402
  },
1403
+ // Machine-readable form of the guidance below. The live order keeps
1404
+ // its own coin; a different coin is only possible once it has
1405
+ // expired unfunded (then pay on the same link creates a new order).
1406
+ nextAction: {
1407
+ type: "choose_method",
1408
+ canRetryQuery: true,
1409
+ canCreateOrder: false,
1410
+ canSend: false,
1411
+ payExistingWith: { chainId: existing.chainId ?? null, tokenSymbol: existing.tokenSymbol ?? null },
1412
+ switchPossibleAfter: created.expiresAt ?? null
1413
+ },
1259
1414
  guidance: `Either pay the existing order with ${existing.tokenSymbol ?? "its own coin"} (re-run with --chain ${existing.chainId} --token ${existing.tokenSymbol}), or wait ${Number.isFinite(remainingMs) ? formatRemaining(remainingMs) : "for it to expire"} for it to expire and then create a new one. Unpaid orders cost nothing.`
1260
1415
  },
1261
1416
  EXIT_ERROR
@@ -40,7 +40,16 @@ function redact(text) {
40
40
  );
41
41
  return s;
42
42
  }
43
+ var PublicHash = class {
44
+ constructor(value) {
45
+ this.value = value;
46
+ }
47
+ toJSON() {
48
+ return this.value;
49
+ }
50
+ };
43
51
  function redactDeep(value) {
52
+ if (value instanceof PublicHash) return value.value;
44
53
  if (typeof value === "string") return redact(value);
45
54
  if (Array.isArray(value)) return value.map(redactDeep);
46
55
  if (value && typeof value === "object") {