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.
200 lines • 10.9 kB
JavaScript
import { parse, evaluate } from "@humanwhocodes/momoa";
import { schnorr, secp256k1 } from "@noble/curves/secp256k1.js";
import { sha256 } from "@noble/hashes/sha2.js";
import { bytesToHex, hexToBytes, utf8ToBytes } from "@noble/hashes/utils.js";
import { bech32 } from "@scure/base";
import { canonicalize } from "json-canonicalize";
import { LocalError } from "../errors.js";
const HEX_32 = /^[0-9a-f]{64}$/;
const HEX_64 = /^[0-9a-f]{128}$/;
const EIP_SIGNATURE = /^0x[0-9a-f]{130}$/;
const HALF_N = secp256k1.Point.Fn.ORDER >> 1n;
const SECRET_FIELD = /(?:private.?key|secret|mnemonic|seed|derivation|nsec|nostr.?key)/i;
const PAYLOAD_FIELDS = [
"action", "audience", "challenge_expires_at", "challenge_id", "issued_at", "nonce",
"nostr_event_kind", "nostr_pubkey", "principal_id", "protocol", "visibility",
"wallet_account", "wallet_signature_scheme",
];
const MAX_BODY_BYTES = 32 * 1024;
const MAX_CONTENT_BYTES = 8 * 1024;
const MAX_PAYLOAD_BYTES = 4 * 1024;
const encoder = new TextEncoder();
function fail(message, code = "IDENTITY_LINK_INVALID_EVENT", details) {
throw new LocalError(message, "preparing Nostr identity link", { code, details });
}
function scalar(text, field) {
for (let i = 0; i < text.length; i += 1) {
const code = text.charCodeAt(i);
if (code >= 0xd800 && code <= 0xdbff) {
const next = text.charCodeAt(i + 1);
if (!(next >= 0xdc00 && next <= 0xdfff))
fail("Unpaired Unicode surrogate", "IDENTITY_LINK_INVALID_JSON", { field });
i += 1;
}
else if (code >= 0xdc00 && code <= 0xdfff)
fail("Unpaired Unicode surrogate", "IDENTITY_LINK_INVALID_JSON", { field });
}
}
function inspect(node, path = "$") {
if (node.type === "Document")
return inspect(node.body, path);
if (node.type === "Object") {
const seen = new Set();
for (const member of node.members) {
const name = member.name.type === "String" ? member.name.value : member.name.name;
if (seen.has(name))
fail(`Duplicate JSON field: ${name}`, "IDENTITY_LINK_DUPLICATE_FIELD", { field: `${path}.${name}` });
if (SECRET_FIELD.test(name))
fail("Secret-bearing identity-link fields are forbidden", "IDENTITY_LINK_SECRET_INPUT_FORBIDDEN", { field: `${path}.${name}` });
seen.add(name);
inspect(member.value, `${path}.${name}`);
}
return;
}
if (node.type === "Array")
return node.elements.forEach((entry, i) => inspect(entry.value, `${path}[${i}]`));
if (node.type === "String")
scalar(node.value, path);
}
export function parseStrictJson(text) {
if (text.startsWith("\ufeff"))
fail("UTF-8 BOM is forbidden", "IDENTITY_LINK_INVALID_JSON");
let ast;
try {
ast = parse(text, { mode: "json", allowTrailingCommas: false });
}
catch {
fail("Invalid JSON", "IDENTITY_LINK_INVALID_JSON");
}
inspect(ast);
return evaluate(ast);
}
function object(value, field) {
if (!value || typeof value !== "object" || Array.isArray(value))
fail(`${field} must be an object`, "IDENTITY_LINK_INVALID_JSON", { field });
return value;
}
function exact(value, fields, path) {
const expected = new Set(fields);
for (const key of Object.keys(value))
if (!expected.has(key))
fail(`Unknown field: ${key}`, "IDENTITY_LINK_UNKNOWN_FIELD", { field: `${path}.${key}` });
for (const key of fields)
if (!(key in value))
fail(`Missing field: ${key}`, "IDENTITY_LINK_INVALID_JSON", { field: `${path}.${key}` });
}
function hash(text) {
return sha256(utf8ToBytes(text));
}
function byteLength(text) {
return encoder.encode(text).byteLength;
}
export function normalizeNostrPublicKey(input) {
if (HEX_32.test(input))
return input;
if (typeof input !== "string" || input !== input.toLowerCase() || !input.startsWith("npub1")) {
fail("Nostr public key must be lowercase 64-character hex or canonical npub", "IDENTITY_LINK_INVALID_PAYLOAD");
}
try {
const decoded = bech32.decode(input, 1000);
const bytes = new Uint8Array(bech32.fromWords(decoded.words));
if (decoded.prefix !== "npub" || bytes.length !== 32 || bech32.encode("npub", bech32.toWords(bytes), 1000) !== input) {
fail("Nostr public key must be lowercase 64-character hex or canonical npub", "IDENTITY_LINK_INVALID_PAYLOAD");
}
return input;
}
catch {
fail("Nostr public key must be lowercase 64-character hex or canonical npub", "IDENTITY_LINK_INVALID_PAYLOAD");
}
}
function parseCanonicalPublicPayload(publicPayload) {
if (byteLength(publicPayload) > MAX_PAYLOAD_BYTES)
fail("public_payload exceeds 4096 bytes", "IDENTITY_LINK_INVALID_PAYLOAD");
const payload = object(parseStrictJson(publicPayload), "public_payload");
exact(payload, PAYLOAD_FIELDS, "public_payload");
if (canonicalize(payload) !== publicPayload)
fail("public_payload is not exact RFC 8785 JSON", "IDENTITY_LINK_NONCANONICAL");
if (payload.action !== "link_external_identity" ||
payload.audience !== "https://api.run402.com/identity-links/v1" ||
payload.protocol !== "run402.identity-link.nostr.v1" ||
payload.visibility !== "public" ||
payload.wallet_signature_scheme !== "eip191_personal_sign" ||
payload.nostr_event_kind !== 1 ||
typeof payload.nostr_pubkey !== "string" || !HEX_32.test(payload.nostr_pubkey) ||
typeof payload.nonce !== "string" || !/^[A-Za-z0-9_-]{43}$/.test(payload.nonce) ||
typeof payload.challenge_id !== "string" || !/^ilc_[A-Za-z0-9]+$/.test(payload.challenge_id) ||
typeof payload.principal_id !== "string" || !/^prin_[A-Za-z0-9]+$/.test(payload.principal_id) ||
typeof payload.issued_at !== "string" || !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(payload.issued_at) ||
typeof payload.challenge_expires_at !== "string" || !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(payload.challenge_expires_at) ||
Number.isNaN(Date.parse(payload.issued_at)) || Number.isNaN(Date.parse(payload.challenge_expires_at)) ||
typeof payload.wallet_account !== "string" || !/^eip155:8453:0x[0-9A-Fa-f]{40}$/.test(payload.wallet_account))
fail("public_payload does not match run402.identity-link.nostr.v1", "IDENTITY_LINK_INVALID_PAYLOAD");
return payload;
}
export function canonicalProofContent(publicPayload, walletSignature) {
parseCanonicalPublicPayload(publicPayload);
assertCanonicalWalletSignature(walletSignature);
return canonicalize({ public_payload: publicPayload, wallet_signature: walletSignature });
}
export function assertCanonicalWalletSignature(signature) {
if (!EIP_SIGNATURE.test(signature))
fail("Wallet signature must be lowercase 65-byte 0x-prefixed hex", "IDENTITY_LINK_INVALID_WALLET_SIGNATURE");
const s = BigInt(`0x${signature.slice(66, 130)}`);
const v = signature.slice(130, 132);
if (s === 0n || s > HALF_N || (v !== "1b" && v !== "1c"))
fail("Wallet signature must be low-s with v 0x1b or 0x1c", "IDENTITY_LINK_INVALID_WALLET_SIGNATURE");
}
export function walletFromPublicPayload(publicPayload) {
const payload = parseCanonicalPublicPayload(publicPayload);
const account = payload.wallet_account;
if (typeof account !== "string" || !/^eip155:8453:0x[0-9A-Fa-f]{40}$/.test(account))
fail("Invalid wallet_account", "IDENTITY_LINK_INVALID_PAYLOAD");
return account.slice("eip155:8453:".length);
}
export function verifyRawNostrEvent(input) {
if (typeof input === "string" && byteLength(input) > MAX_BODY_BYTES)
fail("Raw Nostr event exceeds 32768 bytes", "IDENTITY_LINK_INVALID_JSON");
const raw = typeof input === "string" ? parseStrictJson(input) : input;
const value = object(raw, "nostr_event");
exact(value, ["id", "pubkey", "created_at", "kind", "tags", "content", "sig"], "nostr_event");
const event = value;
if (!HEX_32.test(event.id) || !HEX_32.test(event.pubkey) || !HEX_64.test(event.sig) || event.kind !== 1 || !Number.isSafeInteger(event.created_at) || event.created_at < 0 || !Array.isArray(event.tags) || typeof event.content !== "string") {
fail("Nostr event must be the exact seven-field kind-1 envelope");
}
if (byteLength(event.content) > MAX_CONTENT_BYTES)
fail("Nostr event content exceeds 8192 bytes");
for (const [index, tag] of event.tags.entries()) {
if (!Array.isArray(tag) || tag.some((item) => typeof item !== "string"))
fail("Every Nostr tag must contain strings only", "IDENTITY_LINK_INVALID_EVENT", { field: `nostr_event.tags[${index}]` });
}
if (event.tags.length > 1)
fail("Only an empty tag list or one NIP-OA auth tag is accepted");
if (event.tags.length === 1) {
const tag = event.tags[0];
if (!Array.isArray(tag) || tag.length !== 4 || tag[0] !== "auth" || !HEX_32.test(tag[1] ?? "") || tag[2] !== "" || !HEX_64.test(tag[3] ?? "") || tag[1] === event.pubkey) {
fail("Invalid NIP-OA owner-attestation tag", "IDENTITY_LINK_INVALID_NIP_OA");
}
if (!schnorr.verify(hexToBytes(tag[3]), hash(`nostr:agent-auth:${event.pubkey}:`), hexToBytes(tag[1])))
fail("NIP-OA owner attestation does not verify", "IDENTITY_LINK_INVALID_NIP_OA");
}
const wrapper = object(parseStrictJson(event.content), "nostr_event.content");
exact(wrapper, ["public_payload", "wallet_signature"], "nostr_event.content");
if (canonicalize(wrapper) !== event.content || typeof wrapper.public_payload !== "string" || typeof wrapper.wallet_signature !== "string")
fail("Event content is not the canonical two-field proof wrapper", "IDENTITY_LINK_NONCANONICAL");
assertCanonicalWalletSignature(wrapper.wallet_signature);
const payload = parseCanonicalPublicPayload(wrapper.public_payload);
if (payload.nostr_pubkey !== event.pubkey)
fail("Event author and public_payload do not match", "IDENTITY_LINK_INVALID_PAYLOAD");
const eventId = bytesToHex(hash(JSON.stringify([0, event.pubkey, event.created_at, event.kind, event.tags, event.content])));
if (eventId !== event.id || !schnorr.verify(hexToBytes(event.sig), hexToBytes(event.id), hexToBytes(event.pubkey)))
fail("Nostr event id or BIP-340 signature is invalid");
return event;
}
export function assertNoForbiddenIdentityInput(value) {
for (const key of Object.keys(value)) {
if (SECRET_FIELD.test(key) || ["display_name", "label", "signed_label"].includes(key)) {
fail("Nostr private keys, derivation material, and signed labels are never accepted", "IDENTITY_LINK_SECRET_INPUT_FORBIDDEN", { field: key });
}
}
}
//# sourceMappingURL=identity-links.protocol.js.map