{"version":3,"file":"index.mjs","names":["TOKEN_EXPIRY_MS","TOKEN_EXPIRY_MS","timingDelay","_authConfigSchema"],"sources":["../src/config.ts","../src/rbac.ts","../src/invite.ts","../src/magic-link/index.ts","../src/signup.ts","../src/oauth/consumer.ts","../src/index.ts"],"sourcesContent":["/**\n * Configuration schema for @emdash-cms/auth\n */\n\nimport { z } from \"zod\";\n\nimport type { RoleName } from \"./types.js\";\n\n/** Matches http(s) scheme at start of URL */\nconst HTTP_SCHEME_RE = /^https?:\\/\\//i;\n\n/** Validates that a URL string uses http or https scheme. Rejects javascript:/data: URI XSS vectors. */\nconst httpUrl = z\n\t.string()\n\t.url()\n\t.refine((url) => HTTP_SCHEME_RE.test(url), \"URL must use http or https\");\n\n/**\n * OAuth provider configuration\n */\nconst oauthProviderSchema = z.object({\n\tclientId: z.string(),\n\tclientSecret: z.string(),\n});\n\n/**\n * Full auth configuration schema\n */\nexport const authConfigSchema = z.object({\n\t/**\n\t * Secret key for encrypting tokens and session data.\n\t * Generate with: `emdash auth secret`\n\t */\n\tsecret: z.string().min(32, \"Auth secret must be at least 32 characters\"),\n\n\t/**\n\t * Passkey (WebAuthn) configuration\n\t */\n\tpasskeys: z\n\t\t.object({\n\t\t\t/**\n\t\t\t * Relying party name shown to users during passkey registration\n\t\t\t */\n\t\t\trpName: z.string(),\n\t\t\t/**\n\t\t\t * Relying party ID (domain). Defaults to the hostname from baseUrl.\n\t\t\t */\n\t\t\trpId: z.string().optional(),\n\t\t})\n\t\t.optional(),\n\n\t/**\n\t * Self-signup configuration\n\t */\n\tselfSignup: z\n\t\t.object({\n\t\t\t/**\n\t\t\t * Email domains allowed to self-register\n\t\t\t */\n\t\t\tdomains: z.array(z.string()),\n\t\t\t/**\n\t\t\t * Default role for self-registered users\n\t\t\t */\n\t\t\tdefaultRole: z.enum([\"subscriber\", \"contributor\", \"author\"] as const).default(\"contributor\"),\n\t\t})\n\t\t.optional(),\n\n\t/**\n\t * OAuth provider configurations (for \"Login with X\")\n\t */\n\toauth: z\n\t\t.object({\n\t\t\tgithub: oauthProviderSchema.optional(),\n\t\t\tgoogle: oauthProviderSchema.optional(),\n\t\t})\n\t\t.optional(),\n\n\t/**\n\t * Configure EmDash as an OAuth provider\n\t */\n\tprovider: z\n\t\t.object({\n\t\t\tenabled: z.boolean(),\n\t\t\t/**\n\t\t\t * Issuer URL for OIDC. Defaults to site URL.\n\t\t\t */\n\t\t\tissuer: httpUrl.optional(),\n\t\t})\n\t\t.optional(),\n\n\t/**\n\t * Enterprise SSO configuration\n\t */\n\tsso: z\n\t\t.object({\n\t\t\tenabled: z.boolean(),\n\t\t})\n\t\t.optional(),\n\n\t/**\n\t * Session configuration\n\t */\n\tsession: z\n\t\t.object({\n\t\t\t/**\n\t\t\t * Session max age in seconds. Default: 30 days\n\t\t\t */\n\t\t\tmaxAge: z.number().default(30 * 24 * 60 * 60),\n\t\t\t/**\n\t\t\t * Extend session on activity. Default: true\n\t\t\t */\n\t\t\tsliding: z.boolean().default(true),\n\t\t})\n\t\t.optional(),\n});\n\nexport type AuthConfig = z.infer<typeof authConfigSchema>;\n\n/**\n * Validated and resolved auth configuration\n */\nexport interface ResolvedAuthConfig {\n\tsecret: string;\n\tbaseUrl: string;\n\tsiteName: string;\n\n\tpasskeys: {\n\t\trpName: string;\n\t\trpId: string;\n\t\torigin: string;\n\t};\n\n\tselfSignup?: {\n\t\tdomains: string[];\n\t\tdefaultRole: RoleName;\n\t};\n\n\toauth?: {\n\t\tgithub?: {\n\t\t\tclientId: string;\n\t\t\tclientSecret: string;\n\t\t};\n\t\tgoogle?: {\n\t\t\tclientId: string;\n\t\t\tclientSecret: string;\n\t\t};\n\t};\n\n\tprovider?: {\n\t\tenabled: boolean;\n\t\tissuer: string;\n\t};\n\n\tsso?: {\n\t\tenabled: boolean;\n\t};\n\n\tsession: {\n\t\tmaxAge: number;\n\t\tsliding: boolean;\n\t};\n}\n\nconst selfSignupRoleMap: Record<\"subscriber\" | \"contributor\" | \"author\", RoleName> = {\n\tsubscriber: \"SUBSCRIBER\",\n\tcontributor: \"CONTRIBUTOR\",\n\tauthor: \"AUTHOR\",\n};\n\n/**\n * Resolve auth configuration with defaults\n */\nexport function resolveConfig(\n\tconfig: AuthConfig,\n\tbaseUrl: string,\n\tsiteName: string,\n): ResolvedAuthConfig {\n\tconst url = new URL(baseUrl);\n\n\treturn {\n\t\tsecret: config.secret,\n\t\tbaseUrl,\n\t\tsiteName,\n\n\t\tpasskeys: {\n\t\t\trpName: config.passkeys?.rpName ?? siteName,\n\t\t\trpId: config.passkeys?.rpId ?? url.hostname,\n\t\t\torigin: url.origin,\n\t\t},\n\n\t\tselfSignup: config.selfSignup\n\t\t\t? {\n\t\t\t\t\tdomains: config.selfSignup.domains.map((d) => d.toLowerCase()),\n\t\t\t\t\tdefaultRole: selfSignupRoleMap[config.selfSignup.defaultRole],\n\t\t\t\t}\n\t\t\t: undefined,\n\n\t\toauth: config.oauth,\n\n\t\tprovider: config.provider\n\t\t\t? {\n\t\t\t\t\tenabled: config.provider.enabled,\n\t\t\t\t\tissuer: config.provider.issuer ?? baseUrl,\n\t\t\t\t}\n\t\t\t: undefined,\n\n\t\tsso: config.sso,\n\n\t\tsession: {\n\t\t\tmaxAge: config.session?.maxAge ?? 30 * 24 * 60 * 60,\n\t\t\tsliding: config.session?.sliding ?? true,\n\t\t},\n\t};\n}\n","/**\n * Role-Based Access Control\n */\n\nimport type { ApiTokenScope } from \"./tokens.js\";\nimport { Role, type RoleLevel } from \"./types.js\";\n\n/**\n * Permission definitions with minimum role required\n */\nexport const Permissions = {\n\t// Content\n\t\"content:read\": Role.SUBSCRIBER,\n\t// content:read_drafts gates non-published content (drafts, scheduled, trash)\n\t// and editor-only views (revisions, compare, preview-url). Subscribers may\n\t// hold content:read for member-only published content but must not see\n\t// drafts.\n\t\"content:read_drafts\": Role.CONTRIBUTOR,\n\t\"content:create\": Role.CONTRIBUTOR,\n\t\"content:edit_own\": Role.AUTHOR,\n\t\"content:edit_any\": Role.EDITOR,\n\t\"content:delete_own\": Role.AUTHOR,\n\t\"content:delete_any\": Role.EDITOR,\n\t// Permanent deletion (empty trash) is irreversible and bypasses the\n\t// soft-delete safety net, so it sits at the same authorization tier as\n\t// other destructive system actions (schema:manage, comments:delete).\n\t\"content:delete_permanent\": Role.ADMIN,\n\t\"content:publish_own\": Role.AUTHOR,\n\t\"content:publish_any\": Role.EDITOR,\n\n\t// Media\n\t\"media:read\": Role.SUBSCRIBER,\n\t\"media:upload\": Role.CONTRIBUTOR,\n\t\"media:edit_own\": Role.AUTHOR,\n\t\"media:edit_any\": Role.EDITOR,\n\t\"media:delete_own\": Role.AUTHOR,\n\t\"media:delete_any\": Role.EDITOR,\n\n\t// Taxonomies\n\t\"taxonomies:read\": Role.SUBSCRIBER,\n\t\"taxonomies:manage\": Role.EDITOR,\n\n\t// Comments\n\t\"comments:read\": Role.SUBSCRIBER,\n\t\"comments:moderate\": Role.EDITOR,\n\t\"comments:delete\": Role.ADMIN,\n\t\"comments:settings\": Role.ADMIN,\n\n\t// Menus\n\t\"menus:read\": Role.SUBSCRIBER,\n\t\"menus:manage\": Role.EDITOR,\n\n\t// Bylines\n\t\"bylines:read\": Role.SUBSCRIBER,\n\t\"bylines:manage\": Role.EDITOR,\n\n\t// Widgets\n\t\"widgets:read\": Role.SUBSCRIBER,\n\t\"widgets:manage\": Role.EDITOR,\n\n\t// Sections\n\t\"sections:read\": Role.SUBSCRIBER,\n\t\"sections:manage\": Role.EDITOR,\n\n\t// Redirects\n\t\"redirects:read\": Role.EDITOR,\n\t\"redirects:manage\": Role.ADMIN,\n\n\t// Users\n\t\"users:read\": Role.ADMIN,\n\t\"users:invite\": Role.ADMIN,\n\t\"users:manage\": Role.ADMIN,\n\n\t// Settings\n\t\"settings:read\": Role.EDITOR,\n\t\"settings:manage\": Role.ADMIN,\n\n\t// Schema (content types)\n\t\"schema:read\": Role.EDITOR,\n\t\"schema:manage\": Role.ADMIN,\n\n\t// Plugins\n\t\"plugins:read\": Role.EDITOR,\n\t\"plugins:manage\": Role.ADMIN,\n\n\t// Import\n\t\"import:execute\": Role.ADMIN,\n\n\t// Backups (full content export — admin-only, same tier as settings:manage)\n\t\"backups:manage\": Role.ADMIN,\n\n\t// Search\n\t\"search:read\": Role.SUBSCRIBER,\n\t\"search:manage\": Role.ADMIN,\n\n\t// Auth\n\t\"auth:manage_own_credentials\": Role.SUBSCRIBER,\n\t\"auth:manage_connections\": Role.ADMIN,\n} as const;\n\nexport type Permission = keyof typeof Permissions;\n\n/**\n * Check if a user has a specific permission\n */\nexport function hasPermission(\n\tuser: { role: RoleLevel } | null | undefined,\n\tpermission: Permission,\n): boolean {\n\tif (!user) return false;\n\treturn user.role >= Permissions[permission];\n}\n\n/**\n * Require a permission, throwing if not met\n */\nexport function requirePermission(\n\tuser: { role: RoleLevel } | null | undefined,\n\tpermission: Permission,\n): asserts user is { role: RoleLevel } {\n\tif (!user) {\n\t\tthrow new PermissionError(\"unauthorized\", \"Authentication required\");\n\t}\n\tif (!hasPermission(user, permission)) {\n\t\tthrow new PermissionError(\"forbidden\", `Missing permission: ${permission}`);\n\t}\n}\n\n/**\n * Check if user can perform action on a resource they own\n */\nexport function canActOnOwn(\n\tuser: { role: RoleLevel; id: string } | null | undefined,\n\townerId: string,\n\townPermission: Permission,\n\tanyPermission: Permission,\n): boolean {\n\tif (!user) return false;\n\t// Defense in depth: an empty-string ownerId means \"no recorded owner\"\n\t// (e.g. seed-imported content with `authorId: null` extracted to \"\"),\n\t// not \"owned by an unauthenticated user\". If both the user.id and the\n\t// ownerId are \"\", treating them as a match would accidentally grant\n\t// edit-own — fall through to the any-permission check instead.\n\tif (ownerId !== \"\" && user.id === ownerId) {\n\t\treturn hasPermission(user, ownPermission);\n\t}\n\treturn hasPermission(user, anyPermission);\n}\n\n/**\n * Require permission on a resource, checking ownership\n */\nexport function requirePermissionOnResource(\n\tuser: { role: RoleLevel; id: string } | null | undefined,\n\townerId: string,\n\townPermission: Permission,\n\tanyPermission: Permission,\n): asserts user is { role: RoleLevel; id: string } {\n\tif (!user) {\n\t\tthrow new PermissionError(\"unauthorized\", \"Authentication required\");\n\t}\n\tif (!canActOnOwn(user, ownerId, ownPermission, anyPermission)) {\n\t\tthrow new PermissionError(\"forbidden\", `Missing permission: ${anyPermission}`);\n\t}\n}\n\nexport class PermissionError extends Error {\n\tconstructor(\n\t\tpublic code: \"unauthorized\" | \"forbidden\",\n\t\tmessage: string,\n\t) {\n\t\tsuper(message);\n\t\tthis.name = \"PermissionError\";\n\t}\n}\n\n// ---------------------------------------------------------------------------\n// API Token Scope ↔ Role mapping\n//\n// Maps each API token scope to the minimum RBAC role required to hold it.\n// Used at token issuance time to clamp granted scopes to the user's role.\n// ---------------------------------------------------------------------------\n\n/**\n * Minimum role required for each API token scope.\n *\n * This is the authoritative mapping between the two authorization systems\n * (RBAC roles and API token scopes). When issuing a token, the granted\n * scopes must be intersected with the scopes allowed by the user's role.\n */\nconst SCOPE_MIN_ROLE: Record<Exclude<ApiTokenScope, `mcp:tools:${string}`>, RoleLevel> = {\n\t\"content:read\": Role.SUBSCRIBER,\n\t\"content:write\": Role.CONTRIBUTOR,\n\t\"media:read\": Role.SUBSCRIBER,\n\t\"media:write\": Role.CONTRIBUTOR,\n\t\"schema:read\": Role.EDITOR,\n\t\"schema:write\": Role.ADMIN,\n\t\"taxonomies:manage\": Role.EDITOR,\n\t\"menus:manage\": Role.EDITOR,\n\t\"settings:read\": Role.EDITOR,\n\t\"settings:manage\": Role.ADMIN,\n\t\"mcp:tools\": Role.ADMIN,\n\tadmin: Role.ADMIN,\n};\n\n/**\n * Return the maximum set of API token scopes a given role level may hold.\n *\n * Used at token issuance time (device flow, authorization code exchange)\n * to enforce: effective_scopes = requested_scopes ∩ scopesForRole(role).\n */\nexport function scopesForRole(role: RoleLevel): ApiTokenScope[] {\n\t// eslint-disable-next-line typescript/no-unsafe-type-assertion -- Object.entries loses tuple types; SCOPE_MIN_ROLE keys are ApiTokenScope by construction\n\tconst entries = Object.entries(SCOPE_MIN_ROLE) as [ApiTokenScope, RoleLevel][];\n\treturn entries.reduce<ApiTokenScope[]>((acc, [scope, minRole]) => {\n\t\tif (role >= minRole) acc.push(scope);\n\t\treturn acc;\n\t}, []);\n}\n\n/**\n * Clamp a set of requested scopes to those permitted by a user's role.\n *\n * Returns the intersection of `requested` and the scopes the role allows.\n * This is the central policy enforcement point: effective permissions =\n * role permissions ∩ token scopes.\n */\nexport function clampScopes(requested: string[], role: RoleLevel): string[] {\n\tconst allowed = new Set<string>(scopesForRole(role));\n\treturn requested.filter(\n\t\t(scope) => allowed.has(scope) || (scope.startsWith(\"mcp:tools:\") && role >= Role.SUBSCRIBER),\n\t);\n}\n","/**\n * Invite system for new users\n */\n\nimport { generateTokenWithHash, hashToken } from \"./tokens.js\";\nimport type { AuthAdapter, RoleLevel, EmailMessage, User } from \"./types.js\";\n\n/** Escape HTML special characters to prevent injection in email templates */\nexport function escapeHtml(s: string): string {\n\treturn s\n\t\t.replaceAll(\"&\", \"&amp;\")\n\t\t.replaceAll(\"<\", \"&lt;\")\n\t\t.replaceAll(\">\", \"&gt;\")\n\t\t.replaceAll('\"', \"&quot;\");\n}\n\nconst TOKEN_EXPIRY_MS = 7 * 24 * 60 * 60 * 1000; // 7 days\n\n/** Function that sends an email (matches the EmailPipeline.send signature) */\nexport type EmailSendFn = (message: EmailMessage) => Promise<void>;\n\nexport interface InviteConfig {\n\tbaseUrl: string;\n\tsiteName: string;\n\t/** Optional email sender. When omitted, invite URL is returned without sending. */\n\temail?: EmailSendFn;\n}\n\n/** Result of creating an invite token (without sending email) */\nexport interface InviteTokenResult {\n\t/** The complete invite URL */\n\turl: string;\n\t/** The invite email address */\n\temail: string;\n}\n\n/**\n * Create an invite token and URL without sending email.\n *\n * Validates the user doesn't already exist, generates a token, stores it,\n * and returns the invite URL. Callers decide whether to send email or\n * display the URL as a copy-link fallback.\n */\nexport async function createInviteToken(\n\tconfig: Pick<InviteConfig, \"baseUrl\">,\n\tadapter: AuthAdapter,\n\temail: string,\n\trole: RoleLevel,\n\tinvitedBy: string,\n): Promise<InviteTokenResult> {\n\t// Check if user already exists\n\tconst existing = await adapter.getUserByEmail(email);\n\tif (existing) {\n\t\tthrow new InviteError(\"user_exists\", \"A user with this email already exists\");\n\t}\n\n\t// Generate token\n\tconst { token, hash } = generateTokenWithHash();\n\n\t// Store token\n\tawait adapter.createToken({\n\t\thash,\n\t\temail,\n\t\ttype: \"invite\",\n\t\trole,\n\t\tinvitedBy,\n\t\texpiresAt: new Date(Date.now() + TOKEN_EXPIRY_MS),\n\t});\n\n\t// Build invite URL pointing to the admin UI page (not the API endpoint).\n\t// The admin SPA handles token validation and passkey registration.\n\tconst url = new URL(`${config.baseUrl}/admin/invite/accept`);\n\turl.searchParams.set(\"token\", token);\n\treturn { url: url.toString(), email };\n}\n\n/**\n * Build the invite email message.\n */\nfunction buildInviteEmail(inviteUrl: string, email: string, siteName: string): EmailMessage {\n\tconst safeName = escapeHtml(siteName);\n\treturn {\n\t\tto: email,\n\t\tsubject: `You've been invited to ${siteName}`,\n\t\ttext: `You've been invited to join ${siteName}.\\n\\nClick this link to create your account:\\n${inviteUrl}\\n\\nThis link expires in 7 days.`,\n\t\thtml: `\n<!DOCTYPE html>\n<html>\n<head>\n  <meta charset=\"utf-8\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n</head>\n<body style=\"font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.5; color: #333; max-width: 600px; margin: 0 auto; padding: 20px;\">\n  <h1 style=\"font-size: 24px; margin-bottom: 20px;\">You've been invited to ${safeName}</h1>\n  <p>Click the button below to create your account:</p>\n  <p style=\"margin: 30px 0;\">\n    <a href=\"${inviteUrl}\" style=\"background-color: #0066cc; color: white; padding: 12px 24px; text-decoration: none; border-radius: 6px; display: inline-block;\">Accept Invite</a>\n  </p>\n  <p style=\"color: #666; font-size: 14px;\">This link expires in 7 days.</p>\n</body>\n</html>`,\n\t};\n}\n\n/**\n * Create and send an invite to a new user.\n *\n * When `config.email` is provided, sends the invite email.\n * When omitted, creates the token and returns the invite URL\n * without sending (for the copy-link fallback).\n */\nexport async function createInvite(\n\tconfig: InviteConfig,\n\tadapter: AuthAdapter,\n\temail: string,\n\trole: RoleLevel,\n\tinvitedBy: string,\n): Promise<InviteTokenResult> {\n\tconst result = await createInviteToken(config, adapter, email, role, invitedBy);\n\n\t// Send email if a sender is configured\n\tif (config.email) {\n\t\tconst message = buildInviteEmail(result.url, email, config.siteName);\n\t\tawait config.email(message);\n\t}\n\n\treturn result;\n}\n\n/**\n * Validate an invite token and return the invite data\n */\nexport async function validateInvite(\n\tadapter: AuthAdapter,\n\ttoken: string,\n): Promise<{ email: string; role: RoleLevel }> {\n\tconst hash = hashToken(token);\n\n\tconst authToken = await adapter.getToken(hash, \"invite\");\n\tif (!authToken) {\n\t\tthrow new InviteError(\"invalid_token\", \"Invalid or expired invite link\");\n\t}\n\n\tif (authToken.expiresAt < new Date()) {\n\t\tawait adapter.deleteToken(hash);\n\t\tthrow new InviteError(\"token_expired\", \"This invite has expired\");\n\t}\n\n\tif (!authToken.email || authToken.role === null) {\n\t\tthrow new InviteError(\"invalid_token\", \"Invalid invite data\");\n\t}\n\n\treturn {\n\t\temail: authToken.email,\n\t\trole: authToken.role,\n\t};\n}\n\n/**\n * Complete the invite process (after passkey registration)\n */\nexport async function completeInvite(\n\tadapter: AuthAdapter,\n\ttoken: string,\n\tuserData: {\n\t\tname?: string;\n\t\tavatarUrl?: string;\n\t},\n): Promise<User> {\n\tconst hash = hashToken(token);\n\n\t// Validate token one more time\n\tconst authToken = await adapter.getToken(hash, \"invite\");\n\tif (!authToken || authToken.expiresAt < new Date()) {\n\t\tthrow new InviteError(\"invalid_token\", \"Invalid or expired invite\");\n\t}\n\n\tif (!authToken.email || authToken.role === null) {\n\t\tthrow new InviteError(\"invalid_token\", \"Invalid invite data\");\n\t}\n\n\t// Delete token (single-use)\n\tawait adapter.deleteToken(hash);\n\n\t// Create user\n\tconst user = await adapter.createUser({\n\t\temail: authToken.email,\n\t\tname: userData.name,\n\t\tavatarUrl: userData.avatarUrl,\n\t\trole: authToken.role,\n\t\temailVerified: true, // Email verified by accepting invite\n\t});\n\n\treturn user;\n}\n\nexport class InviteError extends Error {\n\tconstructor(\n\t\tpublic code: \"invalid_token\" | \"token_expired\" | \"user_exists\",\n\t\tmessage: string,\n\t) {\n\t\tsuper(message);\n\t\tthis.name = \"InviteError\";\n\t}\n}\n","/**\n * Magic link authentication\n */\n\nimport { escapeHtml } from \"../invite.js\";\nimport { generateTokenWithHash, hashToken } from \"../tokens.js\";\nimport type { AuthAdapter, User, EmailMessage } from \"../types.js\";\n\nconst TOKEN_EXPIRY_MS = 15 * 60 * 1000; // 15 minutes\n\n/** Function that sends an email (matches the EmailPipeline.send signature) */\nexport type EmailSendFn = (message: EmailMessage) => Promise<void>;\n\nexport interface MagicLinkConfig {\n\tbaseUrl: string;\n\tsiteName: string;\n\t/** Optional email sender. When omitted, magic links cannot be sent. */\n\temail?: EmailSendFn;\n}\n\n/**\n * Add artificial delay with jitter to prevent timing attacks.\n * Range approximates the time for token creation + email send.\n */\nasync function timingDelay(): Promise<void> {\n\tconst delay = 100 + Math.random() * 150; // 100-250ms\n\tawait new Promise((resolve) => setTimeout(resolve, delay));\n}\n\n/**\n * Send a magic link to a user's email.\n *\n * Requires `config.email` to be set. Throws if no email sender is configured.\n */\nexport async function sendMagicLink(\n\tconfig: MagicLinkConfig,\n\tadapter: AuthAdapter,\n\temail: string,\n\ttype: \"magic_link\" | \"recovery\" = \"magic_link\",\n): Promise<void> {\n\tif (!config.email) {\n\t\tthrow new MagicLinkError(\"email_not_configured\", \"Email is not configured\");\n\t}\n\n\t// Find user\n\tconst user = await adapter.getUserByEmail(email);\n\tif (!user) {\n\t\t// Don't reveal whether user exists - add delay to match successful path timing\n\t\tawait timingDelay();\n\t\treturn;\n\t}\n\n\t// Generate token\n\tconst { token, hash } = generateTokenWithHash();\n\n\t// Store token hash\n\tawait adapter.createToken({\n\t\thash,\n\t\tuserId: user.id,\n\t\temail: user.email,\n\t\ttype,\n\t\texpiresAt: new Date(Date.now() + TOKEN_EXPIRY_MS),\n\t});\n\n\t// Build magic link URL\n\tconst url = new URL(\"/_emdash/api/auth/magic-link/verify\", config.baseUrl);\n\turl.searchParams.set(\"token\", token);\n\n\t// Send email\n\tconst safeName = escapeHtml(config.siteName);\n\tawait config.email({\n\t\tto: user.email,\n\t\tsubject: `Sign in to ${config.siteName}`,\n\t\ttext: `Click this link to sign in to ${config.siteName}:\\n\\n${url.toString()}\\n\\nThis link expires in 15 minutes.\\n\\nIf you didn't request this, you can safely ignore this email.`,\n\t\thtml: `\n<!DOCTYPE html>\n<html>\n<head>\n  <meta charset=\"utf-8\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n</head>\n<body style=\"font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.5; color: #333; max-width: 600px; margin: 0 auto; padding: 20px;\">\n  <h1 style=\"font-size: 24px; margin-bottom: 20px;\">Sign in to ${safeName}</h1>\n  <p>Click the button below to sign in:</p>\n  <p style=\"margin: 30px 0;\">\n    <a href=\"${url.toString()}\" style=\"background-color: #0066cc; color: white; padding: 12px 24px; text-decoration: none; border-radius: 6px; display: inline-block;\">Sign in</a>\n  </p>\n  <p style=\"color: #666; font-size: 14px;\">This link expires in 15 minutes.</p>\n  <p style=\"color: #666; font-size: 14px;\">If you didn't request this, you can safely ignore this email.</p>\n</body>\n</html>`,\n\t});\n}\n\n/**\n * Verify a magic link token and return the user\n */\nexport async function verifyMagicLink(adapter: AuthAdapter, token: string): Promise<User> {\n\tconst hash = hashToken(token);\n\n\t// Find and validate token\n\tconst authToken = await adapter.getToken(hash, \"magic_link\");\n\tif (!authToken) {\n\t\t// Also check for recovery tokens\n\t\tconst recoveryToken = await adapter.getToken(hash, \"recovery\");\n\t\tif (!recoveryToken) {\n\t\t\tthrow new MagicLinkError(\"invalid_token\", \"Invalid or expired link\");\n\t\t}\n\t\treturn verifyTokenAndGetUser(adapter, recoveryToken, hash);\n\t}\n\n\treturn verifyTokenAndGetUser(adapter, authToken, hash);\n}\n\nasync function verifyTokenAndGetUser(\n\tadapter: AuthAdapter,\n\tauthToken: { userId: string | null; expiresAt: Date },\n\thash: string,\n): Promise<User> {\n\t// Check expiry\n\tif (authToken.expiresAt < new Date()) {\n\t\tawait adapter.deleteToken(hash);\n\t\tthrow new MagicLinkError(\"token_expired\", \"This link has expired\");\n\t}\n\n\t// Delete token (single-use)\n\tawait adapter.deleteToken(hash);\n\n\t// Get user\n\tif (!authToken.userId) {\n\t\tthrow new MagicLinkError(\"invalid_token\", \"Invalid token\");\n\t}\n\n\tconst user = await adapter.getUserById(authToken.userId);\n\tif (!user) {\n\t\tthrow new MagicLinkError(\"user_not_found\", \"User not found\");\n\t}\n\n\treturn user;\n}\n\nexport class MagicLinkError extends Error {\n\tconstructor(\n\t\tpublic code: \"invalid_token\" | \"token_expired\" | \"user_not_found\" | \"email_not_configured\",\n\t\tmessage: string,\n\t) {\n\t\tsuper(message);\n\t\tthis.name = \"MagicLinkError\";\n\t}\n}\n","/**\n * Self-signup for allowed email domains\n */\n\nimport { escapeHtml } from \"./invite.js\";\nimport { generateTokenWithHash, hashToken } from \"./tokens.js\";\nimport type { AuthAdapter, RoleLevel, EmailMessage, User } from \"./types.js\";\n\nconst TOKEN_EXPIRY_MS = 15 * 60 * 1000; // 15 minutes\n\n/** Function that sends an email (matches the EmailPipeline.send signature) */\nexport type EmailSendFn = (message: EmailMessage) => Promise<void>;\n\n/**\n * Add artificial delay with jitter to prevent timing attacks.\n * Range approximates the time for token creation + email send.\n */\nasync function timingDelay(): Promise<void> {\n\tconst delay = 100 + Math.random() * 150; // 100-250ms\n\tawait new Promise((resolve) => setTimeout(resolve, delay));\n}\n\nexport interface SignupConfig {\n\tbaseUrl: string;\n\tsiteName: string;\n\t/** Optional email sender. When omitted, signup verification cannot be sent. */\n\temail?: EmailSendFn;\n}\n\n/**\n * Check if an email domain is allowed for self-signup\n */\nexport async function canSignup(\n\tadapter: AuthAdapter,\n\temail: string,\n): Promise<{ allowed: boolean; role: RoleLevel } | null> {\n\tconst domain = email.split(\"@\")[1]?.toLowerCase();\n\tif (!domain) return null;\n\n\tconst allowedDomain = await adapter.getAllowedDomain(domain);\n\tif (!allowedDomain || !allowedDomain.enabled) {\n\t\treturn null;\n\t}\n\n\treturn {\n\t\tallowed: true,\n\t\trole: allowedDomain.defaultRole,\n\t};\n}\n\n/**\n * Request self-signup (sends verification email).\n *\n * Requires `config.email` to be set. Throws if no email sender is configured.\n */\nexport async function requestSignup(\n\tconfig: SignupConfig,\n\tadapter: AuthAdapter,\n\temail: string,\n): Promise<void> {\n\tif (!config.email) {\n\t\tthrow new SignupError(\"email_not_configured\", \"Email is not configured\");\n\t}\n\n\t// Check if user already exists\n\tconst existing = await adapter.getUserByEmail(email);\n\tif (existing) {\n\t\t// Don't reveal that user exists - add delay to match successful path timing\n\t\tawait timingDelay();\n\t\treturn;\n\t}\n\n\t// Check if domain is allowed\n\tconst signup = await canSignup(adapter, email);\n\tif (!signup) {\n\t\t// Don't reveal that domain is not allowed - add delay to match successful path timing\n\t\tawait timingDelay();\n\t\treturn;\n\t}\n\n\t// Generate token\n\tconst { token, hash } = generateTokenWithHash();\n\n\t// Store token with role info\n\tawait adapter.createToken({\n\t\thash,\n\t\temail,\n\t\ttype: \"email_verify\",\n\t\trole: signup.role,\n\t\texpiresAt: new Date(Date.now() + TOKEN_EXPIRY_MS),\n\t});\n\n\t// Build verification URL\n\tconst url = new URL(\"/_emdash/api/auth/signup/verify\", config.baseUrl);\n\turl.searchParams.set(\"token\", token);\n\n\t// Send email\n\tconst safeName = escapeHtml(config.siteName);\n\tawait config.email({\n\t\tto: email,\n\t\tsubject: `Verify your email for ${config.siteName}`,\n\t\ttext: `Click this link to verify your email and create your account:\\n\\n${url.toString()}\\n\\nThis link expires in 15 minutes.\\n\\nIf you didn't request this, you can safely ignore this email.`,\n\t\thtml: `\n<!DOCTYPE html>\n<html>\n<head>\n  <meta charset=\"utf-8\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n</head>\n<body style=\"font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.5; color: #333; max-width: 600px; margin: 0 auto; padding: 20px;\">\n  <h1 style=\"font-size: 24px; margin-bottom: 20px;\">Verify your email</h1>\n  <p>Click the button below to verify your email and create your ${safeName} account:</p>\n  <p style=\"margin: 30px 0;\">\n    <a href=\"${url.toString()}\" style=\"background-color: #0066cc; color: white; padding: 12px 24px; text-decoration: none; border-radius: 6px; display: inline-block;\">Verify Email</a>\n  </p>\n  <p style=\"color: #666; font-size: 14px;\">This link expires in 15 minutes.</p>\n  <p style=\"color: #666; font-size: 14px;\">If you didn't request this, you can safely ignore this email.</p>\n</body>\n</html>`,\n\t});\n}\n\n/**\n * Validate a signup verification token\n */\nexport async function validateSignupToken(\n\tadapter: AuthAdapter,\n\ttoken: string,\n): Promise<{ email: string; role: RoleLevel }> {\n\tconst hash = hashToken(token);\n\n\tconst authToken = await adapter.getToken(hash, \"email_verify\");\n\tif (!authToken) {\n\t\tthrow new SignupError(\"invalid_token\", \"Invalid or expired verification link\");\n\t}\n\n\tif (authToken.expiresAt < new Date()) {\n\t\tawait adapter.deleteToken(hash);\n\t\tthrow new SignupError(\"token_expired\", \"This link has expired\");\n\t}\n\n\tif (!authToken.email || authToken.role === null) {\n\t\tthrow new SignupError(\"invalid_token\", \"Invalid token data\");\n\t}\n\n\treturn {\n\t\temail: authToken.email,\n\t\trole: authToken.role,\n\t};\n}\n\n/**\n * Complete signup process (after passkey registration)\n */\nexport async function completeSignup(\n\tadapter: AuthAdapter,\n\ttoken: string,\n\tuserData: {\n\t\tname?: string;\n\t\tavatarUrl?: string;\n\t},\n): Promise<User> {\n\tconst hash = hashToken(token);\n\n\t// Validate token one more time\n\tconst authToken = await adapter.getToken(hash, \"email_verify\");\n\tif (!authToken || authToken.expiresAt < new Date()) {\n\t\tthrow new SignupError(\"invalid_token\", \"Invalid or expired verification\");\n\t}\n\n\tif (!authToken.email || authToken.role === null) {\n\t\tthrow new SignupError(\"invalid_token\", \"Invalid token data\");\n\t}\n\n\t// Check user doesn't already exist\n\tconst existing = await adapter.getUserByEmail(authToken.email);\n\tif (existing) {\n\t\tawait adapter.deleteToken(hash);\n\t\tthrow new SignupError(\"user_exists\", \"An account with this email already exists\");\n\t}\n\n\t// Delete token (single-use)\n\tawait adapter.deleteToken(hash);\n\n\t// Create user\n\tconst user = await adapter.createUser({\n\t\temail: authToken.email,\n\t\tname: userData.name,\n\t\tavatarUrl: userData.avatarUrl,\n\t\trole: authToken.role,\n\t\temailVerified: true,\n\t});\n\n\treturn user;\n}\n\nexport class SignupError extends Error {\n\tconstructor(\n\t\tpublic code:\n\t\t\t| \"invalid_token\"\n\t\t\t| \"token_expired\"\n\t\t\t| \"user_exists\"\n\t\t\t| \"domain_not_allowed\"\n\t\t\t| \"email_not_configured\",\n\t\tmessage: string,\n\t) {\n\t\tsuper(message);\n\t\tthis.name = \"SignupError\";\n\t}\n}\n","/**\n * OAuth consumer - \"Login with X\" functionality\n */\n\nimport { sha256 } from \"@oslojs/crypto/sha2\";\nimport { encodeBase64urlNoPadding } from \"@oslojs/encoding\";\nimport { z } from \"zod\";\n\nimport { completeInvite, InviteError, validateInvite } from \"../invite.js\";\nimport { hashToken } from \"../tokens.js\";\nimport type { AuthAdapter, User, RoleLevel } from \"../types.js\";\nimport { github, fetchGitHubEmail } from \"./providers/github.js\";\nimport { google } from \"./providers/google.js\";\nimport type { OAuthProvider, OAuthConfig, OAuthProfile, OAuthState } from \"./types.js\";\n\nexport { github, google };\n\nexport interface OAuthConsumerConfig {\n\tbaseUrl: string;\n\tproviders: {\n\t\tgithub?: OAuthConfig;\n\t\tgoogle?: OAuthConfig;\n\t};\n\t/**\n\t * Check if self-signup is allowed for this email domain\n\t */\n\tcanSelfSignup?: (email: string) => Promise<{ allowed: boolean; role: RoleLevel } | null>;\n}\n\n/**\n * Generate an OAuth authorization URL\n */\nexport async function createAuthorizationUrl(\n\tconfig: OAuthConsumerConfig,\n\tproviderName: \"github\" | \"google\",\n\tstateStore: StateStore,\n\toptions?: {\n\t\t/** When set, this flow accepts an invite and the callback completes it. */\n\t\tinviteToken?: string;\n\t},\n): Promise<{ url: string; state: string }> {\n\tconst providerConfig = config.providers[providerName];\n\tif (!providerConfig) {\n\t\tthrow new Error(`OAuth provider ${providerName} not configured`);\n\t}\n\n\tconst provider = getProvider(providerName);\n\tconst state = generateState();\n\tconst redirectUri = new URL(\n\t\t`/_emdash/api/auth/oauth/${providerName}/callback`,\n\t\tconfig.baseUrl,\n\t).toString();\n\n\t// Generate PKCE code verifier for providers that support it\n\tconst codeVerifier = generateCodeVerifier();\n\tconst codeChallenge = await generateCodeChallenge(codeVerifier);\n\n\t// Store state for verification\n\tawait stateStore.set(state, {\n\t\tprovider: providerName,\n\t\tredirectUri,\n\t\tcodeVerifier,\n\t\t...(options?.inviteToken ? { inviteToken: options.inviteToken } : {}),\n\t});\n\n\t// Build authorization URL\n\tconst url = new URL(provider.authorizeUrl);\n\turl.searchParams.set(\"client_id\", providerConfig.clientId);\n\turl.searchParams.set(\"redirect_uri\", redirectUri);\n\turl.searchParams.set(\"response_type\", \"code\");\n\turl.searchParams.set(\"scope\", provider.scopes.join(\" \"));\n\turl.searchParams.set(\"state\", state);\n\n\t// PKCE for all providers (GitHub has supported S256 since 2021)\n\turl.searchParams.set(\"code_challenge\", codeChallenge);\n\turl.searchParams.set(\"code_challenge_method\", \"S256\");\n\n\treturn { url: url.toString(), state };\n}\n\n/**\n * Handle OAuth callback\n */\nexport async function handleOAuthCallback(\n\tconfig: OAuthConsumerConfig,\n\tadapter: AuthAdapter,\n\tproviderName: \"github\" | \"google\",\n\tcode: string,\n\tstate: string,\n\tstateStore: StateStore,\n): Promise<User> {\n\tconst providerConfig = config.providers[providerName];\n\tif (!providerConfig) {\n\t\tthrow new Error(`OAuth provider ${providerName} not configured`);\n\t}\n\n\t// Verify state\n\tconst storedState = await stateStore.get(state);\n\tif (!storedState || storedState.provider !== providerName) {\n\t\tthrow new OAuthError(\"invalid_state\", \"Invalid OAuth state\");\n\t}\n\n\t// Delete state (single-use)\n\tawait stateStore.delete(state);\n\n\tconst provider = getProvider(providerName);\n\n\t// Exchange code for tokens\n\tconst tokens = await exchangeCode(\n\t\tprovider,\n\t\tproviderConfig,\n\t\tcode,\n\t\tstoredState.redirectUri,\n\t\tstoredState.codeVerifier,\n\t);\n\n\t// Fetch user profile\n\tconst profile = await fetchProfile(provider, tokens.accessToken, providerName);\n\n\t// When the flow carried an invite token, complete the invite instead of\n\t// applying the self-signup policy.\n\tif (storedState.inviteToken) {\n\t\treturn acceptInviteViaOAuth(adapter, providerName, profile, storedState.inviteToken);\n\t}\n\n\t// Find or create user\n\treturn findOrCreateOAuthUser(adapter, providerName, profile, config.canSelfSignup);\n}\n\n/**\n * Complete an invite using an OAuth identity.\n *\n * The invite is only accepted when the provider has verified the email AND it\n * matches the invited address (case-insensitive), so a login for a different\n * account cannot consume someone else's invite. On success the user is created\n * with the invited role and the OAuth account is linked.\n */\nexport async function acceptInviteViaOAuth(\n\tadapter: AuthAdapter,\n\tproviderName: string,\n\tprofile: OAuthProfile,\n\tinviteToken: string,\n): Promise<User> {\n\tlet invite: { email: string; role: RoleLevel };\n\ttry {\n\t\tinvite = await validateInvite(adapter, inviteToken);\n\t} catch (error) {\n\t\tif (error instanceof InviteError) {\n\t\t\tthrow new OAuthError(\"invite_invalid\", error.message);\n\t\t}\n\t\tthrow error;\n\t}\n\n\tif (!profile.emailVerified) {\n\t\tthrow new OAuthError(\n\t\t\t\"invite_email_unverified\",\n\t\t\t\"Cannot accept invite: email not verified by provider\",\n\t\t);\n\t}\n\n\tif (profile.email.toLowerCase() !== invite.email.toLowerCase()) {\n\t\tthrow new OAuthError(\n\t\t\t\"invite_email_mismatch\",\n\t\t\t\"This invite was sent to a different email address than your account.\",\n\t\t);\n\t}\n\n\t// Already linked (e.g. a retried callback): consume the invite and return.\n\tconst existingAccount = await adapter.getOAuthAccount(providerName, profile.id);\n\tif (existingAccount) {\n\t\tconst user = await adapter.getUserById(existingAccount.userId);\n\t\tif (!user) {\n\t\t\tthrow new OAuthError(\"user_not_found\", \"Linked user not found\");\n\t\t}\n\t\t// The linked user must actually own the invited email; otherwise a\n\t\t// provider identity whose EmDash email was changed after linking could\n\t\t// consume an invite issued to someone else.\n\t\tif (user.email.toLowerCase() !== invite.email.toLowerCase()) {\n\t\t\tthrow new OAuthError(\n\t\t\t\t\"invite_email_mismatch\",\n\t\t\t\t\"This invite was sent to a different email address than your account.\",\n\t\t\t);\n\t\t}\n\t\tawait adapter.deleteToken(hashToken(inviteToken));\n\t\treturn user;\n\t}\n\n\t// The invited email already belongs to a user (invite already accepted, or a\n\t// pre-existing account): link the OAuth account and consume the invite rather\n\t// than failing, so the single-use token cannot be replayed.\n\tconst existingUser = await adapter.getUserByEmail(profile.email);\n\tif (existingUser) {\n\t\tawait adapter.createOAuthAccount({\n\t\t\tprovider: providerName,\n\t\t\tproviderAccountId: profile.id,\n\t\t\tuserId: existingUser.id,\n\t\t});\n\t\tawait adapter.deleteToken(hashToken(inviteToken));\n\t\treturn existingUser;\n\t}\n\n\t// Consume the invite: create the user with the invited role, then link.\n\tconst user = await completeInvite(adapter, inviteToken, {\n\t\tname: profile.name ?? undefined,\n\t\tavatarUrl: profile.avatarUrl ?? undefined,\n\t});\n\tawait adapter.createOAuthAccount({\n\t\tprovider: providerName,\n\t\tproviderAccountId: profile.id,\n\t\tuserId: user.id,\n\t});\n\treturn user;\n}\n\n/**\n * Exchange authorization code for tokens\n */\nasync function exchangeCode(\n\tprovider: OAuthProvider,\n\tconfig: OAuthConfig,\n\tcode: string,\n\tredirectUri: string,\n\tcodeVerifier?: string,\n): Promise<{ accessToken: string; idToken?: string }> {\n\tconst body = new URLSearchParams({\n\t\tgrant_type: \"authorization_code\",\n\t\tcode,\n\t\tredirect_uri: redirectUri,\n\t\tclient_id: config.clientId,\n\t\tclient_secret: config.clientSecret,\n\t});\n\n\tif (codeVerifier) {\n\t\tbody.set(\"code_verifier\", codeVerifier);\n\t}\n\n\tconst response = await fetch(provider.tokenUrl, {\n\t\tmethod: \"POST\",\n\t\theaders: {\n\t\t\t\"Content-Type\": \"application/x-www-form-urlencoded\",\n\t\t\tAccept: \"application/json\",\n\t\t},\n\t\tbody,\n\t});\n\n\tif (!response.ok) {\n\t\tconst error = await response.text();\n\t\tthrow new OAuthError(\"token_exchange_failed\", `Token exchange failed: ${error}`);\n\t}\n\n\tconst json: unknown = await response.json();\n\tconst data = z\n\t\t.object({\n\t\t\taccess_token: z.string(),\n\t\t\tid_token: z.string().optional(),\n\t\t})\n\t\t.parse(json);\n\n\treturn {\n\t\taccessToken: data.access_token,\n\t\tidToken: data.id_token,\n\t};\n}\n\n/**\n * Fetch user profile from OAuth provider\n */\nasync function fetchProfile(\n\tprovider: OAuthProvider,\n\taccessToken: string,\n\tproviderName: string,\n): Promise<OAuthProfile> {\n\tif (!provider.userInfoUrl) {\n\t\tthrow new Error(\"Provider does not have userinfo URL\");\n\t}\n\n\tconst response = await fetch(provider.userInfoUrl, {\n\t\theaders: {\n\t\t\tAuthorization: `Bearer ${accessToken}`,\n\t\t\tAccept: \"application/json\",\n\t\t\t\"User-Agent\": \"emdash-cms\",\n\t\t},\n\t});\n\n\tif (!response.ok) {\n\t\tthrow new OAuthError(\"profile_fetch_failed\", `Failed to fetch profile: ${response.status}`);\n\t}\n\n\tconst data = await response.json();\n\tconst profile = provider.parseProfile(data);\n\n\t// GitHub may not return email in main profile\n\tif (providerName === \"github\" && !profile.email) {\n\t\tprofile.email = await fetchGitHubEmail(accessToken);\n\t}\n\n\treturn profile;\n}\n\n/**\n * Signup policy callback.\n * Return `{ allowed: true, role }` to permit signup, or `null` to deny.\n */\nexport type CanSelfSignup = (\n\temail: string,\n) => Promise<{ allowed: boolean; role: RoleLevel } | null>;\n\n/**\n * Find existing user or create new one (with auto-linking).\n *\n * Shared across all OAuth providers (GitHub, Google, AT Protocol, etc.).\n * The provider-specific token exchange happens before this function is called;\n * this function only deals with the EmDash user record.\n */\nexport async function findOrCreateOAuthUser(\n\tadapter: AuthAdapter,\n\tproviderName: string,\n\tprofile: OAuthProfile,\n\tcanSelfSignup?: CanSelfSignup,\n): Promise<User> {\n\t// Check if OAuth account already linked\n\tconst existingAccount = await adapter.getOAuthAccount(providerName, profile.id);\n\tif (existingAccount) {\n\t\tconst user = await adapter.getUserById(existingAccount.userId);\n\t\tif (!user) {\n\t\t\tthrow new OAuthError(\"user_not_found\", \"Linked user not found\");\n\t\t}\n\t\treturn user;\n\t}\n\n\t// Check if user with this email exists (auto-link)\n\t// Only auto-link when the provider has verified the email to prevent\n\t// account takeover via unverified email on a third-party provider\n\tconst existingUser = await adapter.getUserByEmail(profile.email);\n\tif (existingUser) {\n\t\tif (!profile.emailVerified) {\n\t\t\tthrow new OAuthError(\n\t\t\t\t\"signup_not_allowed\",\n\t\t\t\t\"Cannot link account: email not verified by provider\",\n\t\t\t);\n\t\t}\n\t\tawait adapter.createOAuthAccount({\n\t\t\tprovider: providerName,\n\t\t\tproviderAccountId: profile.id,\n\t\t\tuserId: existingUser.id,\n\t\t});\n\t\treturn existingUser;\n\t}\n\n\t// Check if self-signup is allowed\n\tif (canSelfSignup) {\n\t\tconst signup = await canSelfSignup(profile.email);\n\t\tif (signup?.allowed) {\n\t\t\t// Create new user\n\t\t\tconst user = await adapter.createUser({\n\t\t\t\temail: profile.email,\n\t\t\t\tname: profile.name,\n\t\t\t\tavatarUrl: profile.avatarUrl,\n\t\t\t\trole: signup.role,\n\t\t\t\temailVerified: profile.emailVerified,\n\t\t\t});\n\n\t\t\t// Link OAuth account\n\t\t\tawait adapter.createOAuthAccount({\n\t\t\t\tprovider: providerName,\n\t\t\t\tproviderAccountId: profile.id,\n\t\t\t\tuserId: user.id,\n\t\t\t});\n\n\t\t\treturn user;\n\t\t}\n\t}\n\n\tthrow new OAuthError(\"signup_not_allowed\", \"Self-signup not allowed for this email domain\");\n}\n\nfunction getProvider(name: \"github\" | \"google\"): OAuthProvider {\n\tswitch (name) {\n\t\tcase \"github\":\n\t\t\treturn github;\n\t\tcase \"google\":\n\t\t\treturn google;\n\t}\n}\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\n/**\n * Generate a random state string for OAuth CSRF protection\n */\nfunction generateState(): string {\n\tconst bytes = new Uint8Array(32);\n\tcrypto.getRandomValues(bytes);\n\treturn encodeBase64urlNoPadding(bytes);\n}\n\nfunction generateCodeVerifier(): string {\n\tconst bytes = new Uint8Array(32);\n\tcrypto.getRandomValues(bytes);\n\treturn encodeBase64urlNoPadding(bytes);\n}\n\nasync function generateCodeChallenge(verifier: string): Promise<string> {\n\tconst bytes = new TextEncoder().encode(verifier);\n\tconst hash = sha256(bytes);\n\treturn encodeBase64urlNoPadding(hash);\n}\n\n// ============================================================================\n// State storage interface\n// ============================================================================\n\nexport interface StateStore {\n\tset(state: string, data: OAuthState): Promise<void>;\n\tget(state: string): Promise<OAuthState | null>;\n\tdelete(state: string): Promise<void>;\n}\n\n// ============================================================================\n// Errors\n// ============================================================================\n\nexport class OAuthError extends Error {\n\tconstructor(\n\t\tpublic code:\n\t\t\t| \"invalid_state\"\n\t\t\t| \"token_exchange_failed\"\n\t\t\t| \"profile_fetch_failed\"\n\t\t\t| \"user_not_found\"\n\t\t\t| \"signup_not_allowed\"\n\t\t\t| \"invite_invalid\"\n\t\t\t| \"invite_email_mismatch\"\n\t\t\t| \"invite_email_unverified\",\n\t\tmessage: string,\n\t) {\n\t\tsuper(message);\n\t\tthis.name = \"OAuthError\";\n\t}\n}\n","/**\n * @emdash-cms/auth - Passkey-first authentication for EmDash\n *\n * Email is now handled by the plugin email pipeline (see PLUGIN-EMAIL.md).\n * Auth functions accept an optional `email` send function instead of a\n * hardcoded adapter. The route layer bridges `emdash.email.send()` from\n * the pipeline into the auth functions.\n *\n * @example\n * ```ts\n * import { auth } from '@emdash-cms/auth'\n *\n * export default defineConfig({\n *   integrations: [\n *     emdash({\n *       auth: auth({\n *         secret: import.meta.env.EMDASH_AUTH_SECRET,\n *         passkeys: { rpName: 'My Site' },\n *       }),\n *     }),\n *   ],\n * })\n * ```\n */\n\n// Types\nexport * from \"./types.js\";\n\n// Config\nimport { authConfigSchema as _authConfigSchema } from \"./config.js\";\nexport {\n\tauthConfigSchema,\n\tresolveConfig,\n\ttype AuthConfig,\n\ttype ResolvedAuthConfig,\n} from \"./config.js\";\n\n// RBAC\nexport {\n\tPermissions,\n\thasPermission,\n\trequirePermission,\n\tcanActOnOwn,\n\trequirePermissionOnResource,\n\tPermissionError,\n\tscopesForRole,\n\tclampScopes,\n\ttype Permission,\n} from \"./rbac.js\";\n\n// Tokens\nexport {\n\tgenerateToken,\n\thashToken,\n\tgenerateTokenWithHash,\n\tgenerateSessionId,\n\tgenerateAuthSecret,\n\tsecureCompare,\n\tencrypt,\n\tdecrypt,\n\t// Prefixed API tokens (ec_pat_, ec_oat_, ec_ort_)\n\tTOKEN_PREFIXES,\n\tgeneratePrefixedToken,\n\thashPrefixedToken,\n\t// Scopes\n\tVALID_SCOPES,\n\tvalidateScopes,\n\tisValidScope,\n\thasScope,\n\ttype ApiTokenScope,\n\t// PKCE\n\tcomputeS256Challenge,\n} from \"./tokens.js\";\n\n// Passkey\nexport * from \"./passkey/index.js\";\n\n// Magic Link\nexport {\n\tsendMagicLink,\n\tverifyMagicLink,\n\tMagicLinkError,\n\ttype MagicLinkConfig,\n} from \"./magic-link/index.js\";\n\n// Invite\nexport {\n\tcreateInvite,\n\tcreateInviteToken,\n\tvalidateInvite,\n\tcompleteInvite,\n\tInviteError,\n\tescapeHtml,\n\ttype InviteConfig,\n\ttype InviteTokenResult,\n\ttype EmailSendFn,\n} from \"./invite.js\";\n\n// Signup\nexport {\n\tcanSignup,\n\trequestSignup,\n\tvalidateSignupToken,\n\tcompleteSignup,\n\tSignupError,\n\ttype SignupConfig,\n} from \"./signup.js\";\n\n// OAuth\nexport {\n\tcreateAuthorizationUrl,\n\thandleOAuthCallback,\n\tfindOrCreateOAuthUser,\n\tacceptInviteViaOAuth,\n\tOAuthError,\n\tgithub,\n\tgoogle,\n\ttype CanSelfSignup,\n\ttype StateStore,\n\ttype OAuthConsumerConfig,\n} from \"./oauth/consumer.js\";\nexport type { OAuthProvider, OAuthConfig, OAuthProfile, OAuthState } from \"./oauth/types.js\";\n\n// Email types (implementations moved to plugin email pipeline)\nexport type { EmailAdapter, EmailMessage } from \"./types.js\";\n\n/**\n * Create an auth configuration\n *\n * This is a helper function that validates the config at runtime.\n */\nexport function auth(config: import(\"./config.js\").AuthConfig): import(\"./config.js\").AuthConfig {\n\t// Validate config\n\tconst result = _authConfigSchema.safeParse(config);\n\tif (!result.success) {\n\t\tthrow new Error(`Invalid auth config: ${result.error.message}`);\n\t}\n\treturn result.data;\n}\n"],"mappings":"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"}