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.
139 lines • 5.85 kB
JavaScript
import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync, renameSync, statSync, rmSync, } from "node:fs";
import { dirname, join } from "node:path";
import { randomBytes } from "node:crypto";
import { getConfigBaseDir } from "./config.js";
/**
* Path to the cached control-plane session: `{base}/control-plane-session.json`.
* `RUN402_CONTROL_PLANE_SESSION_PATH` overrides for testing.
*/
export function getControlPlaneSessionPath() {
return (process.env.RUN402_CONTROL_PLANE_SESSION_PATH ||
join(getConfigBaseDir(), "control-plane-session.json"));
}
/**
* Read a wire grade. An unknown or absent value is treated as `device` (the
* read-only grade) so a cache the client cannot classify never claims more
* authority than the gateway will grant.
*/
function toGrade(raw) {
return raw === "browser" || raw === "loopback" || raw === "device" ? raw : "device";
}
/** 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.
}
}
/**
* Load the cached control-plane session. Returns `null` for the "no session"
* 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 readControlPlaneSession(path) {
const p = path ?? getControlPlaneSessionPath();
if (!existsSync(p))
return null;
selfHealPermissions(p);
let raw;
try {
raw = readFileSync(p, "utf-8");
}
catch {
return null;
}
let parsed;
try {
parsed = JSON.parse(raw);
}
catch {
return null;
}
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
throw new Error("control-plane-session.json must contain a JSON object. Delete it and run 'run402 login' to recreate it.");
}
const data = parsed;
if (typeof data.control_plane_session_token !== "string" ||
data.control_plane_session_token.length === 0) {
throw new Error("control-plane-session.json missing valid 'control_plane_session_token'. Run 'run402 login' to refresh it.");
}
if (typeof data.expires_at !== "number" || !Number.isFinite(data.expires_at)) {
throw new Error("control-plane-session.json missing valid 'expires_at'. Run 'run402 login' to refresh it.");
}
return {
control_plane_session_token: data.control_plane_session_token,
token_type: typeof data.token_type === "string" ? data.token_type : "Bearer",
grade: toGrade(data.grade),
principal_id: typeof data.principal_id === "string" ? data.principal_id : "",
amr: Array.isArray(data.amr) ? data.amr.filter((a) => typeof a === "string") : [],
expires_at: data.expires_at,
};
}
/** Persist a control-plane session atomically (temp-file + rename), mode 0600. */
export function saveControlPlaneSession(data, path) {
const p = path ?? getControlPlaneSessionPath();
const dir = dirname(p);
mkdirSync(dir, { recursive: true });
const tmp = join(dir, `.control-plane-session.${randomBytes(4).toString("hex")}.tmp`);
writeFileSync(tmp, JSON.stringify(data, null, 2), { mode: 0o600 });
renameSync(tmp, p);
chmodSync(p, 0o600);
}
/** Delete the cached sign-in session — local half of `run402 logout`. Idempotent. */
export function clearControlPlaneSession(path) {
const p = path ?? getControlPlaneSessionPath();
try {
rmSync(p, { force: true });
}
catch {
// Best-effort: a failed unlink should never crash logout.
}
}
/** Whether a cached session is past its usable life (with a small skew buffer). */
export function isControlPlaneSessionExpired(session, nowMs = Date.now(), skewMs = 10_000) {
return nowMs + skewMs >= session.expires_at;
}
/** Read the cached session and return it only if still usable; `null` if absent or expired. */
export function loadLiveControlPlaneSession(path, nowMs = Date.now()) {
const s = readControlPlaneSession(path);
if (!s)
return null;
return isControlPlaneSessionExpired(s, nowMs) ? null : s;
}
/** Map a gateway token payload (relative `expires_in`) into the cached shape (absolute `expires_at`). */
export function controlPlaneSessionFromTokenResponse(resp, nowMs = Date.now()) {
return {
control_plane_session_token: resp.control_plane_session_token,
token_type: resp.token_type ?? "Bearer",
grade: toGrade(resp.grade),
principal_id: resp.principal_id ?? "",
amr: Array.isArray(resp.amr) ? resp.amr.filter((a) => typeof a === "string") : [],
expires_at: nowMs + (typeof resp.expires_in === "number" ? resp.expires_in : 0) * 1000,
};
}
/**
* Delete the session caches of the retired email-scoped session
* (`{base}/operator-session.json`) and the retired approval cache file
* (`{base}/write-auth-session.json`). Neither is read any more; `run402 login`
* and `run402 logout` call this so a stale token does not linger on disk.
* Best-effort and idempotent.
*/
export function clearRetiredSessionCaches() {
for (const name of ["operator-session.json", "write-auth-session.json"]) {
try {
rmSync(join(getConfigBaseDir(), name), { force: true });
}
catch {
// Best-effort: a failed unlink never blocks login or logout.
}
}
}
//# sourceMappingURL=control-plane-session.js.map