@emdash-cms/auth 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,9 @@
1
+ The MIT License
2
+
3
+ Copyright 2026 Cloudflare Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -1,4 +1,6 @@
1
- import { sha256 } from "@oslojs/crypto/sha2";
1
+ import { hmac } from "@oslojs/crypto/hmac";
2
+ import { SHA256, sha256 } from "@oslojs/crypto/sha2";
3
+ import { constantTimeEqual } from "@oslojs/crypto/subtle";
2
4
  import { decodeBase64urlIgnorePadding, encodeBase64urlNoPadding } from "@oslojs/encoding";
3
5
  import { ECDSAPublicKey, decodePKIXECDSASignature, decodeSEC1PublicKey, p256, verifyECDSASignature } from "@oslojs/crypto/ecdsa";
4
6
  import { AttestationStatementFormat, COSEKeyType, ClientDataType, coseAlgorithmES256, coseAlgorithmRS256, coseEllipticCurveP256, createAssertionSignatureMessage, parseAttestationObject, parseAuthenticatorData, parseClientDataJSON } from "@oslojs/webauthn";
@@ -123,12 +125,10 @@ function computeS256Challenge(codeVerifier) {
123
125
  * Constant-time comparison to prevent timing attacks
124
126
  */
125
127
  function secureCompare(a, b) {
126
- if (a.length !== b.length) return false;
127
- const aBytes = new TextEncoder().encode(a);
128
- const bBytes = new TextEncoder().encode(b);
129
- let result = 0;
130
- for (let i = 0; i < aBytes.length; i++) result |= aBytes[i] ^ bBytes[i];
131
- return result === 0;
128
+ const text = new TextEncoder();
129
+ const salt = crypto.getRandomValues(new Uint8Array(TOKEN_BYTES));
130
+ const hash = (str) => hmac(SHA256, salt, text.encode(str));
131
+ return constantTimeEqual(hash(a), hash(b));
132
132
  }
133
133
  const ALGORITHM = "AES-GCM";
134
134
  const IV_BYTES = 12;
@@ -370,4 +370,4 @@ async function authenticateWithPasskey(config, adapter, response, challengeStore
370
370
 
371
371
  //#endregion
372
372
  export { hasScope as _, registerPasskey as a, secureCompare as b, VALID_SCOPES as c, encrypt as d, generateAuthSecret as f, generateTokenWithHash as g, generateToken as h, generateRegistrationOptions as i, computeS256Challenge as l, generateSessionId as m, generateAuthenticationOptions as n, verifyRegistrationResponse as o, generatePrefixedToken as p, verifyAuthenticationResponse as r, TOKEN_PREFIXES as s, authenticateWithPasskey as t, decrypt as u, hashPrefixedToken as v, validateScopes as x, hashToken as y };
373
- //# sourceMappingURL=authenticate-j5GayLXB.mjs.map
373
+ //# sourceMappingURL=authenticate-CZ5fe42l.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"authenticate-CZ5fe42l.mjs","names":["CHALLENGE_TTL"],"sources":["../src/tokens.ts","../src/passkey/register.ts","../src/passkey/authenticate.ts"],"sourcesContent":["/**\n * Secure token utilities\n *\n * Crypto via Oslo.js (@oslojs/crypto). Base64url via @oslojs/encoding.\n *\n * Tokens are opaque random values. We store only the SHA-256 hash in the database.\n */\n\nimport { hmac } from \"@oslojs/crypto/hmac\";\nimport { sha256, SHA256 } from \"@oslojs/crypto/sha2\";\nimport { constantTimeEqual } from \"@oslojs/crypto/subtle\";\nimport { encodeBase64urlNoPadding, decodeBase64urlIgnorePadding } from \"@oslojs/encoding\";\n\nconst TOKEN_BYTES = 32; // 256 bits of entropy\n\n// ---------------------------------------------------------------------------\n// API Token Prefixes\n// ---------------------------------------------------------------------------\n\n/** Valid API token prefixes */\nexport const TOKEN_PREFIXES = {\n\tPAT: \"ec_pat_\",\n\tOAUTH_ACCESS: \"ec_oat_\",\n\tOAUTH_REFRESH: \"ec_ort_\",\n} as const;\n\n// ---------------------------------------------------------------------------\n// Scopes\n// ---------------------------------------------------------------------------\n\n/** All valid API token scopes */\nexport const VALID_SCOPES = [\n\t\"content:read\",\n\t\"content:write\",\n\t\"media:read\",\n\t\"media:write\",\n\t\"schema:read\",\n\t\"schema:write\",\n\t\"admin\",\n] as const;\n\nexport type ApiTokenScope = (typeof VALID_SCOPES)[number];\n\n/**\n * Validate that scopes are all valid.\n * Returns the invalid scopes, or empty array if all valid.\n */\nexport function validateScopes(scopes: string[]): string[] {\n\tconst validSet = new Set<string>(VALID_SCOPES);\n\treturn scopes.filter((s) => !validSet.has(s));\n}\n\n/**\n * Check if a set of scopes includes a required scope.\n * The `admin` scope grants access to everything.\n */\nexport function hasScope(scopes: string[], required: string): boolean {\n\tif (scopes.includes(\"admin\")) return true;\n\treturn scopes.includes(required);\n}\n\n/**\n * Generate a cryptographically secure random token\n * Returns base64url-encoded string (URL-safe)\n */\nexport function generateToken(): string {\n\tconst bytes = new Uint8Array(TOKEN_BYTES);\n\tcrypto.getRandomValues(bytes);\n\treturn encodeBase64urlNoPadding(bytes);\n}\n\n/**\n * Hash a token for storage\n * We never store raw tokens - only their SHA-256 hash\n */\nexport function hashToken(token: string): string {\n\tconst bytes = decodeBase64urlIgnorePadding(token);\n\tconst hash = sha256(bytes);\n\treturn encodeBase64urlNoPadding(hash);\n}\n\n/**\n * Generate a token and its hash together\n */\nexport function generateTokenWithHash(): { token: string; hash: string } {\n\tconst token = generateToken();\n\tconst hash = hashToken(token);\n\treturn { token, hash };\n}\n\n/**\n * Generate a session ID (shorter, for cookie storage)\n */\nexport function generateSessionId(): string {\n\tconst bytes = new Uint8Array(20); // 160 bits\n\tcrypto.getRandomValues(bytes);\n\treturn encodeBase64urlNoPadding(bytes);\n}\n\n/**\n * Generate an auth secret for configuration\n */\nexport function generateAuthSecret(): string {\n\tconst bytes = new Uint8Array(32);\n\tcrypto.getRandomValues(bytes);\n\treturn encodeBase64urlNoPadding(bytes);\n}\n\n// ---------------------------------------------------------------------------\n// Prefixed API tokens (ec_pat_, ec_oat_, ec_ort_)\n// ---------------------------------------------------------------------------\n\n/**\n * Generate a prefixed API token and its hash.\n * Returns the raw token (shown once to the user), the hash (stored server-side),\n * and a display prefix (for identification in UIs/logs).\n *\n * Uses oslo/crypto for SHA-256 hashing.\n */\nexport function generatePrefixedToken(prefix: string): {\n\traw: string;\n\thash: string;\n\tprefix: string;\n} {\n\tconst bytes = new Uint8Array(TOKEN_BYTES);\n\tcrypto.getRandomValues(bytes);\n\n\tconst encoded = encodeBase64urlNoPadding(bytes);\n\tconst raw = `${prefix}${encoded}`;\n\tconst hash = hashPrefixedToken(raw);\n\n\t// First few chars for identification in UIs\n\tconst displayPrefix = raw.slice(0, prefix.length + 4);\n\n\treturn { raw, hash, prefix: displayPrefix };\n}\n\n/**\n * Hash a prefixed API token for storage/lookup.\n * Hashes the full prefixed token string via SHA-256, returns base64url (no padding).\n */\nexport function hashPrefixedToken(token: string): string {\n\tconst bytes = new TextEncoder().encode(token);\n\tconst hash = sha256(bytes);\n\treturn encodeBase64urlNoPadding(hash);\n}\n\n// ---------------------------------------------------------------------------\n// PKCE (RFC 7636) — server-side verification\n// ---------------------------------------------------------------------------\n\n/**\n * Compute an S256 PKCE code challenge from a code verifier.\n * Used server-side to verify that code_verifier matches the stored code_challenge.\n *\n * Equivalent to: BASE64URL(SHA256(ASCII(code_verifier)))\n */\nexport function computeS256Challenge(codeVerifier: string): string {\n\tconst hash = sha256(new TextEncoder().encode(codeVerifier));\n\treturn encodeBase64urlNoPadding(hash);\n}\n\n/**\n * Constant-time comparison to prevent timing attacks\n */\nexport function secureCompare(a: string, b: string): boolean {\n\tconst text = new TextEncoder();\n\tconst salt = crypto.getRandomValues(new Uint8Array(TOKEN_BYTES));\n\tconst hash = (str: string) => hmac(SHA256, salt, text.encode(str));\n\n\treturn constantTimeEqual(hash(a), hash(b));\n}\n\n// ============================================================================\n// Encryption utilities (for storing OAuth secrets)\n// ============================================================================\n\nconst ALGORITHM = \"AES-GCM\";\nconst IV_BYTES = 12;\n\n/**\n * Derive an encryption key from the auth secret\n */\nasync function deriveKey(secret: string): Promise<CryptoKey> {\n\tconst decoded = decodeBase64urlIgnorePadding(secret);\n\t// Create a new ArrayBuffer to ensure compatibility with crypto.subtle\n\tconst buffer = new Uint8Array(decoded).buffer;\n\tconst keyMaterial = await crypto.subtle.importKey(\"raw\", buffer, \"PBKDF2\", false, [\"deriveKey\"]);\n\n\treturn crypto.subtle.deriveKey(\n\t\t{\n\t\t\tname: \"PBKDF2\",\n\t\t\tsalt: new TextEncoder().encode(\"emdash-auth-v1\"),\n\t\t\titerations: 100000,\n\t\t\thash: \"SHA-256\",\n\t\t},\n\t\tkeyMaterial,\n\t\t{ name: ALGORITHM, length: 256 },\n\t\tfalse,\n\t\t[\"encrypt\", \"decrypt\"],\n\t);\n}\n\n/**\n * Encrypt a value using AES-GCM\n */\nexport async function encrypt(plaintext: string, secret: string): Promise<string> {\n\tconst key = await deriveKey(secret);\n\tconst iv = crypto.getRandomValues(new Uint8Array(IV_BYTES));\n\tconst encoded = new TextEncoder().encode(plaintext);\n\n\tconst ciphertext = await crypto.subtle.encrypt({ name: ALGORITHM, iv }, key, encoded);\n\n\t// Prepend IV to ciphertext\n\tconst combined = new Uint8Array(iv.length + ciphertext.byteLength);\n\tcombined.set(iv);\n\tcombined.set(new Uint8Array(ciphertext), iv.length);\n\n\treturn encodeBase64urlNoPadding(combined);\n}\n\n/**\n * Decrypt a value encrypted with encrypt()\n */\nexport async function decrypt(encrypted: string, secret: string): Promise<string> {\n\tconst key = await deriveKey(secret);\n\tconst combined = decodeBase64urlIgnorePadding(encrypted);\n\n\tconst iv = combined.slice(0, IV_BYTES);\n\tconst ciphertext = combined.slice(IV_BYTES);\n\n\tconst decrypted = await crypto.subtle.decrypt({ name: ALGORITHM, iv }, key, ciphertext);\n\n\treturn new TextDecoder().decode(decrypted);\n}\n","/**\n * Passkey registration (credential creation)\n *\n * Based on oslo webauthn documentation:\n * https://webauthn.oslojs.dev/examples/registration\n */\n\nimport { ECDSAPublicKey, p256 } from \"@oslojs/crypto/ecdsa\";\nimport { encodeBase64urlNoPadding, decodeBase64urlIgnorePadding } from \"@oslojs/encoding\";\nimport {\n\tparseAttestationObject,\n\tparseClientDataJSON,\n\tcoseAlgorithmES256,\n\tcoseAlgorithmRS256,\n\tcoseEllipticCurveP256,\n\tClientDataType,\n\tAttestationStatementFormat,\n\tCOSEKeyType,\n} from \"@oslojs/webauthn\";\n\nimport { generateToken } from \"../tokens.js\";\nimport type { Credential, NewCredential, AuthAdapter, User, DeviceType } from \"../types.js\";\nimport type {\n\tRegistrationOptions,\n\tRegistrationResponse,\n\tVerifiedRegistration,\n\tChallengeStore,\n\tPasskeyConfig,\n} from \"./types.js\";\n\nconst CHALLENGE_TTL = 5 * 60 * 1000; // 5 minutes\n\nexport type { PasskeyConfig };\n\n/**\n * Generate registration options for creating a new passkey\n */\nexport async function generateRegistrationOptions(\n\tconfig: PasskeyConfig,\n\tuser: Pick<User, \"id\" | \"email\" | \"name\">,\n\texistingCredentials: Credential[],\n\tchallengeStore: ChallengeStore,\n): Promise<RegistrationOptions> {\n\tconst challenge = generateToken();\n\n\t// Store challenge for verification\n\tawait challengeStore.set(challenge, {\n\t\ttype: \"registration\",\n\t\tuserId: user.id,\n\t\texpiresAt: Date.now() + CHALLENGE_TTL,\n\t});\n\n\t// Encode user ID as base64url\n\tconst userIdBytes = new TextEncoder().encode(user.id);\n\tconst userIdEncoded = encodeBase64urlNoPadding(userIdBytes);\n\n\treturn {\n\t\tchallenge,\n\t\trp: {\n\t\t\tname: config.rpName,\n\t\t\tid: config.rpId,\n\t\t},\n\t\tuser: {\n\t\t\tid: userIdEncoded,\n\t\t\tname: user.email,\n\t\t\tdisplayName: user.name || user.email,\n\t\t},\n\t\tpubKeyCredParams: [\n\t\t\t{ type: \"public-key\", alg: coseAlgorithmES256 }, // ES256 (-7)\n\t\t\t{ type: \"public-key\", alg: coseAlgorithmRS256 }, // RS256 (-257)\n\t\t],\n\t\ttimeout: 60000,\n\t\tattestation: \"none\", // We don't need attestation for our use case\n\t\tauthenticatorSelection: {\n\t\t\tresidentKey: \"preferred\", // Allow discoverable credentials\n\t\t\tuserVerification: \"preferred\",\n\t\t},\n\t\texcludeCredentials: existingCredentials.map((cred) => ({\n\t\t\ttype: \"public-key\" as const,\n\t\t\tid: cred.id,\n\t\t\ttransports: cred.transports,\n\t\t})),\n\t};\n}\n\n/**\n * Verify a registration response and extract credential data\n */\nexport async function verifyRegistrationResponse(\n\tconfig: PasskeyConfig,\n\tresponse: RegistrationResponse,\n\tchallengeStore: ChallengeStore,\n): Promise<VerifiedRegistration> {\n\t// Decode the response\n\tconst clientDataJSON = decodeBase64urlIgnorePadding(response.response.clientDataJSON);\n\tconst attestationObject = decodeBase64urlIgnorePadding(response.response.attestationObject);\n\n\t// Parse client data\n\tconst clientData = parseClientDataJSON(clientDataJSON);\n\n\t// Verify client data\n\tif (clientData.type !== ClientDataType.Create) {\n\t\tthrow new Error(\"Invalid client data type\");\n\t}\n\n\t// Verify challenge - convert Uint8Array back to base64url string (no padding, matching stored format)\n\tconst challengeString = encodeBase64urlNoPadding(clientData.challenge);\n\tconst challengeData = await challengeStore.get(challengeString);\n\tif (!challengeData) {\n\t\tthrow new Error(\"Challenge not found or expired\");\n\t}\n\tif (challengeData.type !== \"registration\") {\n\t\tthrow new Error(\"Invalid challenge type\");\n\t}\n\tif (challengeData.expiresAt < Date.now()) {\n\t\tawait challengeStore.delete(challengeString);\n\t\tthrow new Error(\"Challenge expired\");\n\t}\n\n\t// Delete challenge (single-use)\n\tawait challengeStore.delete(challengeString);\n\n\t// Verify origin\n\tif (clientData.origin !== config.origin) {\n\t\tthrow new Error(`Invalid origin: expected ${config.origin}, got ${clientData.origin}`);\n\t}\n\n\t// Parse attestation object\n\tconst attestation = parseAttestationObject(attestationObject);\n\n\t// We only support 'none' attestation for simplicity\n\tif (attestation.attestationStatement.format !== AttestationStatementFormat.None) {\n\t\t// For other formats, we'd need to verify the attestation statement\n\t\t// For now, we just ignore it and trust the credential\n\t}\n\n\tconst { authenticatorData } = attestation;\n\n\t// Verify RP ID hash\n\tif (!authenticatorData.verifyRelyingPartyIdHash(config.rpId)) {\n\t\tthrow new Error(\"Invalid RP ID hash\");\n\t}\n\n\t// Verify flags\n\tif (!authenticatorData.userPresent) {\n\t\tthrow new Error(\"User presence not verified\");\n\t}\n\n\t// Extract credential data\n\tif (!authenticatorData.credential) {\n\t\tthrow new Error(\"No credential data in attestation\");\n\t}\n\n\tconst { credential } = authenticatorData;\n\n\t// Verify algorithm is supported and encode public key\n\t// Currently only supporting ES256 (ECDSA with P-256)\n\tconst algorithm = credential.publicKey.algorithm();\n\tlet encodedPublicKey: Uint8Array;\n\n\tif (algorithm === coseAlgorithmES256) {\n\t\t// Verify it's EC2 key type\n\t\tif (credential.publicKey.type() !== COSEKeyType.EC2) {\n\t\t\tthrow new Error(\"Expected EC2 key type for ES256\");\n\t\t}\n\t\tconst cosePublicKey = credential.publicKey.ec2();\n\t\tif (cosePublicKey.curve !== coseEllipticCurveP256) {\n\t\t\tthrow new Error(\"Expected P-256 curve for ES256\");\n\t\t}\n\t\t// Encode as SEC1 uncompressed format for storage\n\t\tencodedPublicKey = new ECDSAPublicKey(\n\t\t\tp256,\n\t\t\tcosePublicKey.x,\n\t\t\tcosePublicKey.y,\n\t\t).encodeSEC1Uncompressed();\n\t} else if (algorithm === coseAlgorithmRS256) {\n\t\t// RSA is less common for passkeys, skip for now\n\t\tthrow new Error(\"RS256 not yet supported - please use ES256\");\n\t} else {\n\t\tthrow new Error(`Unsupported algorithm: ${algorithm}`);\n\t}\n\n\t// Determine device type and backup status\n\t// Note: oslo webauthn doesn't expose backup flags, so we default to singleDevice\n\t// In practice, most modern passkeys are multi-device (e.g., iCloud Keychain, Google Password Manager)\n\tconst deviceType: DeviceType = \"singleDevice\";\n\tconst backedUp = false;\n\n\treturn {\n\t\tcredentialId: response.id,\n\t\tpublicKey: encodedPublicKey,\n\t\tcounter: authenticatorData.signatureCounter,\n\t\tdeviceType,\n\t\tbackedUp,\n\t\ttransports: response.response.transports ?? [],\n\t};\n}\n\n/**\n * Register a new passkey for a user\n */\nexport async function registerPasskey(\n\tadapter: AuthAdapter,\n\tuserId: string,\n\tverified: VerifiedRegistration,\n\tname?: string,\n): Promise<Credential> {\n\t// Check credential limit\n\tconst count = await adapter.countCredentialsByUserId(userId);\n\tif (count >= 10) {\n\t\tthrow new Error(\"Maximum number of passkeys reached (10)\");\n\t}\n\n\t// Check if credential already exists\n\tconst existing = await adapter.getCredentialById(verified.credentialId);\n\tif (existing) {\n\t\tthrow new Error(\"Credential already registered\");\n\t}\n\n\tconst newCredential: NewCredential = {\n\t\tid: verified.credentialId,\n\t\tuserId,\n\t\tpublicKey: verified.publicKey,\n\t\tcounter: verified.counter,\n\t\tdeviceType: verified.deviceType,\n\t\tbackedUp: verified.backedUp,\n\t\ttransports: verified.transports,\n\t\tname,\n\t};\n\n\treturn adapter.createCredential(newCredential);\n}\n","/**\n * Passkey authentication (credential assertion)\n *\n * Based on oslo webauthn documentation:\n * https://webauthn.oslojs.dev/examples/authentication\n */\n\nimport {\n\tverifyECDSASignature,\n\tp256,\n\tdecodeSEC1PublicKey,\n\tdecodePKIXECDSASignature,\n} from \"@oslojs/crypto/ecdsa\";\nimport { sha256 } from \"@oslojs/crypto/sha2\";\nimport { encodeBase64urlNoPadding, decodeBase64urlIgnorePadding } from \"@oslojs/encoding\";\nimport {\n\tparseAuthenticatorData,\n\tparseClientDataJSON,\n\tClientDataType,\n\tcreateAssertionSignatureMessage,\n} from \"@oslojs/webauthn\";\n\nimport { generateToken } from \"../tokens.js\";\nimport type { Credential, AuthAdapter, User } from \"../types.js\";\nimport type {\n\tAuthenticationOptions,\n\tAuthenticationResponse,\n\tVerifiedAuthentication,\n\tChallengeStore,\n\tPasskeyConfig,\n} from \"./types.js\";\n\nconst CHALLENGE_TTL = 5 * 60 * 1000; // 5 minutes\n\n/**\n * Generate authentication options for signing in with a passkey\n */\nexport async function generateAuthenticationOptions(\n\tconfig: PasskeyConfig,\n\tcredentials: Credential[],\n\tchallengeStore: ChallengeStore,\n): Promise<AuthenticationOptions> {\n\tconst challenge = generateToken();\n\n\t// Store challenge for verification\n\tawait challengeStore.set(challenge, {\n\t\ttype: \"authentication\",\n\t\texpiresAt: Date.now() + CHALLENGE_TTL,\n\t});\n\n\treturn {\n\t\tchallenge,\n\t\trpId: config.rpId,\n\t\ttimeout: 60000,\n\t\tuserVerification: \"preferred\",\n\t\tallowCredentials:\n\t\t\tcredentials.length > 0\n\t\t\t\t? credentials.map((cred) => ({\n\t\t\t\t\t\ttype: \"public-key\" as const,\n\t\t\t\t\t\tid: cred.id,\n\t\t\t\t\t\ttransports: cred.transports,\n\t\t\t\t\t}))\n\t\t\t\t: undefined, // Empty = allow any discoverable credential\n\t};\n}\n\n/**\n * Verify an authentication response\n */\nexport async function verifyAuthenticationResponse(\n\tconfig: PasskeyConfig,\n\tresponse: AuthenticationResponse,\n\tcredential: Credential,\n\tchallengeStore: ChallengeStore,\n): Promise<VerifiedAuthentication> {\n\t// Decode the response\n\tconst clientDataJSON = decodeBase64urlIgnorePadding(response.response.clientDataJSON);\n\tconst authenticatorData = decodeBase64urlIgnorePadding(response.response.authenticatorData);\n\tconst signature = decodeBase64urlIgnorePadding(response.response.signature);\n\n\t// Parse client data\n\tconst clientData = parseClientDataJSON(clientDataJSON);\n\n\t// Verify client data type\n\tif (clientData.type !== ClientDataType.Get) {\n\t\tthrow new Error(\"Invalid client data type\");\n\t}\n\n\t// Verify challenge - convert Uint8Array back to base64url string (no padding, matching stored format)\n\tconst challengeString = encodeBase64urlNoPadding(clientData.challenge);\n\tconst challengeData = await challengeStore.get(challengeString);\n\tif (!challengeData) {\n\t\tthrow new Error(\"Challenge not found or expired\");\n\t}\n\tif (challengeData.type !== \"authentication\") {\n\t\tthrow new Error(\"Invalid challenge type\");\n\t}\n\tif (challengeData.expiresAt < Date.now()) {\n\t\tawait challengeStore.delete(challengeString);\n\t\tthrow new Error(\"Challenge expired\");\n\t}\n\n\t// Delete challenge (single-use)\n\tawait challengeStore.delete(challengeString);\n\n\t// Verify origin\n\tif (clientData.origin !== config.origin) {\n\t\tthrow new Error(`Invalid origin: expected ${config.origin}, got ${clientData.origin}`);\n\t}\n\n\t// Parse authenticator data\n\tconst authData = parseAuthenticatorData(authenticatorData);\n\n\t// Verify RP ID hash\n\tif (!authData.verifyRelyingPartyIdHash(config.rpId)) {\n\t\tthrow new Error(\"Invalid RP ID hash\");\n\t}\n\n\t// Verify flags\n\tif (!authData.userPresent) {\n\t\tthrow new Error(\"User presence not verified\");\n\t}\n\n\t// Verify counter (prevent replay attacks)\n\tif (authData.signatureCounter !== 0 && authData.signatureCounter <= credential.counter) {\n\t\tthrow new Error(\"Invalid signature counter - possible cloned authenticator\");\n\t}\n\n\t// Create the message that was signed\n\tconst signatureMessage = createAssertionSignatureMessage(authenticatorData, clientDataJSON);\n\n\t// Ensure public key is a Uint8Array (may come as Buffer from some DB drivers)\n\tconst publicKeyBytes =\n\t\tcredential.publicKey instanceof Uint8Array\n\t\t\t? credential.publicKey\n\t\t\t: new Uint8Array(credential.publicKey);\n\n\t// Decode the stored SEC1-encoded public key and verify signature\n\t// The signature from WebAuthn is DER-encoded (PKIX format)\n\tconst ecdsaPublicKey = decodeSEC1PublicKey(p256, publicKeyBytes);\n\tconst ecdsaSignature = decodePKIXECDSASignature(signature);\n\tconst hash = sha256(signatureMessage);\n\tconst signatureValid = verifyECDSASignature(ecdsaPublicKey, hash, ecdsaSignature);\n\n\tif (!signatureValid) {\n\t\tthrow new Error(\"Invalid signature\");\n\t}\n\n\treturn {\n\t\tcredentialId: response.id,\n\t\tnewCounter: authData.signatureCounter,\n\t};\n}\n\n/**\n * Authenticate a user with a passkey\n */\nexport async function authenticateWithPasskey(\n\tconfig: PasskeyConfig,\n\tadapter: AuthAdapter,\n\tresponse: AuthenticationResponse,\n\tchallengeStore: ChallengeStore,\n): Promise<User> {\n\t// Find the credential\n\tconst credential = await adapter.getCredentialById(response.id);\n\tif (!credential) {\n\t\tthrow new Error(\"Credential not found\");\n\t}\n\n\t// Verify the response\n\tconst verified = await verifyAuthenticationResponse(config, response, credential, challengeStore);\n\n\t// Update counter\n\tawait adapter.updateCredentialCounter(verified.credentialId, verified.newCounter);\n\n\t// Get the user\n\tconst user = await adapter.getUserById(credential.userId);\n\tif (!user) {\n\t\tthrow new Error(\"User not found\");\n\t}\n\n\treturn user;\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../src/config.ts","../src/tokens.ts","../src/rbac.ts","../src/magic-link/index.ts","../src/invite.ts","../src/signup.ts","../src/oauth/consumer.ts","../src/index.ts"],"mappings":"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"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/config.ts","../src/tokens.ts","../src/rbac.ts","../src/magic-link/index.ts","../src/invite.ts","../src/signup.ts","../src/oauth/consumer.ts","../src/index.ts"],"mappings":";;;;;;;;;;;cA4Ba,gBAAA,EAAgB,CAAA,CAAA,SAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAwFjB,UAAA,GAAa,CAAA,CAAE,KAAA,QAAa,gBAAA;;;;UAKvB,kBAAA;EAChB,MAAA;EACA,OAAA;EACA,QAAA;EAEA,QAAA;IACC,MAAA;IACA,IAAA;IACA,MAAA;EAAA;EAGD,UAAA;IACC,OAAA;IACA,WAAA,EAAa,QAAA;EAAA;EAGd,KAAA;IACC,MAAA;MACC,QAAA;MACA,YAAA;IAAA;IAED,MAAA;MACC,QAAA;MACA,YAAA;IAAA;EAAA;EAIF,QAAA;IACC,OAAA;IACA,MAAA;EAAA;EAGD,GAAA;IACC,OAAA;EAAA;EAGD,OAAA;IACC,MAAA;IACA,OAAA;EAAA;AAAA;;;;iBAac,aAAA,CACf,MAAA,EAAQ,UAAA,EACR,OAAA,UACA,QAAA,WACE,kBAAA;;;;;;;;;;;cC5JU,cAAA;EAAA;;;;;cAWA,YAAA;AAAA,KAUD,aAAA,WAAwB,YAAA;;;;;iBAMpB,cAAA,CAAe,MAAA;;;;;iBASf,QAAA,CAAS,MAAA,YAAkB,QAAA;;;;;iBAS3B,aAAA,CAAA;;;;;iBAUA,SAAA,CAAU,KAAA;;;;iBASV,qBAAA,CAAA;EAA2B,KAAA;EAAe,IAAA;AAAA;;;;iBAS1C,iBAAA,CAAA;;;;iBASA,kBAAA,CAAA;;;;;;;;iBAiBA,qBAAA,CAAsB,MAAA;EACrC,GAAA;EACA,IAAA;EACA,MAAA;AAAA;;;;;iBAmBe,iBAAA,CAAkB,KAAA;;;;;;;iBAgBlB,oBAAA,CAAqB,YAAA;;;;iBAQrB,aAAA,CAAc,CAAA,UAAW,CAAA;;;;iBAyCnB,OAAA,CAAQ,SAAA,UAAmB,MAAA,WAAiB,OAAA;;;;iBAkB5C,OAAA,CAAQ,SAAA,UAAmB,MAAA,WAAiB,OAAA;;;;;;cCtNrD,WAAA;EAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KA0ED,UAAA,gBAA0B,WAAA;;;;iBAKtB,aAAA,CACf,IAAA;EAAQ,IAAA,EAAM,SAAA;AAAA,sBACd,UAAA,EAAY,UAAA;;;;iBASG,iBAAA,CACf,IAAA;EAAQ,IAAA,EAAM,SAAA;AAAA,sBACd,UAAA,EAAY,UAAA,WACF,IAAA;EAAU,IAAA,EAAM,SAAA;AAAA;;;;iBAYX,WAAA,CACf,IAAA;EAAQ,IAAA,EAAM,SAAA;EAAW,EAAA;AAAA,sBACzB,OAAA,UACA,aAAA,EAAe,UAAA,EACf,aAAA,EAAe,UAAA;;;;iBAYA,2BAAA,CACf,IAAA;EAAQ,IAAA,EAAM,SAAA;EAAW,EAAA;AAAA,sBACzB,OAAA,UACA,aAAA,EAAe,UAAA,EACf,aAAA,EAAe,UAAA,WACL,IAAA;EAAU,IAAA,EAAM,SAAA;EAAW,EAAA;AAAA;AAAA,cASzB,eAAA,SAAwB,KAAA;EAE5B,IAAA;cAAA,IAAA,gCACP,OAAA;AAAA;;;;;;;iBAqCc,aAAA,CAAc,IAAA,EAAM,SAAA,GAAY,aAAA;;;;;;;;iBAgBhC,WAAA,CAAY,SAAA,YAAqB,IAAA,EAAM,SAAA;;;;KC9L3C,aAAA,IAAe,OAAA,EAAS,YAAA,KAAiB,OAAA;AAAA,UAEpC,eAAA;EAChB,OAAA;EACA,QAAA;EHmGC;EGjGD,KAAA,GAAQ,aAAA;AAAA;;;;;;iBAiBa,aAAA,CACrB,MAAA,EAAQ,eAAA,EACR,OAAA,EAAS,WAAA,EACT,KAAA,UACA,IAAA,+BACE,OAAA;;;;iBA0DmB,eAAA,CAAgB,OAAA,EAAS,WAAA,EAAa,KAAA,WAAgB,OAAA,CAAQ,IAAA;AAAA,cA4CvE,cAAA,SAAuB,KAAA;EAE3B,IAAA;cAAA,IAAA,iFACP,OAAA;AAAA;;;;iBCxIc,UAAA,CAAW,CAAA;;KAWf,WAAA,IAAe,OAAA,EAAS,YAAA,KAAiB,OAAA;AAAA,UAEpC,YAAA;EAChB,OAAA;EACA,QAAA;;EAEA,KAAA,GAAQ,WAAA;AAAA;;UAIQ,iBAAA;;EAEhB,GAAA;;EAEA,KAAA;AAAA;;;;;;;;iBAUqB,iBAAA,CACrB,MAAA,EAAQ,IAAA,CAAK,YAAA,cACb,OAAA,EAAS,WAAA,EACT,KAAA,UACA,IAAA,EAAM,SAAA,EACN,SAAA,WACE,OAAA,CAAQ,iBAAA;;;;;;;;iBA8DW,YAAA,CACrB,MAAA,EAAQ,YAAA,EACR,OAAA,EAAS,WAAA,EACT,KAAA,UACA,IAAA,EAAM,SAAA,EACN,SAAA,WACE,OAAA,CAAQ,iBAAA;;;;iBAeW,cAAA,CACrB,OAAA,EAAS,WAAA,EACT,KAAA,WACE,OAAA;EAAU,KAAA;EAAe,IAAA,EAAM,SAAA;AAAA;;;;iBA0BZ,cAAA,CACrB,OAAA,EAAS,WAAA,EACT,KAAA,UACA,QAAA;EACC,IAAA;EACA,SAAA;AAAA,IAEC,OAAA,CAAQ,IAAA;AAAA,cA4BE,WAAA,SAAoB,KAAA;EAExB,IAAA;cAAA,IAAA,qDACP,OAAA;AAAA;;;;KC5LU,aAAA,IAAe,OAAA,EAAS,YAAA,KAAiB,OAAA;AAAA,UAWpC,YAAA;EAChB,OAAA;EACA,QAAA;EL0FC;EKxFD,KAAA,GAAQ,aAAA;AAAA;;;;iBAMa,SAAA,CACrB,OAAA,EAAS,WAAA,EACT,KAAA,WACE,OAAA;EAAU,OAAA;EAAkB,IAAA,EAAM,SAAA;AAAA;;;;;;iBAoBf,aAAA,CACrB,MAAA,EAAQ,YAAA,EACR,OAAA,EAAS,WAAA,EACT,KAAA,WACE,OAAA;;;;iBAkEmB,mBAAA,CACrB,OAAA,EAAS,WAAA,EACT,KAAA,WACE,OAAA;EAAU,KAAA;EAAe,IAAA,EAAM,SAAA;AAAA;;;;iBA0BZ,cAAA,CACrB,OAAA,EAAS,WAAA,EACT,KAAA,UACA,QAAA;EACC,IAAA;EACA,SAAA;AAAA,IAEC,OAAA,CAAQ,IAAA;AAAA,cAmCE,WAAA,SAAoB,KAAA;EAExB,IAAA;cAAA,IAAA,qGAMP,OAAA;AAAA;;;UC7Le,mBAAA;EAChB,OAAA;EACA,SAAA;IACC,MAAA,GAAS,WAAA;IACT,MAAA,GAAS,WAAA;EAAA;;;;EAKV,aAAA,IAAiB,KAAA,aAAkB,OAAA;IAAU,OAAA;IAAkB,IAAA,EAAM,SAAA;EAAA;AAAA;;;;iBAMhD,sBAAA,CACrB,MAAA,EAAQ,mBAAA,EACR,YAAA,uBACA,UAAA,EAAY,UAAA,GACV,OAAA;EAAU,GAAA;EAAa,KAAA;AAAA;;;;iBA0CJ,mBAAA,CACrB,MAAA,EAAQ,mBAAA,EACR,OAAA,EAAS,WAAA,EACT,YAAA,uBACA,IAAA,UACA,KAAA,UACA,UAAA,EAAY,UAAA,GACV,OAAA,CAAQ,IAAA;AAAA,UA4NM,UAAA;EAChB,GAAA,CAAI,KAAA,UAAe,IAAA,EAAM,UAAA,GAAa,OAAA;EACtC,GAAA,CAAI,KAAA,WAAgB,OAAA,CAAQ,UAAA;EAC5B,MAAA,CAAO,KAAA,WAAgB,OAAA;AAAA;AAAA,cAOX,UAAA,SAAmB,KAAA;EAEvB,IAAA;cAAA,IAAA,gHAMP,OAAA;AAAA;;;;;;;;iBClMc,IAAA,CAAK,MAAA,EAAD,UAAA,GAAyC,UAAA"}
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import { a as toDeviceType, i as roleToLevel, n as Role, o as toRoleLevel, r as roleFromLevel, s as toTokenType, t as AuthError } from "./types-ndj-bYfi.mjs";
2
- import { _ as hasScope, a as registerPasskey, b as secureCompare, c as VALID_SCOPES, d as encrypt, f as generateAuthSecret, g as generateTokenWithHash, h as generateToken, i as generateRegistrationOptions, l as computeS256Challenge, m as generateSessionId, n as generateAuthenticationOptions, o as verifyRegistrationResponse, p as generatePrefixedToken, r as verifyAuthenticationResponse, s as TOKEN_PREFIXES, t as authenticateWithPasskey, u as decrypt, v as hashPrefixedToken, x as validateScopes, y as hashToken } from "./authenticate-j5GayLXB.mjs";
2
+ import { _ as hasScope, a as registerPasskey, b as secureCompare, c as VALID_SCOPES, d as encrypt, f as generateAuthSecret, g as generateTokenWithHash, h as generateToken, i as generateRegistrationOptions, l as computeS256Challenge, m as generateSessionId, n as generateAuthenticationOptions, o as verifyRegistrationResponse, p as generatePrefixedToken, r as verifyAuthenticationResponse, s as TOKEN_PREFIXES, t as authenticateWithPasskey, u as decrypt, v as hashPrefixedToken, x as validateScopes, y as hashToken } from "./authenticate-CZ5fe42l.mjs";
3
3
  import "./passkey/index.mjs";
4
4
  import { fetchGitHubEmail, github } from "./oauth/providers/github.mjs";
5
5
  import { google } from "./oauth/providers/google.mjs";
@@ -242,7 +242,7 @@ async function createInviteToken(config, adapter, email, role, invitedBy) {
242
242
  invitedBy,
243
243
  expiresAt: new Date(Date.now() + TOKEN_EXPIRY_MS$2)
244
244
  });
245
- const url = new URL("/api/auth/invite/accept", config.baseUrl);
245
+ const url = new URL("/_emdash/api/auth/invite/accept", config.baseUrl);
246
246
  url.searchParams.set("token", token);
247
247
  return {
248
248
  url: url.toString(),
@@ -367,7 +367,7 @@ async function sendMagicLink(config, adapter, email, type = "magic_link") {
367
367
  type,
368
368
  expiresAt: new Date(Date.now() + TOKEN_EXPIRY_MS$1)
369
369
  });
370
- const url = new URL("/api/auth/magic-link/verify", config.baseUrl);
370
+ const url = new URL("/_emdash/api/auth/magic-link/verify", config.baseUrl);
371
371
  url.searchParams.set("token", token);
372
372
  const safeName = escapeHtml(config.siteName);
373
373
  await config.email({
@@ -476,7 +476,7 @@ async function requestSignup(config, adapter, email) {
476
476
  role: signup.role,
477
477
  expiresAt: new Date(Date.now() + TOKEN_EXPIRY_MS)
478
478
  });
479
- const url = new URL("/api/auth/signup/verify", config.baseUrl);
479
+ const url = new URL("/_emdash/api/auth/signup/verify", config.baseUrl);
480
480
  url.searchParams.set("token", token);
481
481
  const safeName = escapeHtml(config.siteName);
482
482
  await config.email({
@@ -561,7 +561,7 @@ async function createAuthorizationUrl(config, providerName, stateStore) {
561
561
  if (!providerConfig) throw new Error(`OAuth provider ${providerName} not configured`);
562
562
  const provider = getProvider(providerName);
563
563
  const state = generateState();
564
- const redirectUri = `${config.baseUrl}/api/auth/oauth/${providerName}/callback`;
564
+ const redirectUri = new URL(`/_emdash/api/auth/oauth/${providerName}/callback`, config.baseUrl).toString();
565
565
  const codeVerifier = generateCodeVerifier();
566
566
  const codeChallenge = await generateCodeChallenge(codeVerifier);
567
567
  await stateStore.set(state, {
@@ -1 +1 @@
1
- {"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: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\"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// 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// 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\tif (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<ApiTokenScope, 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\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-eslint(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((s) => allowed.has(s));\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\n\tconst url = new URL(\"/api/auth/invite/accept\", config.baseUrl);\n\turl.searchParams.set(\"token\", token);\n\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(\"/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(\"/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 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): 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 = `${config.baseUrl}/api/auth/oauth/${providerName}/callback`;\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});\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// Find or create user\n\treturn findOrCreateUser(config, adapter, providerName, profile);\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},\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 * Find existing user or create new one (with auto-linking)\n */\nasync function findOrCreateUser(\n\tconfig: OAuthConsumerConfig,\n\tadapter: AuthAdapter,\n\tproviderName: string,\n\tprofile: OAuthProfile,\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 (config.canSelfSignup) {\n\t\tconst signup = await config.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\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\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\tOAuthError,\n\tgithub,\n\tgoogle,\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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"}
1
+ {"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: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\"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// 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// 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\tif (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<ApiTokenScope, 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\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-eslint(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((s) => allowed.has(s));\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\n\tconst url = new URL(\"/_emdash/api/auth/invite/accept\", config.baseUrl);\n\turl.searchParams.set(\"token\", token);\n\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 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): 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});\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// Find or create user\n\treturn findOrCreateUser(config, adapter, providerName, profile);\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},\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 * Find existing user or create new one (with auto-linking)\n */\nasync function findOrCreateUser(\n\tconfig: OAuthConsumerConfig,\n\tadapter: AuthAdapter,\n\tproviderName: string,\n\tprofile: OAuthProfile,\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 (config.canSelfSignup) {\n\t\tconst signup = await config.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\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\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\tOAuthError,\n\tgithub,\n\tgoogle,\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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"}
@@ -1,3 +1,3 @@
1
- import { a as registerPasskey, i as generateRegistrationOptions, n as generateAuthenticationOptions, o as verifyRegistrationResponse, r as verifyAuthenticationResponse, t as authenticateWithPasskey } from "../authenticate-j5GayLXB.mjs";
1
+ import { a as registerPasskey, i as generateRegistrationOptions, n as generateAuthenticationOptions, o as verifyRegistrationResponse, r as verifyAuthenticationResponse, t as authenticateWithPasskey } from "../authenticate-CZ5fe42l.mjs";
2
2
 
3
3
  export { authenticateWithPasskey, generateAuthenticationOptions, generateRegistrationOptions, registerPasskey, verifyAuthenticationResponse, verifyRegistrationResponse };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@emdash-cms/auth",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Passkey-first authentication for EmDash",
5
5
  "type": "module",
6
6
  "main": "dist/index.mjs",
package/src/invite.ts CHANGED
@@ -68,7 +68,7 @@ export async function createInviteToken(
68
68
  });
69
69
 
70
70
  // Build invite URL
71
- const url = new URL("/api/auth/invite/accept", config.baseUrl);
71
+ const url = new URL("/_emdash/api/auth/invite/accept", config.baseUrl);
72
72
  url.searchParams.set("token", token);
73
73
 
74
74
  return { url: url.toString(), email };
@@ -63,7 +63,7 @@ export async function sendMagicLink(
63
63
  });
64
64
 
65
65
  // Build magic link URL
66
- const url = new URL("/api/auth/magic-link/verify", config.baseUrl);
66
+ const url = new URL("/_emdash/api/auth/magic-link/verify", config.baseUrl);
67
67
  url.searchParams.set("token", token);
68
68
 
69
69
  // Send email
@@ -40,7 +40,10 @@ export async function createAuthorizationUrl(
40
40
 
41
41
  const provider = getProvider(providerName);
42
42
  const state = generateState();
43
- const redirectUri = `${config.baseUrl}/api/auth/oauth/${providerName}/callback`;
43
+ const redirectUri = new URL(
44
+ `/_emdash/api/auth/oauth/${providerName}/callback`,
45
+ config.baseUrl,
46
+ ).toString();
44
47
 
45
48
  // Generate PKCE code verifier for providers that support it
46
49
  const codeVerifier = generateCodeVerifier();
package/src/signup.ts CHANGED
@@ -91,7 +91,7 @@ export async function requestSignup(
91
91
  });
92
92
 
93
93
  // Build verification URL
94
- const url = new URL("/api/auth/signup/verify", config.baseUrl);
94
+ const url = new URL("/_emdash/api/auth/signup/verify", config.baseUrl);
95
95
  url.searchParams.set("token", token);
96
96
 
97
97
  // Send email
package/src/tokens.ts CHANGED
@@ -6,7 +6,9 @@
6
6
  * Tokens are opaque random values. We store only the SHA-256 hash in the database.
7
7
  */
8
8
 
9
- import { sha256 } from "@oslojs/crypto/sha2";
9
+ import { hmac } from "@oslojs/crypto/hmac";
10
+ import { sha256, SHA256 } from "@oslojs/crypto/sha2";
11
+ import { constantTimeEqual } from "@oslojs/crypto/subtle";
10
12
  import { encodeBase64urlNoPadding, decodeBase64urlIgnorePadding } from "@oslojs/encoding";
11
13
 
12
14
  const TOKEN_BYTES = 32; // 256 bits of entropy
@@ -162,16 +164,11 @@ export function computeS256Challenge(codeVerifier: string): string {
162
164
  * Constant-time comparison to prevent timing attacks
163
165
  */
164
166
  export function secureCompare(a: string, b: string): boolean {
165
- if (a.length !== b.length) return false;
167
+ const text = new TextEncoder();
168
+ const salt = crypto.getRandomValues(new Uint8Array(TOKEN_BYTES));
169
+ const hash = (str: string) => hmac(SHA256, salt, text.encode(str));
166
170
 
167
- const aBytes = new TextEncoder().encode(a);
168
- const bBytes = new TextEncoder().encode(b);
169
-
170
- let result = 0;
171
- for (let i = 0; i < aBytes.length; i++) {
172
- result |= aBytes[i]! ^ bBytes[i]!;
173
- }
174
- return result === 0;
171
+ return constantTimeEqual(hash(a), hash(b));
175
172
  }
176
173
 
177
174
  // ============================================================================
@@ -1 +0,0 @@
1
- {"version":3,"file":"authenticate-j5GayLXB.mjs","names":["CHALLENGE_TTL"],"sources":["../src/tokens.ts","../src/passkey/register.ts","../src/passkey/authenticate.ts"],"sourcesContent":["/**\n * Secure token utilities\n *\n * Crypto via Oslo.js (@oslojs/crypto). Base64url via @oslojs/encoding.\n *\n * Tokens are opaque random values. We store only the SHA-256 hash in the database.\n */\n\nimport { sha256 } from \"@oslojs/crypto/sha2\";\nimport { encodeBase64urlNoPadding, decodeBase64urlIgnorePadding } from \"@oslojs/encoding\";\n\nconst TOKEN_BYTES = 32; // 256 bits of entropy\n\n// ---------------------------------------------------------------------------\n// API Token Prefixes\n// ---------------------------------------------------------------------------\n\n/** Valid API token prefixes */\nexport const TOKEN_PREFIXES = {\n\tPAT: \"ec_pat_\",\n\tOAUTH_ACCESS: \"ec_oat_\",\n\tOAUTH_REFRESH: \"ec_ort_\",\n} as const;\n\n// ---------------------------------------------------------------------------\n// Scopes\n// ---------------------------------------------------------------------------\n\n/** All valid API token scopes */\nexport const VALID_SCOPES = [\n\t\"content:read\",\n\t\"content:write\",\n\t\"media:read\",\n\t\"media:write\",\n\t\"schema:read\",\n\t\"schema:write\",\n\t\"admin\",\n] as const;\n\nexport type ApiTokenScope = (typeof VALID_SCOPES)[number];\n\n/**\n * Validate that scopes are all valid.\n * Returns the invalid scopes, or empty array if all valid.\n */\nexport function validateScopes(scopes: string[]): string[] {\n\tconst validSet = new Set<string>(VALID_SCOPES);\n\treturn scopes.filter((s) => !validSet.has(s));\n}\n\n/**\n * Check if a set of scopes includes a required scope.\n * The `admin` scope grants access to everything.\n */\nexport function hasScope(scopes: string[], required: string): boolean {\n\tif (scopes.includes(\"admin\")) return true;\n\treturn scopes.includes(required);\n}\n\n/**\n * Generate a cryptographically secure random token\n * Returns base64url-encoded string (URL-safe)\n */\nexport function generateToken(): string {\n\tconst bytes = new Uint8Array(TOKEN_BYTES);\n\tcrypto.getRandomValues(bytes);\n\treturn encodeBase64urlNoPadding(bytes);\n}\n\n/**\n * Hash a token for storage\n * We never store raw tokens - only their SHA-256 hash\n */\nexport function hashToken(token: string): string {\n\tconst bytes = decodeBase64urlIgnorePadding(token);\n\tconst hash = sha256(bytes);\n\treturn encodeBase64urlNoPadding(hash);\n}\n\n/**\n * Generate a token and its hash together\n */\nexport function generateTokenWithHash(): { token: string; hash: string } {\n\tconst token = generateToken();\n\tconst hash = hashToken(token);\n\treturn { token, hash };\n}\n\n/**\n * Generate a session ID (shorter, for cookie storage)\n */\nexport function generateSessionId(): string {\n\tconst bytes = new Uint8Array(20); // 160 bits\n\tcrypto.getRandomValues(bytes);\n\treturn encodeBase64urlNoPadding(bytes);\n}\n\n/**\n * Generate an auth secret for configuration\n */\nexport function generateAuthSecret(): string {\n\tconst bytes = new Uint8Array(32);\n\tcrypto.getRandomValues(bytes);\n\treturn encodeBase64urlNoPadding(bytes);\n}\n\n// ---------------------------------------------------------------------------\n// Prefixed API tokens (ec_pat_, ec_oat_, ec_ort_)\n// ---------------------------------------------------------------------------\n\n/**\n * Generate a prefixed API token and its hash.\n * Returns the raw token (shown once to the user), the hash (stored server-side),\n * and a display prefix (for identification in UIs/logs).\n *\n * Uses oslo/crypto for SHA-256 hashing.\n */\nexport function generatePrefixedToken(prefix: string): {\n\traw: string;\n\thash: string;\n\tprefix: string;\n} {\n\tconst bytes = new Uint8Array(TOKEN_BYTES);\n\tcrypto.getRandomValues(bytes);\n\n\tconst encoded = encodeBase64urlNoPadding(bytes);\n\tconst raw = `${prefix}${encoded}`;\n\tconst hash = hashPrefixedToken(raw);\n\n\t// First few chars for identification in UIs\n\tconst displayPrefix = raw.slice(0, prefix.length + 4);\n\n\treturn { raw, hash, prefix: displayPrefix };\n}\n\n/**\n * Hash a prefixed API token for storage/lookup.\n * Hashes the full prefixed token string via SHA-256, returns base64url (no padding).\n */\nexport function hashPrefixedToken(token: string): string {\n\tconst bytes = new TextEncoder().encode(token);\n\tconst hash = sha256(bytes);\n\treturn encodeBase64urlNoPadding(hash);\n}\n\n// ---------------------------------------------------------------------------\n// PKCE (RFC 7636) — server-side verification\n// ---------------------------------------------------------------------------\n\n/**\n * Compute an S256 PKCE code challenge from a code verifier.\n * Used server-side to verify that code_verifier matches the stored code_challenge.\n *\n * Equivalent to: BASE64URL(SHA256(ASCII(code_verifier)))\n */\nexport function computeS256Challenge(codeVerifier: string): string {\n\tconst hash = sha256(new TextEncoder().encode(codeVerifier));\n\treturn encodeBase64urlNoPadding(hash);\n}\n\n/**\n * Constant-time comparison to prevent timing attacks\n */\nexport function secureCompare(a: string, b: string): boolean {\n\tif (a.length !== b.length) return false;\n\n\tconst aBytes = new TextEncoder().encode(a);\n\tconst bBytes = new TextEncoder().encode(b);\n\n\tlet result = 0;\n\tfor (let i = 0; i < aBytes.length; i++) {\n\t\tresult |= aBytes[i]! ^ bBytes[i]!;\n\t}\n\treturn result === 0;\n}\n\n// ============================================================================\n// Encryption utilities (for storing OAuth secrets)\n// ============================================================================\n\nconst ALGORITHM = \"AES-GCM\";\nconst IV_BYTES = 12;\n\n/**\n * Derive an encryption key from the auth secret\n */\nasync function deriveKey(secret: string): Promise<CryptoKey> {\n\tconst decoded = decodeBase64urlIgnorePadding(secret);\n\t// Create a new ArrayBuffer to ensure compatibility with crypto.subtle\n\tconst buffer = new Uint8Array(decoded).buffer;\n\tconst keyMaterial = await crypto.subtle.importKey(\"raw\", buffer, \"PBKDF2\", false, [\"deriveKey\"]);\n\n\treturn crypto.subtle.deriveKey(\n\t\t{\n\t\t\tname: \"PBKDF2\",\n\t\t\tsalt: new TextEncoder().encode(\"emdash-auth-v1\"),\n\t\t\titerations: 100000,\n\t\t\thash: \"SHA-256\",\n\t\t},\n\t\tkeyMaterial,\n\t\t{ name: ALGORITHM, length: 256 },\n\t\tfalse,\n\t\t[\"encrypt\", \"decrypt\"],\n\t);\n}\n\n/**\n * Encrypt a value using AES-GCM\n */\nexport async function encrypt(plaintext: string, secret: string): Promise<string> {\n\tconst key = await deriveKey(secret);\n\tconst iv = crypto.getRandomValues(new Uint8Array(IV_BYTES));\n\tconst encoded = new TextEncoder().encode(plaintext);\n\n\tconst ciphertext = await crypto.subtle.encrypt({ name: ALGORITHM, iv }, key, encoded);\n\n\t// Prepend IV to ciphertext\n\tconst combined = new Uint8Array(iv.length + ciphertext.byteLength);\n\tcombined.set(iv);\n\tcombined.set(new Uint8Array(ciphertext), iv.length);\n\n\treturn encodeBase64urlNoPadding(combined);\n}\n\n/**\n * Decrypt a value encrypted with encrypt()\n */\nexport async function decrypt(encrypted: string, secret: string): Promise<string> {\n\tconst key = await deriveKey(secret);\n\tconst combined = decodeBase64urlIgnorePadding(encrypted);\n\n\tconst iv = combined.slice(0, IV_BYTES);\n\tconst ciphertext = combined.slice(IV_BYTES);\n\n\tconst decrypted = await crypto.subtle.decrypt({ name: ALGORITHM, iv }, key, ciphertext);\n\n\treturn new TextDecoder().decode(decrypted);\n}\n","/**\n * Passkey registration (credential creation)\n *\n * Based on oslo webauthn documentation:\n * https://webauthn.oslojs.dev/examples/registration\n */\n\nimport { ECDSAPublicKey, p256 } from \"@oslojs/crypto/ecdsa\";\nimport { encodeBase64urlNoPadding, decodeBase64urlIgnorePadding } from \"@oslojs/encoding\";\nimport {\n\tparseAttestationObject,\n\tparseClientDataJSON,\n\tcoseAlgorithmES256,\n\tcoseAlgorithmRS256,\n\tcoseEllipticCurveP256,\n\tClientDataType,\n\tAttestationStatementFormat,\n\tCOSEKeyType,\n} from \"@oslojs/webauthn\";\n\nimport { generateToken } from \"../tokens.js\";\nimport type { Credential, NewCredential, AuthAdapter, User, DeviceType } from \"../types.js\";\nimport type {\n\tRegistrationOptions,\n\tRegistrationResponse,\n\tVerifiedRegistration,\n\tChallengeStore,\n\tPasskeyConfig,\n} from \"./types.js\";\n\nconst CHALLENGE_TTL = 5 * 60 * 1000; // 5 minutes\n\nexport type { PasskeyConfig };\n\n/**\n * Generate registration options for creating a new passkey\n */\nexport async function generateRegistrationOptions(\n\tconfig: PasskeyConfig,\n\tuser: Pick<User, \"id\" | \"email\" | \"name\">,\n\texistingCredentials: Credential[],\n\tchallengeStore: ChallengeStore,\n): Promise<RegistrationOptions> {\n\tconst challenge = generateToken();\n\n\t// Store challenge for verification\n\tawait challengeStore.set(challenge, {\n\t\ttype: \"registration\",\n\t\tuserId: user.id,\n\t\texpiresAt: Date.now() + CHALLENGE_TTL,\n\t});\n\n\t// Encode user ID as base64url\n\tconst userIdBytes = new TextEncoder().encode(user.id);\n\tconst userIdEncoded = encodeBase64urlNoPadding(userIdBytes);\n\n\treturn {\n\t\tchallenge,\n\t\trp: {\n\t\t\tname: config.rpName,\n\t\t\tid: config.rpId,\n\t\t},\n\t\tuser: {\n\t\t\tid: userIdEncoded,\n\t\t\tname: user.email,\n\t\t\tdisplayName: user.name || user.email,\n\t\t},\n\t\tpubKeyCredParams: [\n\t\t\t{ type: \"public-key\", alg: coseAlgorithmES256 }, // ES256 (-7)\n\t\t\t{ type: \"public-key\", alg: coseAlgorithmRS256 }, // RS256 (-257)\n\t\t],\n\t\ttimeout: 60000,\n\t\tattestation: \"none\", // We don't need attestation for our use case\n\t\tauthenticatorSelection: {\n\t\t\tresidentKey: \"preferred\", // Allow discoverable credentials\n\t\t\tuserVerification: \"preferred\",\n\t\t},\n\t\texcludeCredentials: existingCredentials.map((cred) => ({\n\t\t\ttype: \"public-key\" as const,\n\t\t\tid: cred.id,\n\t\t\ttransports: cred.transports,\n\t\t})),\n\t};\n}\n\n/**\n * Verify a registration response and extract credential data\n */\nexport async function verifyRegistrationResponse(\n\tconfig: PasskeyConfig,\n\tresponse: RegistrationResponse,\n\tchallengeStore: ChallengeStore,\n): Promise<VerifiedRegistration> {\n\t// Decode the response\n\tconst clientDataJSON = decodeBase64urlIgnorePadding(response.response.clientDataJSON);\n\tconst attestationObject = decodeBase64urlIgnorePadding(response.response.attestationObject);\n\n\t// Parse client data\n\tconst clientData = parseClientDataJSON(clientDataJSON);\n\n\t// Verify client data\n\tif (clientData.type !== ClientDataType.Create) {\n\t\tthrow new Error(\"Invalid client data type\");\n\t}\n\n\t// Verify challenge - convert Uint8Array back to base64url string (no padding, matching stored format)\n\tconst challengeString = encodeBase64urlNoPadding(clientData.challenge);\n\tconst challengeData = await challengeStore.get(challengeString);\n\tif (!challengeData) {\n\t\tthrow new Error(\"Challenge not found or expired\");\n\t}\n\tif (challengeData.type !== \"registration\") {\n\t\tthrow new Error(\"Invalid challenge type\");\n\t}\n\tif (challengeData.expiresAt < Date.now()) {\n\t\tawait challengeStore.delete(challengeString);\n\t\tthrow new Error(\"Challenge expired\");\n\t}\n\n\t// Delete challenge (single-use)\n\tawait challengeStore.delete(challengeString);\n\n\t// Verify origin\n\tif (clientData.origin !== config.origin) {\n\t\tthrow new Error(`Invalid origin: expected ${config.origin}, got ${clientData.origin}`);\n\t}\n\n\t// Parse attestation object\n\tconst attestation = parseAttestationObject(attestationObject);\n\n\t// We only support 'none' attestation for simplicity\n\tif (attestation.attestationStatement.format !== AttestationStatementFormat.None) {\n\t\t// For other formats, we'd need to verify the attestation statement\n\t\t// For now, we just ignore it and trust the credential\n\t}\n\n\tconst { authenticatorData } = attestation;\n\n\t// Verify RP ID hash\n\tif (!authenticatorData.verifyRelyingPartyIdHash(config.rpId)) {\n\t\tthrow new Error(\"Invalid RP ID hash\");\n\t}\n\n\t// Verify flags\n\tif (!authenticatorData.userPresent) {\n\t\tthrow new Error(\"User presence not verified\");\n\t}\n\n\t// Extract credential data\n\tif (!authenticatorData.credential) {\n\t\tthrow new Error(\"No credential data in attestation\");\n\t}\n\n\tconst { credential } = authenticatorData;\n\n\t// Verify algorithm is supported and encode public key\n\t// Currently only supporting ES256 (ECDSA with P-256)\n\tconst algorithm = credential.publicKey.algorithm();\n\tlet encodedPublicKey: Uint8Array;\n\n\tif (algorithm === coseAlgorithmES256) {\n\t\t// Verify it's EC2 key type\n\t\tif (credential.publicKey.type() !== COSEKeyType.EC2) {\n\t\t\tthrow new Error(\"Expected EC2 key type for ES256\");\n\t\t}\n\t\tconst cosePublicKey = credential.publicKey.ec2();\n\t\tif (cosePublicKey.curve !== coseEllipticCurveP256) {\n\t\t\tthrow new Error(\"Expected P-256 curve for ES256\");\n\t\t}\n\t\t// Encode as SEC1 uncompressed format for storage\n\t\tencodedPublicKey = new ECDSAPublicKey(\n\t\t\tp256,\n\t\t\tcosePublicKey.x,\n\t\t\tcosePublicKey.y,\n\t\t).encodeSEC1Uncompressed();\n\t} else if (algorithm === coseAlgorithmRS256) {\n\t\t// RSA is less common for passkeys, skip for now\n\t\tthrow new Error(\"RS256 not yet supported - please use ES256\");\n\t} else {\n\t\tthrow new Error(`Unsupported algorithm: ${algorithm}`);\n\t}\n\n\t// Determine device type and backup status\n\t// Note: oslo webauthn doesn't expose backup flags, so we default to singleDevice\n\t// In practice, most modern passkeys are multi-device (e.g., iCloud Keychain, Google Password Manager)\n\tconst deviceType: DeviceType = \"singleDevice\";\n\tconst backedUp = false;\n\n\treturn {\n\t\tcredentialId: response.id,\n\t\tpublicKey: encodedPublicKey,\n\t\tcounter: authenticatorData.signatureCounter,\n\t\tdeviceType,\n\t\tbackedUp,\n\t\ttransports: response.response.transports ?? [],\n\t};\n}\n\n/**\n * Register a new passkey for a user\n */\nexport async function registerPasskey(\n\tadapter: AuthAdapter,\n\tuserId: string,\n\tverified: VerifiedRegistration,\n\tname?: string,\n): Promise<Credential> {\n\t// Check credential limit\n\tconst count = await adapter.countCredentialsByUserId(userId);\n\tif (count >= 10) {\n\t\tthrow new Error(\"Maximum number of passkeys reached (10)\");\n\t}\n\n\t// Check if credential already exists\n\tconst existing = await adapter.getCredentialById(verified.credentialId);\n\tif (existing) {\n\t\tthrow new Error(\"Credential already registered\");\n\t}\n\n\tconst newCredential: NewCredential = {\n\t\tid: verified.credentialId,\n\t\tuserId,\n\t\tpublicKey: verified.publicKey,\n\t\tcounter: verified.counter,\n\t\tdeviceType: verified.deviceType,\n\t\tbackedUp: verified.backedUp,\n\t\ttransports: verified.transports,\n\t\tname,\n\t};\n\n\treturn adapter.createCredential(newCredential);\n}\n","/**\n * Passkey authentication (credential assertion)\n *\n * Based on oslo webauthn documentation:\n * https://webauthn.oslojs.dev/examples/authentication\n */\n\nimport {\n\tverifyECDSASignature,\n\tp256,\n\tdecodeSEC1PublicKey,\n\tdecodePKIXECDSASignature,\n} from \"@oslojs/crypto/ecdsa\";\nimport { sha256 } from \"@oslojs/crypto/sha2\";\nimport { encodeBase64urlNoPadding, decodeBase64urlIgnorePadding } from \"@oslojs/encoding\";\nimport {\n\tparseAuthenticatorData,\n\tparseClientDataJSON,\n\tClientDataType,\n\tcreateAssertionSignatureMessage,\n} from \"@oslojs/webauthn\";\n\nimport { generateToken } from \"../tokens.js\";\nimport type { Credential, AuthAdapter, User } from \"../types.js\";\nimport type {\n\tAuthenticationOptions,\n\tAuthenticationResponse,\n\tVerifiedAuthentication,\n\tChallengeStore,\n\tPasskeyConfig,\n} from \"./types.js\";\n\nconst CHALLENGE_TTL = 5 * 60 * 1000; // 5 minutes\n\n/**\n * Generate authentication options for signing in with a passkey\n */\nexport async function generateAuthenticationOptions(\n\tconfig: PasskeyConfig,\n\tcredentials: Credential[],\n\tchallengeStore: ChallengeStore,\n): Promise<AuthenticationOptions> {\n\tconst challenge = generateToken();\n\n\t// Store challenge for verification\n\tawait challengeStore.set(challenge, {\n\t\ttype: \"authentication\",\n\t\texpiresAt: Date.now() + CHALLENGE_TTL,\n\t});\n\n\treturn {\n\t\tchallenge,\n\t\trpId: config.rpId,\n\t\ttimeout: 60000,\n\t\tuserVerification: \"preferred\",\n\t\tallowCredentials:\n\t\t\tcredentials.length > 0\n\t\t\t\t? credentials.map((cred) => ({\n\t\t\t\t\t\ttype: \"public-key\" as const,\n\t\t\t\t\t\tid: cred.id,\n\t\t\t\t\t\ttransports: cred.transports,\n\t\t\t\t\t}))\n\t\t\t\t: undefined, // Empty = allow any discoverable credential\n\t};\n}\n\n/**\n * Verify an authentication response\n */\nexport async function verifyAuthenticationResponse(\n\tconfig: PasskeyConfig,\n\tresponse: AuthenticationResponse,\n\tcredential: Credential,\n\tchallengeStore: ChallengeStore,\n): Promise<VerifiedAuthentication> {\n\t// Decode the response\n\tconst clientDataJSON = decodeBase64urlIgnorePadding(response.response.clientDataJSON);\n\tconst authenticatorData = decodeBase64urlIgnorePadding(response.response.authenticatorData);\n\tconst signature = decodeBase64urlIgnorePadding(response.response.signature);\n\n\t// Parse client data\n\tconst clientData = parseClientDataJSON(clientDataJSON);\n\n\t// Verify client data type\n\tif (clientData.type !== ClientDataType.Get) {\n\t\tthrow new Error(\"Invalid client data type\");\n\t}\n\n\t// Verify challenge - convert Uint8Array back to base64url string (no padding, matching stored format)\n\tconst challengeString = encodeBase64urlNoPadding(clientData.challenge);\n\tconst challengeData = await challengeStore.get(challengeString);\n\tif (!challengeData) {\n\t\tthrow new Error(\"Challenge not found or expired\");\n\t}\n\tif (challengeData.type !== \"authentication\") {\n\t\tthrow new Error(\"Invalid challenge type\");\n\t}\n\tif (challengeData.expiresAt < Date.now()) {\n\t\tawait challengeStore.delete(challengeString);\n\t\tthrow new Error(\"Challenge expired\");\n\t}\n\n\t// Delete challenge (single-use)\n\tawait challengeStore.delete(challengeString);\n\n\t// Verify origin\n\tif (clientData.origin !== config.origin) {\n\t\tthrow new Error(`Invalid origin: expected ${config.origin}, got ${clientData.origin}`);\n\t}\n\n\t// Parse authenticator data\n\tconst authData = parseAuthenticatorData(authenticatorData);\n\n\t// Verify RP ID hash\n\tif (!authData.verifyRelyingPartyIdHash(config.rpId)) {\n\t\tthrow new Error(\"Invalid RP ID hash\");\n\t}\n\n\t// Verify flags\n\tif (!authData.userPresent) {\n\t\tthrow new Error(\"User presence not verified\");\n\t}\n\n\t// Verify counter (prevent replay attacks)\n\tif (authData.signatureCounter !== 0 && authData.signatureCounter <= credential.counter) {\n\t\tthrow new Error(\"Invalid signature counter - possible cloned authenticator\");\n\t}\n\n\t// Create the message that was signed\n\tconst signatureMessage = createAssertionSignatureMessage(authenticatorData, clientDataJSON);\n\n\t// Ensure public key is a Uint8Array (may come as Buffer from some DB drivers)\n\tconst publicKeyBytes =\n\t\tcredential.publicKey instanceof Uint8Array\n\t\t\t? credential.publicKey\n\t\t\t: new Uint8Array(credential.publicKey);\n\n\t// Decode the stored SEC1-encoded public key and verify signature\n\t// The signature from WebAuthn is DER-encoded (PKIX format)\n\tconst ecdsaPublicKey = decodeSEC1PublicKey(p256, publicKeyBytes);\n\tconst ecdsaSignature = decodePKIXECDSASignature(signature);\n\tconst hash = sha256(signatureMessage);\n\tconst signatureValid = verifyECDSASignature(ecdsaPublicKey, hash, ecdsaSignature);\n\n\tif (!signatureValid) {\n\t\tthrow new Error(\"Invalid signature\");\n\t}\n\n\treturn {\n\t\tcredentialId: response.id,\n\t\tnewCounter: authData.signatureCounter,\n\t};\n}\n\n/**\n * Authenticate a user with a passkey\n */\nexport async function authenticateWithPasskey(\n\tconfig: PasskeyConfig,\n\tadapter: AuthAdapter,\n\tresponse: AuthenticationResponse,\n\tchallengeStore: ChallengeStore,\n): Promise<User> {\n\t// Find the credential\n\tconst credential = await adapter.getCredentialById(response.id);\n\tif (!credential) {\n\t\tthrow new Error(\"Credential not found\");\n\t}\n\n\t// Verify the response\n\tconst verified = await verifyAuthenticationResponse(config, response, credential, challengeStore);\n\n\t// Update counter\n\tawait adapter.updateCredentialCounter(verified.credentialId, verified.newCounter);\n\n\t// Get the user\n\tconst user = await adapter.getUserById(credential.userId);\n\tif (!user) {\n\t\tthrow new Error(\"User not found\");\n\t}\n\n\treturn user;\n}\n"],"mappings":"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"}