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.

380 lines • 18.4 kB
/** * `auth` namespace — project-scoped user authentication: magic links, password * set/change, auth settings, and admin role promotion/demotion. * * Magic link + password ops use the project's anon key (they represent * end-user flows). Settings + promote/demote use the service key. */ import { gateSecret } from "../secret-gate.js"; import { LocalError } from "../errors.js"; import { requireProjectCredentials } from "../project-credentials.js"; import { assertEmailAddress, assertHttpUrl, assertNonEmptyString, assertStringInSet, } from "../validation.js"; const MAGIC_LINK_INTENTS = ["signin", "invite", "claim", "recovery"]; const AUTH_SETTINGS_FIELDS = [ "allow_password_set", "preferred_sign_in_method", "public_signup", "require_passkey_for_project_admin", "allowed_email_domains", ]; const SIGN_IN_METHODS = ["password", "magic_link", "oauth_google", "passkey"]; const PUBLIC_SIGNUP_POLICIES = ["open", "known_email", "invite_only"]; function validateAuthSettings(settings) { const raw = settings; for (const key of Object.keys(raw)) { if (!AUTH_SETTINGS_FIELDS.includes(key)) { throw new LocalError(`Unknown auth settings field: ${key}`, "updating auth settings"); } } if (raw.allow_password_set !== undefined && typeof raw.allow_password_set !== "boolean") { throw new LocalError("allow_password_set must be a boolean.", "updating auth settings"); } if (raw.require_passkey_for_project_admin !== undefined && typeof raw.require_passkey_for_project_admin !== "boolean") { throw new LocalError("require_passkey_for_project_admin must be a boolean.", "updating auth settings"); } if (raw.preferred_sign_in_method !== undefined && raw.preferred_sign_in_method !== null) { assertStringInSet(raw.preferred_sign_in_method, SIGN_IN_METHODS, "preferred_sign_in_method", "updating auth settings"); } if (raw.public_signup !== undefined) { assertStringInSet(raw.public_signup, PUBLIC_SIGNUP_POLICIES, "public_signup", "updating auth settings"); } if (raw.allowed_email_domains !== undefined) { if (!Array.isArray(raw.allowed_email_domains) || !raw.allowed_email_domains.every((d) => typeof d === "string")) { throw new LocalError('allowed_email_domains must be an array of strings (e.g., ["example.com"]); pass [] to clear.', "updating auth settings"); } } } export class Auth { client; magicLink; verify; setPassword; promoteUser; demoteUser; constructor(client) { this.client = client; this.magicLink = this.requestMagicLink.bind(this); this.verify = this.verifyMagicLink.bind(this); this.setPassword = this.setUserPassword.bind(this); this.promoteUser = this.promote.bind(this); this.demoteUser = this.demote.bind(this); } /** Request a passwordless email credential. Link remains the wire default. */ async requestMagicLink(projectId, opts) { if (!opts || typeof opts !== "object" || Array.isArray(opts)) { throw new LocalError("r.auth.requestMagicLink(projectId, opts) requires an opts object as the 2nd argument (e.g., { email, redirectUrl })", "requesting magic link"); } assertEmailAddress(opts.email, "email", "requesting magic link"); const delivery = opts.delivery ?? "link"; assertStringInSet(delivery, ["link", "code", "both"], "delivery", "requesting magic link"); if (delivery !== "code" || opts.redirectUrl !== undefined) { assertHttpUrl(opts.redirectUrl, "redirectUrl", "requesting magic link"); } if (opts.intent !== undefined) { assertStringInSet(opts.intent, MAGIC_LINK_INTENTS, "intent", "requesting magic link"); } const project = await requireProjectCredentials(this.client, projectId, "requesting magic link"); const body = { email: opts.email, }; if (opts.redirectUrl !== undefined) body.redirect_url = opts.redirectUrl; if (opts.delivery !== undefined) body.delivery = opts.delivery; if (opts.intent) body.intent = opts.intent; if (opts.clientState !== undefined) body.client_state = opts.clientState; const result = await this.client.request("/auth/v1/magic-link", { method: "POST", headers: { apikey: project.anon_key, Authorization: `Bearer ${opts.intent === "invite" ? project.service_key : project.anon_key}`, }, body, context: "requesting magic link", }); return { message: result.message, ...(result.warnings ? { warnings: result.warnings } : {}), ...(result.challenge_id ? { challengeId: result.challenge_id } : {}), }; } /** Exchange a magic-link token for access + refresh tokens. */ async verifyMagicLink(projectId, token) { gateSecret(this.client, "auth.verifyMagicLink", projectId, token); assertNonEmptyString(token, "token", "verifying magic link"); const project = await requireProjectCredentials(this.client, projectId, "verifying magic link"); return this.client.request("/auth/v1/token?grant_type=magic_link", { method: "POST", headers: { apikey: project.anon_key, Authorization: `Bearer ${project.anon_key}`, }, body: { token }, context: "verifying magic link", }); } /** Exchange an opaque challenge handle plus a six-digit code. Never auto-retried. */ async verifyEmailCode(projectId, opts) { gateSecret(this.client, "auth.verifyEmailCode", projectId, opts); if (!opts || typeof opts !== "object" || Array.isArray(opts)) { throw new LocalError("r.auth.verifyEmailCode(projectId, opts) requires { challengeId, code }", "verifying email code"); } if (typeof opts.challengeId !== "string" || !/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(opts.challengeId)) { throw new LocalError("challengeId must be a UUID.", "verifying email code"); } if (typeof opts.code !== "string" || !/^\d{6}$/.test(opts.code)) { throw new LocalError("code must be exactly six digits.", "verifying email code"); } const project = await requireProjectCredentials(this.client, projectId, "verifying email code"); return this.client.request("/auth/v1/token?grant_type=email_code", { method: "POST", headers: { apikey: project.anon_key, Authorization: `Bearer ${project.anon_key}`, }, body: { challenge_id: opts.challengeId, code: opts.code }, context: "verifying email code", }); } /** * Set / change / reset the authenticated user's password. The caller's * `accessToken` (from `verifyMagicLink` or a prior login) is used as the * Bearer credential. */ async setUserPassword(projectId, opts) { gateSecret(this.client, "auth.setUserPassword", projectId, opts); if (!opts || typeof opts !== "object" || Array.isArray(opts)) { throw new LocalError("r.auth.setUserPassword(projectId, opts) requires an opts object as the 2nd argument (e.g., { accessToken, newPassword })", "setting user password"); } assertNonEmptyString(opts.accessToken, "accessToken", "setting user password"); assertNonEmptyString(opts.newPassword, "newPassword", "setting user password"); if (opts.currentPassword !== undefined) { assertNonEmptyString(opts.currentPassword, "currentPassword", "setting user password"); } const project = await requireProjectCredentials(this.client, projectId, "setting user password"); const body = { new_password: opts.newPassword }; if (opts.currentPassword !== undefined) body.current_password = opts.currentPassword; await this.client.request("/auth/v1/user/password", { method: "POST", headers: { apikey: project.anon_key, Authorization: `Bearer ${opts.accessToken}`, }, body, context: "setting user password", }); } /** Update project-level auth settings. Requires service key. */ async settings(projectId, settings) { if (!settings || typeof settings !== "object" || Array.isArray(settings)) { throw new LocalError("r.auth.settings(projectId, settings) requires a settings object as the 2nd argument (e.g., { allow_password_set: true })", "updating auth settings"); } validateAuthSettings(settings); const project = await requireProjectCredentials(this.client, projectId, "updating auth settings"); return this.client.request("/auth/v1/settings", { method: "PATCH", headers: { apikey: project.anon_key, Authorization: `Bearer ${project.service_key}`, }, body: settings, context: "updating auth settings", }); } /** Create or update an auth user. Requires service key. */ async createUser(projectId, opts) { if (!opts || typeof opts !== "object" || Array.isArray(opts)) { throw new LocalError("r.auth.createUser(projectId, opts) requires an opts object as the 2nd argument (e.g., { email, isAdmin })", "creating auth user"); } assertEmailAddress(opts.email, "email", "creating auth user"); if (opts.redirectUrl !== undefined) { assertHttpUrl(opts.redirectUrl, "redirectUrl", "creating auth user"); } if (opts.isAdmin !== undefined && typeof opts.isAdmin !== "boolean") { throw new LocalError("isAdmin must be a boolean when provided.", "creating auth user"); } if (opts.sendInvite !== undefined && typeof opts.sendInvite !== "boolean") { throw new LocalError("sendInvite must be a boolean when provided.", "creating auth user"); } const project = await requireProjectCredentials(this.client, projectId, "creating auth user"); const body = { email: opts.email }; if (typeof opts.isAdmin === "boolean") body.is_admin = opts.isAdmin; if (typeof opts.sendInvite === "boolean") body.send_invite = opts.sendInvite; if (opts.redirectUrl !== undefined) body.redirect_url = opts.redirectUrl; if (opts.clientState !== undefined) body.client_state = opts.clientState; return this.client.request("/auth/v1/admin/users", { method: "POST", headers: { apikey: project.anon_key, Authorization: `Bearer ${project.service_key}`, }, body, context: "creating auth user", }); } /** Create/update an auth user and send a trusted invite. Requires service key. */ async inviteUser(projectId, opts) { return this.createUser(projectId, { ...opts, sendInvite: true }); } /** Create WebAuthn registration options for the authenticated user. */ async createPasskeyRegistrationOptions(projectId, opts) { gateSecret(this.client, "auth.createPasskeyRegistrationOptions", projectId, opts); if (!opts || typeof opts !== "object") { throw new LocalError("r.auth.createPasskeyRegistrationOptions(projectId, opts) requires { accessToken, appOrigin }", "creating passkey registration options"); } const project = await requireProjectCredentials(this.client, projectId, "creating passkey registration options"); return this.client.request("/auth/v1/passkeys/register/options", { method: "POST", headers: { apikey: project.anon_key, Authorization: `Bearer ${opts.accessToken}`, }, body: { app_origin: opts.appOrigin }, context: "creating passkey registration options", }); } /** Verify and store a WebAuthn passkey registration. */ async verifyPasskeyRegistration(projectId, opts) { gateSecret(this.client, "auth.verifyPasskeyRegistration", projectId, opts); if (!opts || typeof opts !== "object") { throw new LocalError("r.auth.verifyPasskeyRegistration(projectId, opts) requires { accessToken, challengeId, response }", "verifying passkey registration"); } const project = await requireProjectCredentials(this.client, projectId, "verifying passkey registration"); const body = { challenge_id: opts.challengeId, response: opts.response, }; if (opts.label) body.label = opts.label; return this.client.request("/auth/v1/passkeys/register/verify", { method: "POST", headers: { apikey: project.anon_key, Authorization: `Bearer ${opts.accessToken}`, }, body, context: "verifying passkey registration", }); } /** Create WebAuthn login options. */ async createPasskeyLoginOptions(projectId, opts) { if (!opts || typeof opts !== "object") { throw new LocalError("r.auth.createPasskeyLoginOptions(projectId, opts) requires { appOrigin }", "creating passkey login options"); } const project = await requireProjectCredentials(this.client, projectId, "creating passkey login options"); const body = { app_origin: opts.appOrigin }; if (opts.email) body.email = opts.email; return this.client.request("/auth/v1/passkeys/login/options", { method: "POST", headers: { apikey: project.anon_key, Authorization: `Bearer ${project.anon_key}`, }, body, context: "creating passkey login options", }); } /** Verify a WebAuthn login assertion and return a normal auth session. */ async verifyPasskeyLogin(projectId, opts) { gateSecret(this.client, "auth.verifyPasskeyLogin", projectId, opts); if (!opts || typeof opts !== "object") { throw new LocalError("r.auth.verifyPasskeyLogin(projectId, opts) requires { challengeId, response }", "verifying passkey login"); } const project = await requireProjectCredentials(this.client, projectId, "verifying passkey login"); return this.client.request("/auth/v1/passkeys/login/verify", { method: "POST", headers: { apikey: project.anon_key, Authorization: `Bearer ${project.anon_key}`, }, body: { challenge_id: opts.challengeId, response: opts.response, }, context: "verifying passkey login", }); } /** List the authenticated user's active passkeys. */ async listPasskeys(projectId, opts) { gateSecret(this.client, "auth.listPasskeys", projectId, opts); if (!opts || typeof opts !== "object") { throw new LocalError("r.auth.listPasskeys(projectId, opts) requires { accessToken }", "listing passkeys"); } const project = await requireProjectCredentials(this.client, projectId, "listing passkeys"); return this.client.request("/auth/v1/passkeys", { headers: { apikey: project.anon_key, Authorization: `Bearer ${opts.accessToken}`, }, context: "listing passkeys", withAuth: false, }); } /** Delete one authenticated-user passkey by id. */ async deletePasskey(projectId, opts) { gateSecret(this.client, "auth.deletePasskey", projectId, opts); if (!opts || typeof opts !== "object") { throw new LocalError("r.auth.deletePasskey(projectId, opts) requires { accessToken, passkeyId }", "deleting passkey"); } const project = await requireProjectCredentials(this.client, projectId, "deleting passkey"); await this.client.request(`/auth/v1/passkeys/${encodeURIComponent(opts.passkeyId)}`, { method: "DELETE", headers: { apikey: project.anon_key, Authorization: `Bearer ${opts.accessToken}`, }, context: "deleting passkey", }); } /** List configured auth providers for a project. Uses the project's anon key. */ async providers(projectId) { const project = await requireProjectCredentials(this.client, projectId, "listing auth providers"); const result = await this.client.request("/auth/v1/providers", { headers: { apikey: project.anon_key, Authorization: `Bearer ${project.anon_key}`, }, context: "listing auth providers", withAuth: false, }); return { ...result, magic_link: { enabled: result.magic_link.enabled, deliveryModes: result.magic_link.delivery_modes ?? ["link"], }, }; } /** Promote a user (by email) to `project_admin`. Requires service key. */ async promote(projectId, email) { const project = await requireProjectCredentials(this.client, projectId, "promoting user"); await this.client.request(`/projects/v1/admin/${projectId}/promote-user`, { method: "POST", headers: { Authorization: `Bearer ${project.service_key}` }, body: { email }, context: "promoting user", }); } /** Demote a user (by email) from `project_admin` back to the default role. */ async demote(projectId, email) { const project = await requireProjectCredentials(this.client, projectId, "demoting user"); await this.client.request(`/projects/v1/admin/${projectId}/demote-user`, { method: "POST", headers: { Authorization: `Bearer ${project.service_key}` }, body: { email }, context: "demoting user", }); } } //# sourceMappingURL=auth.js.map