@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.
- package/README.md +63 -1
- package/package.json +1 -1
- package/scripts/dist/cli.js +1267 -126
- package/scripts/dist/create-order.js +341 -186
- package/scripts/dist/quote.js +9 -0
- package/scripts/dist/receipt.js +1195 -0
- package/scripts/dist/resume.js +1486 -0
- package/scripts/dist/send-evm.js +502 -215
- package/scripts/dist/send-sol.js +324 -229
- package/scripts/dist/status.js +405 -49
|
@@ -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
|
-
|
|
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
|
|
886
|
-
import
|
|
887
|
-
import
|
|
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 =
|
|
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 =
|
|
1264
|
+
const here = path3.dirname(fileURLToPath(moduleUrl));
|
|
946
1265
|
return [
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
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 =
|
|
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
|
package/scripts/dist/quote.js
CHANGED
|
@@ -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") {
|