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
JavaScript
/**
* `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