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.

213 lines • 9.82 kB
/** * Node credential provider — wraps the local project-key cache + SIWX auth. * Reads `credentials/project-keys.v1.json` (or an explicit `keystorePath`), * signs SIWX headers from the wallet private key, and serves project * anon/service keys from disk only for operations classified as credential- * required. */ import { randomBytes, createECDH } from "node:crypto"; import { keccak_256 } from "@noble/hashes/sha3.js"; import { getProject as coreGetProject, loadKeyStore as coreLoadKeyStore, saveProject as coreSaveProject, updateProject as coreUpdateProject, removeProject as coreRemoveProject, } from "../../core-dist/keystore.js"; import { getActiveOrgId, getActiveProjectId, setActiveProjectId, } from "../../core-dist/profile-state.js"; import { getWalletAuthHeadersAsync } from "../../core-dist/wallet-auth.js"; import { personalMessageSignerFor, resolveWalletAccount } from "./signers/index.js"; import { readWallet as coreReadWallet, saveWallet as coreSaveWallet } from "../../core-dist/wallet.js"; import { getWalletPath as coreGetWalletPath, getActiveProfile, getApiBase, getProfileStatePath as coreGetProfileStatePath, getProjectCredentialsPath as coreGetProjectCredentialsPath, } from "../../core-dist/config.js"; import { readMeta } from "../../core-dist/profiles.js"; import { loadLiveControlPlaneSession } from "../../core-dist/control-plane-session.js"; import { loadLiveApproval, hashControlPlaneSession } from "../../core-dist/write-approvals.js"; import { GRANT_KEY_CREDENTIALS, grantKeyFromEnv } from "../grant-key-credentials.js"; import { LocalError } from "../errors.js"; export class NodeCredentialsProvider { options; constructor(options = {}) { this.options = options; } /** * Marks this provider as grant-key-backed so apikey-attaching helpers stand * down (identical treatment to a CI session — the bearer already authorizes * the apikey-gated CAS routes, and mixing the two families on one request is * exactly what the kernel's credential-atomicity rule forbids). */ get [GRANT_KEY_CREDENTIALS]() { return this.resolveGrantKey() !== undefined; } /** Explicit option wins over the environment; blank env values are ignored. */ resolveGrantKey() { const explicit = this.options.grantKey?.trim(); if (explicit) return explicit; return grantKeyFromEnv(); } /** Effective credential mode. Explicit `authMode` wins; else `cli → auto`, everything else → `wallet`. */ resolveAuthMode() { return this.options.authMode ?? (this.options.surface === "cli" ? "auto" : "wallet"); } /** * Deterministic credential resolution — selects exactly one credential class * and never silently falls back to another after a failure. * * - `wallet` (default; the MCP, sandbox, and agent path): only the SIWX wallet. NEVER * reads the sign-in session or write-approval caches, so a person's * ambient authority cannot leak into an agent tool call. * - `auto` (CLI): SIWX wallet if present; otherwise the live control-plane * sign-in session, plus an `X-Run402-Write-Approval` approval ONLY when the request's * `(capability, target)` exactly matches a cached, origin/session-bound * approval. A gated write with no match is sent cp-bearer-only and fails * closed with `WRITE_APPROVAL_REQUIRED`. * - `session`: only the sign-in session (plus a matching write approval). */ async getAuth(path, metadata) { const mode = this.resolveAuthMode(); if (mode === "none") return null; // A grant key is an EXPLICIT credential class: presenting one is a // deliberate act (an env var or an option), so it wins outright and never // falls back to a wallet or control-plane session. If it is revoked or // expired the request fails closed, which is the intended behaviour — a // silent downgrade to ambient authority would defeat the point of handing // an agent a scoped credential in the first place. const grantKey = this.resolveGrantKey(); if (grantKey) return { Authorization: `Bearer ${grantKey}` }; if (mode === "grant_key") return null; // A key-backed wallet signs locally; a signer-backed one (KMS) through its signer. const wallet = await getWalletAuthHeadersAsync(path, this.options.walletPath, (w) => personalMessageSignerFor(w)); if (mode === "wallet") return wallet ? { ...wallet } : null; if (mode === "auto" && wallet) return { ...wallet }; // session mode, or auto with no wallet: the sign-in session principal. const cp = loadLiveControlPlaneSession(); if (!cp) return wallet ? { ...wallet } : null; const headers = { Authorization: `Bearer ${cp.control_plane_session_token}`, }; // Capability+target-matched approval attachment (never blanket). if (metadata?.capability && metadata.target) { const approval = loadLiveApproval({ apiOrigin: this.apiOrigin(), cpSessionHash: hashControlPlaneSession(cp.control_plane_session_token), capability: metadata.capability, target: metadata.target, }); if (approval) headers["X-Run402-Write-Approval"] = `Bearer ${approval.write_approval_token}`; } return headers; } /** API origin used to bind/look-up approvals (matches the ceremony's mint origin). */ apiOrigin() { try { return new URL(getApiBase()).origin; } catch { return getApiBase(); } } async getProjectCredentials(id) { const p = coreGetProject(id, this.options.keystorePath); return p ?? null; } async listProjectCredentials() { return coreLoadKeyStore(this.options.keystorePath).projects; } async getProject(id) { return this.getProjectCredentials(id); } async saveProject(id, project) { coreSaveProject(id, project, this.options.keystorePath); } async updateProject(id, patch) { coreUpdateProject(id, patch, this.options.keystorePath); } async removeProject(id) { coreRemoveProject(id, this.options.keystorePath); } async setActiveProject(id) { setActiveProjectId(id, this.options.profileStatePath, this.activeScope()); } async getActiveProject() { return getActiveProjectId(this.options.profileStatePath, this.activeScope()) ?? null; } /** * The profile's current organization (`run402 orgs use`). The CLI's * org-context resolver writes and reads it per profile WITHOUT a principal * (the "unknown" bucket), so that bucket is the authoritative one; the * principal-scoped bucket is consulted first only so a provider that wrote * it scoped like the active project is honoured too. */ async getActiveOrg() { return getActiveOrgId(this.options.profileStatePath, this.activeScope()) ?? getActiveOrgId(this.options.profileStatePath) ?? null; } async readWallet() { return coreReadWallet(this.options.walletPath) ?? null; } async signPersonalMessage(message) { const localWallet = coreReadWallet(this.options.walletPath); if (!localWallet) { throw new LocalError("No active wallet is available for EIP-191 signing", "signing EIP-191 identity-link proof", { code: "IDENTITY_LINK_EOA_SIGNER_REQUIRED" }); } const account = await resolveWalletAccount(localWallet); const signature = await account.signMessage({ message }); return { address: account.address, signature }; } async saveWallet(data) { coreSaveWallet(data, this.options.walletPath); } async createWallet() { const privateKeyBytes = randomBytes(32); const privateKey = `0x${privateKeyBytes.toString("hex")}`; const ecdh = createECDH("secp256k1"); ecdh.setPrivateKey(privateKeyBytes); const uncompressedPubKey = ecdh.getPublicKey(); // Strip the 04 uncompressed-point prefix before hashing. const pubKeyBody = uncompressedPubKey.subarray(1); const hash = keccak_256(pubKeyBody); const addressBytes = hash.slice(-20); const address = `0x${Buffer.from(addressBytes).toString("hex")}`; return { address, privateKey, created: new Date().toISOString(), funded: false, }; } getWalletPath() { return this.options.walletPath ?? coreGetWalletPath(); } getProjectCredentialCacheInfo() { const profile = getActiveProfile(); return { source: "local_cache", cache_path: this.options.keystorePath ?? coreGetProjectCredentialsPath(), wallet: profile, profile, }; } getProfileStatePath() { return this.options.profileStatePath ?? coreGetProfileStatePath(); } activeScope() { const localWallet = coreReadWallet(this.options.walletPath); return { api_base: getApiBase(), profile: getActiveProfile(), principal: localWallet?.address ?? null, }; } async getWalletIdentity() { const name = getActiveProfile(); const meta = readMeta(name); let address = meta?.address ?? null; if (!address) { address = coreReadWallet(this.options.walletPath)?.address ?? null; } return { name, address, label: meta?.label ?? null }; } } //# sourceMappingURL=credentials.js.map