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.
174 lines • 6.91 kB
JavaScript
import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync, renameSync, statSync, rmSync, } from "node:fs";
import { dirname, join } from "node:path";
import { randomBytes, createHash } from "node:crypto";
import { getConfigBaseDir } from "./config.js";
const DEFAULT_TTL_MS = 30 * 60 * 1000;
/**
* Path to the approval cache: `{base}/write-approvals.json`.
* `RUN402_WRITE_APPROVALS_PATH` overrides for testing.
*/
export function getWriteApprovalsPath() {
return (process.env.RUN402_WRITE_APPROVALS_PATH ||
join(getConfigBaseDir(), "write-approvals.json"));
}
/** Stable short hash binding an approval to a control-plane session token. */
export function hashControlPlaneSession(token) {
return createHash("sha256").update(token).digest("hex").slice(0, 32);
}
/** Tighten 0600 if group/other-readable, warning once. Best-effort, POSIX-only. */
function selfHealPermissions(p) {
if (process.platform === "win32")
return;
try {
const mode = statSync(p).mode & 0o777;
if ((mode & 0o077) !== 0) {
chmodSync(p, 0o600);
process.stderr.write(`warning: tightened permissions on ${p} from ${mode.toString(8)} to 600 (was readable by other users).\n`);
}
}
catch {
// Best-effort; never block a read on a chmod/stat failure.
}
}
function isApproval(x) {
if (!x || typeof x !== "object")
return false;
const a = x;
return (typeof a.write_approval_token === "string" &&
a.write_approval_token.length > 0 &&
typeof a.action === "string" &&
typeof a.api_origin === "string" &&
typeof a.control_plane_session_hash === "string" &&
typeof a.expires_at === "number" &&
Number.isFinite(a.expires_at));
}
/**
* Read all cached approvals. Returns `[]` for the "no cache" cases (absent,
* unreadable, unparseable). Throws when the file parses as JSON but the shape
* is wrong, so a corrupted cache surfaces a clear fix-it.
*/
export function readApprovals(path) {
const p = path ?? getWriteApprovalsPath();
if (!existsSync(p))
return [];
selfHealPermissions(p);
let raw;
try {
raw = readFileSync(p, "utf-8");
}
catch {
return [];
}
let parsed;
try {
parsed = JSON.parse(raw);
}
catch {
return [];
}
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
throw new Error("write-approvals.json must contain a JSON object. Delete it and re-run 'run402 approve' to recreate it.");
}
const approvals = parsed.approvals;
if (!Array.isArray(approvals)) {
throw new Error("write-approvals.json missing an 'approvals' array. Delete it and re-run 'run402 approve'.");
}
return approvals.filter(isApproval);
}
function writeApprovals(approvals, path) {
const p = path ?? getWriteApprovalsPath();
const dir = dirname(p);
mkdirSync(dir, { recursive: true });
const tmp = join(dir, `.write-approvals.${randomBytes(4).toString("hex")}.tmp`);
writeFileSync(tmp, JSON.stringify({ approvals }, null, 2), { mode: 0o600 });
renameSync(tmp, p);
chmodSync(p, 0o600);
}
function sameTarget(a, b) {
return (a.org_id ?? null) === (b.org_id ?? null) && (a.project_id ?? null) === (b.project_id ?? null);
}
function sameKey(a, b) {
return (a.api_origin === b.api_origin &&
a.control_plane_session_hash === b.control_plane_session_hash &&
a.action === b.action &&
sameTarget(a, b));
}
/**
* Persist an approval. Replaces any existing entry with the same
* `(api_origin, control_plane_session_hash, action, target)` key and leaves
* every other entry intact (multi-entry, non-thrashing). Atomic, mode 0600.
*/
export function saveApproval(approval, path) {
const existing = readApprovals(path).filter((a) => !sameKey(a, approval));
existing.push(approval);
writeApprovals(existing, path);
}
/** Delete the whole approval cache — local half of `run402 logout`. Idempotent. */
export function clearApprovals(path) {
const p = path ?? getWriteApprovalsPath();
try {
rmSync(p, { force: true });
}
catch {
// Best-effort: a failed unlink should never crash logout.
}
}
/** Whether an approval is past its usable life (with a small skew buffer). */
export function isApprovalExpired(approval, nowMs = Date.now(), skewMs = 10_000) {
return nowMs + skewMs >= approval.expires_at;
}
/**
* Return the cached approval matching ALL of `(apiOrigin, cpSessionHash,
* capability, target)` and still live, or `null` if none matches or it is
* expired. This is the exact-match the gateway's target gate requires — a
* non-match (wrong action/target/origin/session) fails closed.
*/
export function loadLiveApproval(q, path, nowMs = Date.now()) {
for (const a of readApprovals(path)) {
if (a.api_origin === q.apiOrigin &&
a.control_plane_session_hash === q.cpSessionHash &&
a.action === q.capability &&
sameTarget(a, q.target) &&
!isApprovalExpired(a, nowMs)) {
return a;
}
}
return null;
}
/** Parse a gateway-returned session expiry (ISO string or epoch) to epoch ms. */
function sessionExpiryMs(session, nowMs) {
const raw = session?.idle_expires_at ?? session?.expires_at ?? session?.absolute_expires_at;
if (typeof raw === "number" && Number.isFinite(raw))
return raw > 1e12 ? raw : raw * 1000;
if (typeof raw === "string") {
const ms = Date.parse(raw);
if (Number.isFinite(ms))
return ms;
}
return nowMs + DEFAULT_TTL_MS;
}
/**
* Build a cache entry from the gateway token response + the binding context
* (the cp-session it was minted under, the API origin, and the `(action,
* target)` it covers). Expiry is taken from the returned `session`.
*/
export function approvalFromTokenResponse(resp, binding, nowMs = Date.now()) {
const amr = Array.isArray(resp.session?.amr)
? resp.session.amr.filter((a) => typeof a === "string")
: undefined;
return {
write_approval_token: resp.write_approval_token,
token_type: typeof resp.token_type === "string" ? resp.token_type : "write_approval",
header: typeof resp.header === "string" ? resp.header : "X-Run402-Write-Approval",
action: binding.action,
...(binding.target.org_id ? { org_id: binding.target.org_id } : {}),
...(binding.target.project_id ? { project_id: binding.target.project_id } : {}),
expires_at: sessionExpiryMs(resp.session, nowMs),
control_plane_session_hash: binding.controlPlaneSessionHash,
control_plane_principal_id: binding.controlPlanePrincipalId,
api_origin: binding.apiOrigin,
...(amr ? { amr } : {}),
minted_at: nowMs,
};
}
//# sourceMappingURL=write-approvals.js.map