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.

80 lines • 4.19 kB
import { LocalError } from "../errors.js"; import { assertNoForbiddenIdentityInput, canonicalProofContent, normalizeNostrPublicKey, verifyRawNostrEvent, walletFromPublicPayload, } from "./identity-links.protocol.js"; export class NostrIdentityLinks { client; constructor(client) { this.client = client; } async begin(input) { if (!input || typeof input !== "object") throw new LocalError("identityLinks.nostr.begin requires an input object", "beginning Nostr identity link"); assertNoForbiddenIdentityInput(input); if (typeof input.nostrPubkey !== "string" || input.visibility !== "public") { throw new LocalError("nostrPubkey and explicit visibility: 'public' are required", "beginning Nostr identity link", { code: "IDENTITY_LINK_PUBLIC_VISIBILITY_REQUIRED" }); } const nostrPubkey = normalizeNostrPublicKey(input.nostrPubkey); const signer = this.client.credentials.signPersonalMessage; if (!signer) throw new LocalError("The active credential provider cannot sign EIP-191 personal messages", "beginning Nostr identity link", { code: "IDENTITY_LINK_EOA_SIGNER_REQUIRED" }); const challenge = await this.client.request("/identity-links/v1/challenges", { method: "POST", headers: { "Idempotency-Key": input.idempotencyKey ?? globalThis.crypto.randomUUID() }, body: { nostr_pubkey: nostrPubkey, visibility: input.visibility }, context: "creating public Nostr identity-link challenge", }); const signed = await signer.call(this.client.credentials, challenge.public_payload); const expected = walletFromPublicPayload(challenge.public_payload); if (signed.address.toLowerCase() !== expected.toLowerCase()) { throw new LocalError("The personal-message signer differs from the EOA that authenticated the challenge", "beginning Nostr identity link", { code: "IDENTITY_LINK_EOA_SIGNER_MISMATCH" }); } const proofContent = canonicalProofContent(challenge.public_payload, signed.signature); return { ...challenge, wallet_address: signed.address, wallet_signature: signed.signature, proof_content: proofContent, }; } async complete(input) { if (!input || !("rawEvent" in input)) throw new LocalError("identityLinks.nostr.complete requires { rawEvent }", "completing Nostr identity link"); const event = verifyRawNostrEvent(input.rawEvent); const wrapper = JSON.parse(event.content); const payload = JSON.parse(wrapper.public_payload); if (typeof payload.challenge_id !== "string") throw new LocalError("Signed payload has no challenge_id", "completing Nostr identity link"); return this.client.request("/identity-links/v1", { method: "POST", body: { identity_link_challenge_id: payload.challenge_id, nostr_event: event }, context: "completing public Nostr identity link", }); } } export class IdentityLinks { client; nostr; constructor(client) { this.client = client; this.nostr = new NostrIdentityLinks(client); } async list() { return this.client.request("/identity-links/v1", { context: "listing linked identities" }); } async getProof(identityLinkId) { if (!identityLinkId) throw new LocalError("identityLinks.getProof requires identityLinkId", "reading public identity-link proof"); return this.client.request(`/identity-link-proofs/v1/${encodeURIComponent(identityLinkId)}`, { withAuth: false, context: "reading public identity-link proof", }); } async revoke(identityLinkId) { if (!identityLinkId) throw new LocalError("identityLinks.revoke requires identityLinkId", "revoking identity link"); return this.client.request(`/identity-links/v1/${encodeURIComponent(identityLinkId)}`, { method: "DELETE", context: "revoking identity link", }); } } //# sourceMappingURL=identity-links.js.map