UNPKG

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.

88 lines • 3.94 kB
import { isRun402Error, Unauthorized } from "./errors.js"; /** * A locally cached project key the gateway refused at authentication. * * The project database routes (SQL, schema, project REST) accept either a * service key or the caller's own principal, and the gateway picks the chain by * the credential's shape, so it cannot fall back on its own: a cached key that * expired or was revoked fails every call even when the caller's wallet or * sign-in session would be authorized. These helpers make that fallback on the * client. * * The codes below come only from the credential check that runs before any * route handler (revoked or expired, an unknown secret, an unverifiable legacy * key), so the refused request had no effect and repeating it as the * principal is safe for reads and writes alike. */ const STALE_PROJECT_KEY_CODES = new Set(["CREDENTIAL_REVOKED", "CREDENTIAL_MISSING", "INVALID_AUTH"]); /** * Keys this credentials provider has seen refused, per project, so later calls * in the same process go straight to the principal. Keyed by the key value, so * re-importing a fresh key makes it usable again. */ const refusedKeys = new WeakMap(); function refusedFor(client) { const owner = client.credentials; let map = refusedKeys.get(owner); if (!map) { map = new Map(); refusedKeys.set(owner, map); } return map; } /** The cached keys for `projectId`, or null when none are cached or this process saw the service key refused. */ export async function usableProjectKeys(client, projectId) { const keys = await client.getProjectCredentials(projectId); if (!keys) return null; return refusedFor(client).get(projectId) === keys.service_key ? null : keys; } function isStaleKeyRefusal(err) { return isRun402Error(err) && err.status === 401 && STALE_PROJECT_KEY_CODES.has(err.code ?? ""); } /** The command that drops a stale entry from this machine's project-key cache. */ export function deleteCachedProjectKeysCommand(projectId) { return `run402 credentials project-keys delete --project ${projectId}`; } /** The gateway's refusal, with the stale local cache entry named as the first next action. */ function staleKeyError(err, projectId) { const body = err.body && typeof err.body === "object" && !Array.isArray(err.body) ? err.body : {}; const remove = { type: "run_cli_command", command: deleteCachedProjectKeysCommand(projectId), why: "This machine's cached project key was refused. Delete the stale cache entry so the call goes as your wallet or sign-in session, or re-key with `run402 credentials issue --kind service --name <name> --import`.", }; const existing = Array.isArray(body.next_actions) ? body.next_actions : []; return new Unauthorized(err.message, err.status, { ...body, hint: "The locally cached project key was refused (expired, revoked or unknown), and no wallet or sign-in session could make the call instead.", next_actions: [remove, ...existing], }, err.context); } /** * Run `call` with the cached keys; when the gateway refuses the cached key at * authentication, run it once more as the caller's principal (`call(null)`). * When the principal is refused too (no wallet or session), throw the * original refusal with the stale cache entry named. */ export async function withPrincipalFallback(client, projectId, keys, call) { try { return await call(keys); } catch (err) { if (!keys || !isStaleKeyRefusal(err)) throw err; refusedFor(client).set(projectId, keys.service_key); try { return await call(null); } catch (retryErr) { if (isRun402Error(retryErr) && retryErr.status === 401) throw staleKeyError(err, projectId); throw retryErr; } } } //# sourceMappingURL=stale-project-key.js.map