run402-mcp
Version:
MCP server for Run402 — full-stack backend infrastructure for AI agents: Postgres, auth, storage, serverless functions and atomic deploys. Paid with x402/MPP. Includes $0.03 image generation.
231 lines • 9.64 kB
JavaScript
/**
* Sanitized local journal for automatic x402 payment attempts.
*
* Each attempt is a separate mode-0600 file. Separate files avoid lost
* updates when multiple SDK processes pay concurrently. Records intentionally
* exclude request headers, query strings, bodies, wallet keys, signed payment
* authorizations, and provider proofs.
*/
import { chmodSync, closeSync, fchmodSync, fsyncSync, mkdirSync, openSync, readFileSync, renameSync, readdirSync, rmSync, statSync, writeFileSync, } from "node:fs";
import { dirname, join } from "node:path";
import { createHash, randomUUID } from "node:crypto";
import { getConfigDir } from "../../core-dist/config.js";
import { LocalError } from "../errors.js";
export const PAYMENT_ATTEMPT_HEADER = "X-Run402-Payment-Attempt-Id";
export const PAYMENT_ATTEMPT_ID_PATTERN = /^pat_[0-9a-f]{32}$/;
export function createPaymentAttemptId() {
return `pat_${randomUUID().replaceAll("-", "")}`;
}
export function paymentAttemptJournalDir() {
return join(getConfigDir(), "payment-attempts");
}
export function createFilePaymentAttemptStore(dir = paymentAttemptJournalDir()) {
return {
claim(record) {
assertPaymentAttemptId(record.payment_attempt_id);
ensureJournalDir(dir);
const path = join(dir, `${record.payment_attempt_id}.json`);
let fd;
try {
fd = openSync(path, "wx", 0o600);
}
catch (cause) {
if (cause?.code === "EEXIST")
return false;
throw cause;
}
try {
writeFileSync(fd, `${JSON.stringify(record, null, 2)}\n`);
fchmodSync(fd, 0o600);
fsyncSync(fd);
}
finally {
closeSync(fd);
}
fsyncDirectory(dir);
return true;
},
write(record) {
assertPaymentAttemptId(record.payment_attempt_id);
ensureJournalDir(dir);
const path = join(dir, `${record.payment_attempt_id}.json`);
const tmp = `${path}.${process.pid}.${randomUUID()}.tmp`;
try {
const fd = openSync(tmp, "wx", 0o600);
try {
writeFileSync(fd, `${JSON.stringify(record, null, 2)}\n`);
fchmodSync(fd, 0o600);
fsyncSync(fd);
}
finally {
closeSync(fd);
}
renameSync(tmp, path);
fsyncDirectory(dir);
}
finally {
rmSync(tmp, { force: true });
}
},
read(paymentAttemptId) {
assertPaymentAttemptId(paymentAttemptId);
const path = join(dir, `${paymentAttemptId}.json`);
let raw;
try {
raw = readFileSync(path, "utf8");
}
catch (cause) {
if (cause?.code === "ENOENT")
return null;
throw new LocalError("The local x402 payment attempt could not be read.", "reading x402 payment attempt", { code: "PAYMENT_ATTEMPT_READ_FAILED", cause });
}
let parsed;
try {
parsed = JSON.parse(raw);
}
catch (cause) {
throw new LocalError("The local x402 payment attempt record is not valid JSON.", "reading x402 payment attempt", { code: "PAYMENT_ATTEMPT_RECORD_INVALID", cause });
}
if (!isPaymentAttemptRecord(parsed)) {
throw new LocalError("The local x402 payment attempt record has an invalid shape.", "reading x402 payment attempt", { code: "PAYMENT_ATTEMPT_RECORD_INVALID" });
}
return parsed;
},
};
}
function ensureJournalDir(dir) {
mkdirSync(dir, { recursive: true, mode: 0o700 });
chmodSync(dir, 0o700);
// Persist both the directory metadata and its entry in the parent before a
// payment can depend on a claim within it.
fsyncDirectory(dir);
fsyncDirectory(dirname(dir));
}
function fsyncDirectory(dir) {
// Node cannot open directory handles for fsync on Windows. Exclusive create
// and atomic replace still apply there; POSIX needs this extra durability
// barrier for the directory entry itself.
if (process.platform === "win32")
return;
const fd = openSync(dir, "r");
try {
fsyncSync(fd);
}
finally {
closeSync(fd);
}
}
/** Read one sanitized attempt from the active wallet profile's journal. */
export function readPaymentAttempt(paymentAttemptId) {
return createFilePaymentAttemptStore().read(paymentAttemptId);
}
/**
* List recent sanitized attempts for reconciliation. This is a local read;
* provider/target state remains authoritative for ambiguous attempts.
*/
export function listPaymentAttempts(opts = {}) {
const limit = Math.max(1, Math.min(100, Math.trunc(opts.limit ?? 20)));
const dir = paymentAttemptJournalDir();
let names;
try {
names = readdirSync(dir).filter((name) => /^pat_[0-9a-f]{32}\.json$/.test(name));
}
catch {
return [];
}
return names
.map((name) => {
try {
const path = join(dir, name);
const parsed = JSON.parse(readFileSync(path, "utf8"));
return isPaymentAttemptRecord(parsed)
? { record: parsed, mtimeMs: statSync(path).mtimeMs }
: null;
}
catch {
return null;
}
})
.filter((item) => item !== null)
.sort((a, b) => b.mtimeMs - a.mtimeMs)
.slice(0, limit)
.map((item) => item.record);
}
export function requestSummary(input, init) {
const method = (init?.method ?? (input instanceof Request ? input.method : "GET")).toUpperCase();
const callerKey = mergedHeaders(input, init).get("idempotency-key");
const caller_key_sha256 = callerKey
? createHash("sha256").update(callerKey).digest("hex")
: null;
try {
const url = new URL(input instanceof Request ? input.url : String(input));
return {
method,
origin: url.origin,
path_sha256: createHash("sha256").update(url.pathname).digest("hex"),
caller_key_sha256,
};
}
catch {
return { method, origin: null, path_sha256: null, caller_key_sha256 };
}
}
export function attemptIdFromRequest(input, init) {
const headers = mergedHeaders(input, init);
const supplied = headers.get(PAYMENT_ATTEMPT_HEADER);
if (!supplied)
return null;
assertPaymentAttemptId(supplied);
return supplied;
}
export function withPaymentAttemptHeader(input, init, paymentAttemptId, paymentBearing) {
const headers = mergedHeaders(input, init);
// Reserved correlation metadata is sent only with the payment-bearing call,
// never the unpriced discovery request. `redirect: "error"` prevents both
// this id and the signed payment authorization from being forwarded to a
// redirect target (especially an unrelated origin).
headers.delete(PAYMENT_ATTEMPT_HEADER);
if (paymentBearing)
headers.set(PAYMENT_ATTEMPT_HEADER, paymentAttemptId);
return [input, { ...init, headers, ...(paymentBearing ? { redirect: "error" } : {}) }];
}
export function hasPaymentAuthorization(input, init) {
const headers = mergedHeaders(input, init);
return headers.has("PAYMENT-SIGNATURE") || headers.has("X-PAYMENT");
}
function mergedHeaders(input, init) {
const headers = new Headers(input instanceof Request ? input.headers : undefined);
if (init?.headers) {
new Headers(init.headers).forEach((value, key) => headers.set(key, value));
}
return headers;
}
function assertPaymentAttemptId(value) {
if (!PAYMENT_ATTEMPT_ID_PATTERN.test(value)) {
throw new LocalError("Payment attempt id must match pat_ followed by 32 lowercase hexadecimal characters.", "reading x402 payment attempt", { code: "INVALID_PAYMENT_ATTEMPT_ID", details: { field: "paymentAttemptId" } });
}
}
function isPaymentAttemptRecord(value) {
if (!value || typeof value !== "object" || Array.isArray(value))
return false;
const record = value;
return (record.version === 1 &&
typeof record.payment_attempt_id === "string" &&
PAYMENT_ATTEMPT_ID_PATTERN.test(record.payment_attempt_id) &&
record.rail === "x402" &&
["intent", "submitting", "response_received", "intent_pending", "completed", "failed", "ambiguous"]
.includes(record.state ?? "") &&
["not_started", "in_progress", "completed", "ambiguous"].includes(record.mutation_state ?? "") &&
typeof record.method === "string" &&
(record.origin === null || typeof record.origin === "string") &&
(record.path_sha256 === null ||
(typeof record.path_sha256 === "string" && /^[0-9a-f]{64}$/.test(record.path_sha256))) &&
(record.caller_key_sha256 === undefined || /^[0-9a-f]{64}$/.test(record.caller_key_sha256)) &&
(record.payment_id === undefined || typeof record.payment_id === "string") &&
(record.intent_state === undefined || typeof record.intent_state === "string") &&
(record.retry_after_seconds === undefined ||
(Number.isSafeInteger(record.retry_after_seconds) && record.retry_after_seconds >= 0)) &&
typeof record.created_at === "string" &&
typeof record.updated_at === "string");
}
//# sourceMappingURL=payment-attempts.js.map