@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.
- package/README.md +36 -0
- package/package.json +1 -1
- package/scripts/dist/cli.js +503 -83
- package/scripts/dist/create-order.js +321 -186
- package/scripts/dist/receipt.js +53 -53
- package/scripts/dist/resume.js +1486 -0
- package/scripts/dist/send-evm.js +202 -202
- package/scripts/dist/send-sol.js +202 -202
- package/scripts/dist/status.js +53 -53
|
@@ -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
|
-
|
|
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
|
|
895
|
-
import
|
|
896
|
-
import
|
|
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 =
|
|
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 =
|
|
1264
|
+
const here = path3.dirname(fileURLToPath(moduleUrl));
|
|
955
1265
|
return [
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
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 =
|
|
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);
|
package/scripts/dist/receipt.js
CHANGED
|
@@ -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);
|