@rozoai/checkout 0.1.16 → 0.1.18
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 +51 -2
- package/llms.txt +5 -2
- package/package.json +1 -1
- package/scripts/dist/cli.js +655 -123
- package/scripts/dist/create-order.js +423 -217
- package/scripts/dist/quote.js +99 -33
- package/scripts/dist/receipt.js +149 -83
- package/scripts/dist/resume.js +1552 -0
- package/scripts/dist/send-evm.js +298 -232
- package/scripts/dist/send-sol.js +298 -232
- package/scripts/dist/status.js +168 -85
|
@@ -13,6 +13,8 @@ var SUPPORT = Object.freeze({
|
|
|
13
13
|
discord: "https://discord.gg/EfWejgTbuU"
|
|
14
14
|
});
|
|
15
15
|
var SUPPORT_TEXT = `Need help? Email ${SUPPORT.email}, or reach ROZO on X ${SUPPORT.x} or Discord ${SUPPORT.discord}.`;
|
|
16
|
+
var BULK_DOCS_URL = "https://docs.rozo.ai/products/checkout/bulk-and-agents";
|
|
17
|
+
var BULK_HINT_TEXT = `Buying regularly or for others? ${BULK_DOCS_URL}`;
|
|
16
18
|
var CONTACT_EMAIL_MAX_LENGTH = 254;
|
|
17
19
|
var EMAIL_RE = /^[a-z0-9][^\s@]*@[^\s@.]+(\.[^\s@.]+)+$/;
|
|
18
20
|
function normalizeContactEmail(raw) {
|
|
@@ -198,18 +200,66 @@ function maskMemo(memo) {
|
|
|
198
200
|
return `${s.slice(0, 4)}...${s.slice(-3)}`;
|
|
199
201
|
}
|
|
200
202
|
|
|
201
|
-
// scripts/src/lib/
|
|
203
|
+
// scripts/src/lib/version.mjs
|
|
202
204
|
import { createRequire } from "node:module";
|
|
205
|
+
var PKG_VERSION = (() => {
|
|
206
|
+
const requireFrom = createRequire(import.meta.url);
|
|
207
|
+
for (const candidate of ["../../package.json", "../../../package.json"]) {
|
|
208
|
+
try {
|
|
209
|
+
const pkg = requireFrom(candidate);
|
|
210
|
+
if (pkg?.name === "@rozoai/checkout" && pkg.version) return pkg.version;
|
|
211
|
+
} catch {
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return "0.0.0";
|
|
215
|
+
})();
|
|
203
216
|
|
|
204
217
|
// scripts/src/lib/http.mjs
|
|
205
218
|
var DEFAULT_TIMEOUT_MS = 2e4;
|
|
206
|
-
var USER_AGENT =
|
|
207
|
-
|
|
219
|
+
var USER_AGENT = `rozo-checkout-skill/${PKG_VERSION}`;
|
|
220
|
+
var MAX_RETRIES = 3;
|
|
221
|
+
var MAX_TOTAL_WAIT_MS = 12e4;
|
|
222
|
+
var FALLBACK_BACKOFF_MS = [2e3, 4e3, 8e3];
|
|
223
|
+
var defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
224
|
+
function parseRetryAfter(value, now = Date.now()) {
|
|
225
|
+
if (value === null || value === void 0) return null;
|
|
226
|
+
const s = String(value).trim();
|
|
227
|
+
if (!s) return null;
|
|
228
|
+
if (/^\d+(\.\d+)?$/.test(s)) return Math.round(Number(s) * 1e3);
|
|
229
|
+
const at = Date.parse(s);
|
|
230
|
+
if (!Number.isFinite(at)) return null;
|
|
231
|
+
return Math.max(0, at - now);
|
|
232
|
+
}
|
|
233
|
+
function bodyRetryAfterMs(json) {
|
|
234
|
+
const raw = json?.retryAfterSeconds ?? json?.error?.retryAfterSeconds;
|
|
235
|
+
if (raw === null || raw === void 0 || raw === "") return null;
|
|
236
|
+
const n = Number(raw);
|
|
237
|
+
return Number.isFinite(n) && n >= 0 ? Math.round(n * 1e3) : null;
|
|
238
|
+
}
|
|
239
|
+
function rateLimitInfo(res) {
|
|
240
|
+
const pick = (name) => res.headers?.get?.(name) ?? null;
|
|
241
|
+
const info = {
|
|
242
|
+
limit: pick("x-ratelimit-limit"),
|
|
243
|
+
remaining: pick("x-ratelimit-remaining"),
|
|
244
|
+
tier: pick("x-ratelimit-tier"),
|
|
245
|
+
scope: pick("x-ratelimit-scope")
|
|
246
|
+
};
|
|
247
|
+
const present = Object.fromEntries(Object.entries(info).filter(([, v]) => v !== null));
|
|
248
|
+
return Object.keys(present).length ? present : null;
|
|
249
|
+
}
|
|
250
|
+
function parseBody(text) {
|
|
251
|
+
if (!text) return null;
|
|
252
|
+
try {
|
|
253
|
+
return JSON.parse(text);
|
|
254
|
+
} catch {
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
async function fetchOnce(method, url, body, timeoutMs) {
|
|
208
259
|
const controller = new AbortController();
|
|
209
260
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
210
|
-
let res;
|
|
211
261
|
try {
|
|
212
|
-
res = await fetch(url, {
|
|
262
|
+
const res = await fetch(url, {
|
|
213
263
|
method,
|
|
214
264
|
headers: {
|
|
215
265
|
accept: "application/json",
|
|
@@ -219,6 +269,8 @@ async function request(method, url, { body, timeoutMs = DEFAULT_TIMEOUT_MS } = {
|
|
|
219
269
|
body: body !== void 0 ? JSON.stringify(body) : void 0,
|
|
220
270
|
signal: controller.signal
|
|
221
271
|
});
|
|
272
|
+
const text = await res.text();
|
|
273
|
+
return { res, text };
|
|
222
274
|
} catch (err) {
|
|
223
275
|
throw new SkillError(
|
|
224
276
|
err?.name === "AbortError" ? "HTTP_TIMEOUT" : "HTTP_UNREACHABLE",
|
|
@@ -228,31 +280,54 @@ async function request(method, url, { body, timeoutMs = DEFAULT_TIMEOUT_MS } = {
|
|
|
228
280
|
} finally {
|
|
229
281
|
clearTimeout(timer);
|
|
230
282
|
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
283
|
+
}
|
|
284
|
+
async function request(method, url, {
|
|
285
|
+
body,
|
|
286
|
+
timeoutMs = DEFAULT_TIMEOUT_MS,
|
|
287
|
+
sleep = defaultSleep,
|
|
288
|
+
now = Date.now,
|
|
289
|
+
maxTotalWaitMs = MAX_TOTAL_WAIT_MS
|
|
290
|
+
} = {}) {
|
|
291
|
+
let waitedMs = 0;
|
|
292
|
+
let retries = 0;
|
|
293
|
+
for (; ; ) {
|
|
294
|
+
const { res, text } = await fetchOnce(method, url, body, timeoutMs);
|
|
295
|
+
const json = parseBody(text);
|
|
296
|
+
if (res.ok) {
|
|
297
|
+
if (json === null) {
|
|
298
|
+
throw new SkillError("HTTP_BAD_JSON", "Endpoint returned a non-JSON body.", {
|
|
299
|
+
url: redactUrl(url),
|
|
300
|
+
snippet: redact(text).slice(0, 400)
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
return json;
|
|
304
|
+
}
|
|
305
|
+
const headerWaitMs = parseRetryAfter(res.headers?.get?.("retry-after"), now());
|
|
306
|
+
const hintMs = headerWaitMs ?? bodyRetryAfterMs(json);
|
|
307
|
+
const retryable = res.status === 429 || res.status === 503 && hintMs !== null;
|
|
308
|
+
if (retryable && retries < MAX_RETRIES) {
|
|
309
|
+
const waitMs = hintMs ?? FALLBACK_BACKOFF_MS[retries];
|
|
310
|
+
if (waitedMs + waitMs <= Math.min(maxTotalWaitMs, MAX_TOTAL_WAIT_MS)) {
|
|
311
|
+
retries += 1;
|
|
312
|
+
waitedMs += waitMs;
|
|
313
|
+
await sleep(waitMs);
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
238
316
|
}
|
|
239
|
-
}
|
|
240
|
-
if (!res.ok) {
|
|
241
317
|
const code = json?.code || json?.error?.code || (typeof json?.error === "string" && /^[A-Z][A-Z0-9_]+$/.test(json.error) ? json.error : null) || `HTTP_${res.status}`;
|
|
242
318
|
const message = json?.message || (typeof json?.error === "string" ? json.error : json?.error?.message) || `HTTP ${res.status}`;
|
|
319
|
+
const rateLimit = rateLimitInfo(res);
|
|
243
320
|
throw new SkillError(code, redact(String(message)), {
|
|
244
321
|
httpStatus: res.status,
|
|
245
322
|
url: redactUrl(url),
|
|
246
|
-
body: json ?? redact(text).slice(0, 800)
|
|
323
|
+
body: json ?? redact(text).slice(0, 800),
|
|
324
|
+
...retryable ? {
|
|
325
|
+
retries,
|
|
326
|
+
retryAfterSeconds: hintMs === null ? null : Math.ceil(hintMs / 1e3)
|
|
327
|
+
} : {},
|
|
328
|
+
...rateLimit ? { rateLimit } : {}
|
|
247
329
|
});
|
|
248
330
|
}
|
|
249
|
-
if (json === null) {
|
|
250
|
-
throw new SkillError("HTTP_BAD_JSON", "Endpoint returned a non-JSON body.", {
|
|
251
|
-
url: redactUrl(url),
|
|
252
|
-
snippet: redact(text).slice(0, 400)
|
|
253
|
-
});
|
|
254
|
-
}
|
|
255
|
-
return json;
|
|
256
331
|
}
|
|
257
332
|
function redactUrl(url) {
|
|
258
333
|
try {
|
|
@@ -277,31 +352,331 @@ function normalizeUtmSource(value) {
|
|
|
277
352
|
return s;
|
|
278
353
|
}
|
|
279
354
|
|
|
280
|
-
// scripts/src/lib/
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
355
|
+
// scripts/src/lib/identity.mjs
|
|
356
|
+
import crypto2 from "node:crypto";
|
|
357
|
+
|
|
358
|
+
// scripts/src/lib/prefs.mjs
|
|
359
|
+
import fs2 from "node:fs";
|
|
360
|
+
import path2 from "node:path";
|
|
361
|
+
import os2 from "node:os";
|
|
362
|
+
|
|
363
|
+
// scripts/src/lib/state.mjs
|
|
364
|
+
import fs from "node:fs";
|
|
365
|
+
import path from "node:path";
|
|
366
|
+
import os from "node:os";
|
|
367
|
+
import crypto from "node:crypto";
|
|
368
|
+
var LOCK_STALE_MS = 6e4;
|
|
369
|
+
var LOCK_WAIT_MS = 1e4;
|
|
370
|
+
var LOCK_POLL_MS = 25;
|
|
371
|
+
function lockPath() {
|
|
372
|
+
return path.join(stateRoot(), ".send.lock");
|
|
373
|
+
}
|
|
374
|
+
function sleepSync(ms) {
|
|
375
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
376
|
+
}
|
|
377
|
+
function tryAcquire(file) {
|
|
378
|
+
try {
|
|
379
|
+
const fd = fs.openSync(file, "wx", 384);
|
|
380
|
+
fs.writeFileSync(fd, JSON.stringify({ pid: process.pid, at: (/* @__PURE__ */ new Date()).toISOString() }));
|
|
381
|
+
fs.closeSync(fd);
|
|
382
|
+
return true;
|
|
383
|
+
} catch (err) {
|
|
384
|
+
if (err.code !== "EEXIST") throw err;
|
|
385
|
+
return false;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
function withLock(fn) {
|
|
389
|
+
const file = lockPath();
|
|
390
|
+
fs.mkdirSync(path.dirname(file), { recursive: true, mode: 448 });
|
|
391
|
+
const deadline = Date.now() + LOCK_WAIT_MS;
|
|
392
|
+
for (; ; ) {
|
|
393
|
+
if (tryAcquire(file)) break;
|
|
284
394
|
try {
|
|
285
|
-
const
|
|
286
|
-
if (
|
|
395
|
+
const age = Date.now() - fs.statSync(file).mtimeMs;
|
|
396
|
+
if (age > LOCK_STALE_MS) {
|
|
397
|
+
const stolen = `${file}.stale.${crypto.randomBytes(4).toString("hex")}`;
|
|
398
|
+
try {
|
|
399
|
+
fs.renameSync(file, stolen);
|
|
400
|
+
fs.unlinkSync(stolen);
|
|
401
|
+
} catch {
|
|
402
|
+
}
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
405
|
+
} catch (err) {
|
|
406
|
+
if (err.code !== "ENOENT") throw err;
|
|
407
|
+
continue;
|
|
408
|
+
}
|
|
409
|
+
if (Date.now() > deadline) {
|
|
410
|
+
throw new SkillError(
|
|
411
|
+
"LOCK_TIMEOUT",
|
|
412
|
+
"Another rozo-checkout process is holding the send lock. Refusing to proceed rather than risk a concurrent second send."
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
sleepSync(LOCK_POLL_MS);
|
|
416
|
+
}
|
|
417
|
+
try {
|
|
418
|
+
return fn();
|
|
419
|
+
} finally {
|
|
420
|
+
try {
|
|
421
|
+
fs.unlinkSync(file);
|
|
287
422
|
} catch {
|
|
288
423
|
}
|
|
289
424
|
}
|
|
290
|
-
|
|
291
|
-
|
|
425
|
+
}
|
|
426
|
+
function stateRoot() {
|
|
427
|
+
return process.env.ROZO_CHECKOUT_STATE_DIR || path.join(os.homedir(), ".rozo-checkout", "state");
|
|
428
|
+
}
|
|
429
|
+
function statePath(rozoPaymentId) {
|
|
430
|
+
if (!/^[A-Za-z0-9-]{8,64}$/.test(String(rozoPaymentId || ""))) {
|
|
431
|
+
throw new SkillError("BAD_ROZO_PAYMENT_ID", "Refusing to build a state path from that id.");
|
|
432
|
+
}
|
|
433
|
+
return path.join(stateRoot(), `${rozoPaymentId}.json`);
|
|
434
|
+
}
|
|
435
|
+
function writeAtomic(file, data) {
|
|
436
|
+
const dir = path.dirname(file);
|
|
437
|
+
fs.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
438
|
+
const tmp = path.join(dir, `.${path.basename(file)}.${crypto.randomBytes(6).toString("hex")}.tmp`);
|
|
439
|
+
const fd = fs.openSync(tmp, "wx", 384);
|
|
440
|
+
try {
|
|
441
|
+
fs.writeFileSync(fd, JSON.stringify(data, null, 2) + "\n", "utf8");
|
|
442
|
+
fs.fsyncSync(fd);
|
|
443
|
+
} finally {
|
|
444
|
+
fs.closeSync(fd);
|
|
445
|
+
}
|
|
446
|
+
fs.renameSync(tmp, file);
|
|
447
|
+
}
|
|
448
|
+
function readState(rozoPaymentId) {
|
|
449
|
+
const file = statePath(rozoPaymentId);
|
|
450
|
+
let raw;
|
|
451
|
+
try {
|
|
452
|
+
raw = fs.readFileSync(file, "utf8");
|
|
453
|
+
} catch (err) {
|
|
454
|
+
if (err.code === "ENOENT") return null;
|
|
455
|
+
throw new SkillError("STATE_UNREADABLE", `Cannot read local state: ${err.code}`);
|
|
456
|
+
}
|
|
457
|
+
try {
|
|
458
|
+
return JSON.parse(raw);
|
|
459
|
+
} catch {
|
|
460
|
+
throw new SkillError(
|
|
461
|
+
"STATE_CORRUPT",
|
|
462
|
+
"The local state file for this order is corrupt. Refusing to act; inspect it manually."
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
function createOrderRecord(record) {
|
|
467
|
+
return withLock(() => createOrderRecordUnlocked(record));
|
|
468
|
+
}
|
|
469
|
+
function mergeBitrefill(prev, next) {
|
|
470
|
+
if (!next) return prev ?? null;
|
|
471
|
+
if (!prev?.expiresAt || !next.expiresAt) return { ...next, expiresAt: next.expiresAt ?? prev?.expiresAt ?? null };
|
|
472
|
+
const a = Date.parse(prev.expiresAt);
|
|
473
|
+
const b = Date.parse(next.expiresAt);
|
|
474
|
+
return { ...next, expiresAt: Number.isFinite(a) && (!Number.isFinite(b) || a < b) ? prev.expiresAt : next.expiresAt };
|
|
475
|
+
}
|
|
476
|
+
function createOrderRecordUnlocked(record) {
|
|
477
|
+
const { rozoPaymentId } = record;
|
|
478
|
+
const existing = readState(rozoPaymentId);
|
|
479
|
+
const next = {
|
|
480
|
+
version: 1,
|
|
481
|
+
rozoPaymentId,
|
|
482
|
+
// 'coinbase' (default, historical records) or 'bitrefill'. Drives which
|
|
483
|
+
// payability checks presend and status run.
|
|
484
|
+
provider: record.provider ?? existing?.provider ?? "coinbase",
|
|
485
|
+
bitrefill: mergeBitrefill(existing?.bitrefill, record.bitrefill),
|
|
486
|
+
linkId: record.linkId,
|
|
487
|
+
paymentLink: record.paymentLink ?? null,
|
|
488
|
+
merchant: record.merchant ?? null,
|
|
489
|
+
invoiceAmount: record.invoiceAmount ?? null,
|
|
490
|
+
source: record.source,
|
|
491
|
+
receiverAddress: record.receiverAddress,
|
|
492
|
+
receiverMemo: record.receiverMemo ?? null,
|
|
493
|
+
amount: record.amount,
|
|
494
|
+
amountUnit: record.amountUnit ?? null,
|
|
495
|
+
expiresAt: record.expiresAt ?? null,
|
|
496
|
+
createdAt: existing?.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
497
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
498
|
+
confirmation: existing?.confirmation ?? null,
|
|
499
|
+
send: existing?.send ?? null
|
|
500
|
+
};
|
|
501
|
+
writeAtomic(statePath(rozoPaymentId), next);
|
|
502
|
+
return next;
|
|
503
|
+
}
|
|
504
|
+
function depositDigest(source) {
|
|
505
|
+
const canonical = JSON.stringify({
|
|
506
|
+
chainId: String(source?.chainId ?? ""),
|
|
507
|
+
tokenSymbol: String(source?.tokenSymbol ?? "").toUpperCase(),
|
|
508
|
+
tokenAddress: String(source?.tokenAddress ?? ""),
|
|
509
|
+
receiverAddress: String(source?.receiverAddress ?? ""),
|
|
510
|
+
receiverMemo: source?.receiverMemo ?? null,
|
|
511
|
+
amount: String(source?.amount ?? ""),
|
|
512
|
+
amountUnit: source?.amountUnit ?? null,
|
|
513
|
+
lnInvoice: source?.lnInvoice ?? null
|
|
514
|
+
});
|
|
515
|
+
return crypto.createHash("sha256").update(canonical, "utf8").digest("hex");
|
|
516
|
+
}
|
|
517
|
+
function recordConfirmation(rozoPaymentId, { source, invoiceAmount, tier }) {
|
|
518
|
+
return withLock(() => {
|
|
519
|
+
const state = readState(rozoPaymentId);
|
|
520
|
+
if (!state) {
|
|
521
|
+
throw new SkillError("NO_ORDER_STATE", "Cannot confirm an order with no local record.");
|
|
522
|
+
}
|
|
523
|
+
const next = {
|
|
524
|
+
...state,
|
|
525
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
526
|
+
confirmation: {
|
|
527
|
+
confirmedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
528
|
+
depositDigest: depositDigest(source),
|
|
529
|
+
invoiceAmount: invoiceAmount ?? state.invoiceAmount ?? null,
|
|
530
|
+
tier: tier ?? null
|
|
531
|
+
}
|
|
532
|
+
};
|
|
533
|
+
writeAtomic(statePath(rozoPaymentId), next);
|
|
534
|
+
return next;
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// scripts/src/lib/prefs.mjs
|
|
539
|
+
function prefsPath() {
|
|
540
|
+
const root = process.env.ROZO_CHECKOUT_STATE_DIR ? path2.dirname(stateRoot()) : path2.join(os2.homedir(), ".rozo-checkout");
|
|
541
|
+
return path2.join(root, "prefs.json");
|
|
542
|
+
}
|
|
543
|
+
var ALLOWED = ["lastPayerAddress", "lastAddressFamily", "lastPreset", "installId", "updatedAt"];
|
|
544
|
+
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}$/;
|
|
545
|
+
var LOCK_WAIT_MS2 = 2e3;
|
|
546
|
+
var LOCK_STALE_MS2 = 1e4;
|
|
547
|
+
function sleepSync2(ms) {
|
|
548
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
549
|
+
}
|
|
550
|
+
function withPrefsLock(fn) {
|
|
551
|
+
const lock = `${prefsPath()}.lock`;
|
|
552
|
+
fs2.mkdirSync(path2.dirname(lock), { recursive: true, mode: 448 });
|
|
553
|
+
const deadline = Date.now() + LOCK_WAIT_MS2;
|
|
554
|
+
for (; ; ) {
|
|
555
|
+
try {
|
|
556
|
+
fs2.closeSync(fs2.openSync(lock, "wx", 384));
|
|
557
|
+
break;
|
|
558
|
+
} catch (err) {
|
|
559
|
+
if (err.code !== "EEXIST") throw err;
|
|
560
|
+
}
|
|
561
|
+
try {
|
|
562
|
+
if (Date.now() - fs2.statSync(lock).mtimeMs > LOCK_STALE_MS2) {
|
|
563
|
+
fs2.rmSync(lock, { force: true });
|
|
564
|
+
continue;
|
|
565
|
+
}
|
|
566
|
+
} catch {
|
|
567
|
+
continue;
|
|
568
|
+
}
|
|
569
|
+
if (Date.now() > deadline) return void 0;
|
|
570
|
+
sleepSync2(10);
|
|
571
|
+
}
|
|
572
|
+
try {
|
|
573
|
+
return fn();
|
|
574
|
+
} finally {
|
|
575
|
+
fs2.rmSync(lock, { force: true });
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
var USER_FIELDS = ["lastPayerAddress", "lastAddressFamily", "lastPreset"];
|
|
579
|
+
function readPrefs() {
|
|
580
|
+
let raw;
|
|
581
|
+
try {
|
|
582
|
+
raw = fs2.readFileSync(prefsPath(), "utf8");
|
|
583
|
+
} catch {
|
|
584
|
+
return null;
|
|
585
|
+
}
|
|
586
|
+
let doc;
|
|
587
|
+
try {
|
|
588
|
+
doc = JSON.parse(raw);
|
|
589
|
+
} catch {
|
|
590
|
+
return null;
|
|
591
|
+
}
|
|
592
|
+
if (!doc || typeof doc !== "object" || Array.isArray(doc)) return null;
|
|
593
|
+
const out = {};
|
|
594
|
+
for (const k of ALLOWED) {
|
|
595
|
+
const v = doc[k];
|
|
596
|
+
if (typeof v === "string" && v.trim()) out[k] = v.trim();
|
|
597
|
+
}
|
|
598
|
+
return Object.keys(out).length ? out : null;
|
|
599
|
+
}
|
|
600
|
+
function savePrefs(update, { installIdIfAbsent } = {}) {
|
|
601
|
+
try {
|
|
602
|
+
const saved = withPrefsLock(() => {
|
|
603
|
+
const existing = readPrefs() || {};
|
|
604
|
+
const next = { ...existing };
|
|
605
|
+
for (const k of USER_FIELDS) {
|
|
606
|
+
const v = update?.[k];
|
|
607
|
+
if (typeof v === "string" && v.trim()) next[k] = v.trim();
|
|
608
|
+
}
|
|
609
|
+
if (!INSTALL_ID_RE.test(String(existing.installId ?? "")) && INSTALL_ID_RE.test(String(installIdIfAbsent ?? ""))) {
|
|
610
|
+
next.installId = installIdIfAbsent;
|
|
611
|
+
}
|
|
612
|
+
next.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
613
|
+
writeAtomic(prefsPath(), next);
|
|
614
|
+
return next;
|
|
615
|
+
});
|
|
616
|
+
return saved ?? null;
|
|
617
|
+
} catch {
|
|
618
|
+
return null;
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// scripts/src/lib/identity.mjs
|
|
623
|
+
var ACCOUNT_HASH_PREFIX = "rozo-acct-v1:";
|
|
624
|
+
function isValidInstallId(value) {
|
|
625
|
+
return typeof value === "string" && INSTALL_ID_RE.test(value);
|
|
626
|
+
}
|
|
627
|
+
function attributionDisabled(env = process.env) {
|
|
628
|
+
const v = String(env.ROZO_CHECKOUT_ANON_ID ?? "").trim().toLowerCase();
|
|
629
|
+
return ["off", "0", "false", "no"].includes(v);
|
|
630
|
+
}
|
|
631
|
+
function getOrCreateInstallId() {
|
|
632
|
+
const existing = readPrefs()?.installId;
|
|
633
|
+
if (isValidInstallId(existing)) return existing;
|
|
634
|
+
const saved = savePrefs({}, { installIdIfAbsent: crypto2.randomUUID().toLowerCase() });
|
|
635
|
+
if (isValidInstallId(saved?.installId)) return saved.installId;
|
|
636
|
+
const onDisk = readPrefs()?.installId;
|
|
637
|
+
return isValidInstallId(onDisk) ? onDisk : crypto2.randomUUID().toLowerCase();
|
|
638
|
+
}
|
|
639
|
+
function hashAccount(raw) {
|
|
640
|
+
return crypto2.createHash("sha256").update(`${ACCOUNT_HASH_PREFIX}${raw}`, "utf8").digest("hex");
|
|
641
|
+
}
|
|
642
|
+
function rawApiKey(env) {
|
|
643
|
+
const v = typeof env.OPENROUTER_API_KEY === "string" ? env.OPENROUTER_API_KEY.trim() : "";
|
|
644
|
+
return v && v.length <= 1024 ? v : null;
|
|
645
|
+
}
|
|
646
|
+
function attributionIdentity(env = process.env) {
|
|
647
|
+
if (attributionDisabled(env)) return {};
|
|
648
|
+
const out = {};
|
|
649
|
+
try {
|
|
650
|
+
const id = getOrCreateInstallId();
|
|
651
|
+
if (isValidInstallId(id)) out.install_id = id;
|
|
652
|
+
} catch {
|
|
653
|
+
}
|
|
654
|
+
const raw = rawApiKey(env);
|
|
655
|
+
if (raw) out.account_hash = hashAccount(raw);
|
|
656
|
+
return out;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// scripts/src/lib/api.mjs
|
|
292
660
|
var MPP_BASE = process.env.ROZO_CHECKOUT_MPP_BASE || "https://apiserver.mpprouter.dev/v1/services/rozo-agent-api";
|
|
293
661
|
var CLIENT_LABEL = `rozo-checkout-cli/${PKG_VERSION}`;
|
|
294
662
|
var ATTRIBUTION_CLIENT = `rozo-checkout-skill/${PKG_VERSION}`;
|
|
295
|
-
function buildAttribution({ utmSource } = {}) {
|
|
663
|
+
function buildAttribution({ utmSource, identity } = {}) {
|
|
296
664
|
const raw = utmSource !== void 0 ? utmSource : process.env.ROZO_CHECKOUT_UTM_SOURCE;
|
|
297
665
|
const utm_source = normalizeUtmSource(raw);
|
|
298
|
-
|
|
666
|
+
const ids = identity ?? attributionIdentity();
|
|
667
|
+
return {
|
|
668
|
+
client: ATTRIBUTION_CLIENT,
|
|
669
|
+
...utm_source ? { utm_source } : {},
|
|
670
|
+
...ids.install_id ? { install_id: ids.install_id } : {},
|
|
671
|
+
...ids.account_hash ? { account_hash: ids.account_hash } : {}
|
|
672
|
+
};
|
|
299
673
|
}
|
|
300
674
|
var INTENTS_BASE = process.env.ROZO_CHECKOUT_INTENTS_BASE || "https://intentapiv4.rozo.ai/functions/v1/payment-api";
|
|
301
675
|
async function quoteInvoice({ url, linkId }) {
|
|
302
676
|
const body = url ? { url } : { payment_id: linkId };
|
|
303
677
|
return postJson(`${MPP_BASE}/quote-invoice`, body);
|
|
304
678
|
}
|
|
679
|
+
var QUOTE_RECEIPT_RETRY_BUDGET_MS = 2e4;
|
|
305
680
|
async function createInvoice({ url, linkId, source, quoteReceipt, utmSource, email }) {
|
|
306
681
|
const body = {
|
|
307
682
|
...url ? { url } : { payment_id: linkId },
|
|
@@ -313,7 +688,11 @@ async function createInvoice({ url, linkId, source, quoteReceipt, utmSource, ema
|
|
|
313
688
|
// sent empty, when the user gave none.
|
|
314
689
|
...email ? { email } : {}
|
|
315
690
|
};
|
|
316
|
-
return postJson(
|
|
691
|
+
return postJson(
|
|
692
|
+
`${MPP_BASE}/create-invoice`,
|
|
693
|
+
body,
|
|
694
|
+
quoteReceipt ? { maxTotalWaitMs: QUOTE_RECEIPT_RETRY_BUDGET_MS } : void 0
|
|
695
|
+
);
|
|
317
696
|
}
|
|
318
697
|
async function invoiceStatus({ linkId, rozoPaymentId }) {
|
|
319
698
|
const qs = new URLSearchParams();
|
|
@@ -502,6 +881,8 @@ var MARGINS_MS = {
|
|
|
502
881
|
};
|
|
503
882
|
var BOLT11_MIN_VALIDITY_MS = 10 * MINUTE;
|
|
504
883
|
var DEFAULT_MARGIN_MS = 10 * MINUTE;
|
|
884
|
+
var DEFAULT_WATCH_MS = 10 * MINUTE;
|
|
885
|
+
var LIGHTNING_WATCH_CAP_MS = 60 * MINUTE;
|
|
505
886
|
function formatRemaining(ms) {
|
|
506
887
|
if (!Number.isFinite(ms)) return "unknown";
|
|
507
888
|
if (ms <= 0) return "expired";
|
|
@@ -891,9 +1272,9 @@ function checkPayable(statusResponse, now = Date.now()) {
|
|
|
891
1272
|
}
|
|
892
1273
|
|
|
893
1274
|
// scripts/src/lib/blacklist.mjs
|
|
894
|
-
import
|
|
895
|
-
import
|
|
896
|
-
import
|
|
1275
|
+
import fs3 from "node:fs";
|
|
1276
|
+
import path3 from "node:path";
|
|
1277
|
+
import crypto3 from "node:crypto";
|
|
897
1278
|
import { fileURLToPath } from "node:url";
|
|
898
1279
|
var BlacklistError = class extends Error {
|
|
899
1280
|
constructor(code, message) {
|
|
@@ -928,7 +1309,7 @@ function parseBlacklist(doc) {
|
|
|
928
1309
|
}
|
|
929
1310
|
addresses.push(e.address);
|
|
930
1311
|
}
|
|
931
|
-
const digest =
|
|
1312
|
+
const digest = crypto3.createHash("sha256").update(JSON.stringify(addresses), "utf8").digest("hex");
|
|
932
1313
|
if (typeof provenance.addressesSha256 !== "string" || !provenance.addressesSha256) {
|
|
933
1314
|
throw new BlacklistError("BLACKLIST_UNAVAILABLE", "Blacklist provenance digest is missing.");
|
|
934
1315
|
}
|
|
@@ -951,11 +1332,11 @@ function parseBlacklist(doc) {
|
|
|
951
1332
|
return { entries, index, provenance, digest };
|
|
952
1333
|
}
|
|
953
1334
|
function candidatePaths(moduleUrl) {
|
|
954
|
-
const here =
|
|
1335
|
+
const here = path3.dirname(fileURLToPath(moduleUrl));
|
|
955
1336
|
return [
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
1337
|
+
path3.join(here, "blacklist.json"),
|
|
1338
|
+
path3.join(here, "..", "src", "lib", "blacklist.json"),
|
|
1339
|
+
path3.join(here, "..", "..", "src", "lib", "blacklist.json")
|
|
959
1340
|
];
|
|
960
1341
|
}
|
|
961
1342
|
var cached = null;
|
|
@@ -966,7 +1347,7 @@ function loadBlacklist(explicitPath) {
|
|
|
966
1347
|
for (const p of paths) {
|
|
967
1348
|
let raw;
|
|
968
1349
|
try {
|
|
969
|
-
raw =
|
|
1350
|
+
raw = fs3.readFileSync(p, "utf8");
|
|
970
1351
|
} catch (err) {
|
|
971
1352
|
lastErr = err;
|
|
972
1353
|
continue;
|
|
@@ -1013,181 +1394,6 @@ function assertNotBlacklisted(targets, blacklist) {
|
|
|
1013
1394
|
return true;
|
|
1014
1395
|
}
|
|
1015
1396
|
|
|
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
1397
|
// scripts/src/create-order.mjs
|
|
1192
1398
|
function confirmTier(usdAmount) {
|
|
1193
1399
|
const usd = Number(usdAmount);
|