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
JavaScript
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