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.
212 lines • 9.66 kB
JavaScript
/**
* 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 { getWalletAuthHeaders } from "../../core-dist/wallet-auth.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;
const wallet = getWalletAuthHeaders(path, this.options.walletPath);
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 { privateKeyToAccount } = await import("viem/accounts");
const account = privateKeyToAccount(localWallet.privateKey);
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