@emdash-cms/auth 0.0.1 → 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 +9 -0
- package/dist/adapters/kysely.d.mts +1 -1
- package/dist/adapters/kysely.mjs +1 -1
- package/dist/adapters/kysely.mjs.map +1 -1
- package/dist/{authenticate-D5UgaoTH.d.mts → authenticate-AIvzeyyc.d.mts} +2 -2
- package/dist/{authenticate-D5UgaoTH.d.mts.map → authenticate-AIvzeyyc.d.mts.map} +1 -1
- package/dist/{authenticate-j5GayLXB.mjs → authenticate-CZ5fe42l.mjs} +8 -8
- package/dist/authenticate-CZ5fe42l.mjs.map +1 -0
- package/dist/index.d.mts +2 -2
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +7 -7
- package/dist/index.mjs.map +1 -1
- package/dist/passkey/index.d.mts +1 -1
- package/dist/passkey/index.mjs +1 -1
- package/dist/{types-HtRc90Wi.d.mts → types-ByJGa0Mk.d.mts} +2 -2
- package/dist/{types-HtRc90Wi.d.mts.map → types-ByJGa0Mk.d.mts.map} +1 -1
- package/dist/{types-CiSNpRI9.mjs → types-ndj-bYfi.mjs} +2 -2
- package/dist/types-ndj-bYfi.mjs.map +1 -0
- package/package.json +2 -2
- package/src/invite.ts +1 -1
- package/src/magic-link/index.ts +1 -1
- package/src/oauth/consumer.ts +4 -1
- package/src/signup.ts +1 -1
- package/src/tokens.ts +7 -10
- package/dist/authenticate-j5GayLXB.mjs.map +0 -1
- package/dist/types-CiSNpRI9.mjs.map +0 -1
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.
|
package/dist/adapters/kysely.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as toDeviceType, n as Role, o as toRoleLevel, s as toTokenType } from "../types-
|
|
1
|
+
import { a as toDeviceType, n as Role, o as toRoleLevel, s as toTokenType } from "../types-ndj-bYfi.mjs";
|
|
2
2
|
import { ulid } from "ulidx";
|
|
3
3
|
|
|
4
4
|
//#region src/adapters/kysely.ts
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"kysely.mjs","names":[],"sources":["../../src/adapters/kysely.ts"],"sourcesContent":["/**\n * Kysely database adapter for @emdashcms/auth\n */\n\nimport type { Kysely, Insertable, Selectable, Updateable } from \"kysely\";\nimport { ulid } from \"ulidx\";\n\nimport {\n\tRole,\n\ttoRoleLevel,\n\ttoDeviceType,\n\ttoTokenType,\n\ttype AuthAdapter,\n\ttype User,\n\ttype NewUser,\n\ttype UpdateUser,\n\ttype Credential,\n\ttype NewCredential,\n\ttype AuthToken,\n\ttype NewAuthToken,\n\ttype TokenType,\n\ttype OAuthAccount,\n\ttype NewOAuthAccount,\n\ttype AllowedDomain,\n\ttype RoleLevel,\n} from \"../types.js\";\n\n// ============================================================================\n// Database schema types\n// ============================================================================\n\nexport interface AuthTables {\n\tusers: UserTable;\n\tcredentials: CredentialTable;\n\tauth_tokens: AuthTokenTable;\n\toauth_accounts: OAuthAccountTable;\n\tallowed_domains: AllowedDomainTable;\n}\n\ninterface UserTable {\n\tid: string;\n\temail: string;\n\tname: string | null;\n\tavatar_url: string | null;\n\trole: number;\n\temail_verified: number;\n\tdisabled: number;\n\tdata: string | null;\n\tcreated_at: string;\n\tupdated_at: string;\n}\n\ninterface CredentialTable {\n\tid: string;\n\tuser_id: string;\n\tpublic_key: Uint8Array;\n\tcounter: number;\n\tdevice_type: string;\n\tbacked_up: number;\n\ttransports: string | null;\n\tname: string | null;\n\tcreated_at: string;\n\tlast_used_at: string;\n}\n\ninterface AuthTokenTable {\n\thash: string;\n\tuser_id: string | null;\n\temail: string | null;\n\ttype: string;\n\trole: number | null;\n\tinvited_by: string | null;\n\texpires_at: string;\n\tcreated_at: string;\n}\n\ninterface OAuthAccountTable {\n\tprovider: string;\n\tprovider_account_id: string;\n\tuser_id: string;\n\tcreated_at: string;\n}\n\ninterface AllowedDomainTable {\n\tdomain: string;\n\tdefault_role: number;\n\tenabled: number;\n\tcreated_at: string;\n}\n\n// ============================================================================\n// Adapter implementation\n// ============================================================================\n\nexport function createKyselyAdapter<T extends AuthTables>(db: Kysely<T>): AuthAdapter {\n\t// Type cast to work with generic Kysely instance\n\t// eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- generic Kysely<T extends AuthTables> narrowed to concrete AuthTables for internal queries\n\tconst kdb = db as unknown as Kysely<AuthTables>;\n\n\treturn {\n\t\t// ========================================================================\n\t\t// Users\n\t\t// ========================================================================\n\n\t\tasync getUserById(id: string): Promise<User | null> {\n\t\t\tconst row = await kdb.selectFrom(\"users\").selectAll().where(\"id\", \"=\", id).executeTakeFirst();\n\n\t\t\treturn row ? rowToUser(row) : null;\n\t\t},\n\n\t\tasync getUserByEmail(email: string): Promise<User | null> {\n\t\t\tconst row = await kdb\n\t\t\t\t.selectFrom(\"users\")\n\t\t\t\t.selectAll()\n\t\t\t\t.where(\"email\", \"=\", email.toLowerCase())\n\t\t\t\t.executeTakeFirst();\n\n\t\t\treturn row ? rowToUser(row) : null;\n\t\t},\n\n\t\tasync createUser(user: NewUser): Promise<User> {\n\t\t\tconst now = new Date().toISOString();\n\t\t\tconst id = ulid();\n\n\t\t\tconst row: Insertable<UserTable> = {\n\t\t\t\tid,\n\t\t\t\temail: user.email.toLowerCase(),\n\t\t\t\tname: user.name ?? null,\n\t\t\t\tavatar_url: user.avatarUrl ?? null,\n\t\t\t\trole: user.role ?? Role.SUBSCRIBER,\n\t\t\t\temail_verified: user.emailVerified ? 1 : 0,\n\t\t\t\tdisabled: 0,\n\t\t\t\tdata: user.data ? JSON.stringify(user.data) : null,\n\t\t\t\tcreated_at: now,\n\t\t\t\tupdated_at: now,\n\t\t\t};\n\n\t\t\tawait kdb.insertInto(\"users\").values(row).execute();\n\n\t\t\treturn {\n\t\t\t\tid,\n\t\t\t\temail: row.email,\n\t\t\t\tname: user.name ?? null,\n\t\t\t\tavatarUrl: user.avatarUrl ?? null,\n\t\t\t\trole: toRoleLevel(row.role),\n\t\t\t\temailVerified: row.email_verified === 1,\n\t\t\t\tdisabled: false,\n\t\t\t\tdata: user.data ?? null,\n\t\t\t\tcreatedAt: new Date(now),\n\t\t\t\tupdatedAt: new Date(now),\n\t\t\t};\n\t\t},\n\n\t\tasync updateUser(id: string, data: UpdateUser): Promise<void> {\n\t\t\tconst update: Updateable<UserTable> = {\n\t\t\t\tupdated_at: new Date().toISOString(),\n\t\t\t};\n\n\t\t\tif (data.email !== undefined) update.email = data.email.toLowerCase();\n\t\t\tif (data.name !== undefined) update.name = data.name;\n\t\t\tif (data.avatarUrl !== undefined) update.avatar_url = data.avatarUrl;\n\t\t\tif (data.role !== undefined) update.role = data.role;\n\t\t\tif (data.emailVerified !== undefined) update.email_verified = data.emailVerified ? 1 : 0;\n\t\t\tif (data.disabled !== undefined) update.disabled = data.disabled ? 1 : 0;\n\t\t\tif (data.data !== undefined) update.data = data.data ? JSON.stringify(data.data) : null;\n\n\t\t\tawait kdb.updateTable(\"users\").set(update).where(\"id\", \"=\", id).execute();\n\t\t},\n\n\t\tasync deleteUser(id: string): Promise<void> {\n\t\t\tawait kdb.deleteFrom(\"users\").where(\"id\", \"=\", id).execute();\n\t\t},\n\n\t\tasync countUsers(): Promise<number> {\n\t\t\tconst result = await kdb\n\t\t\t\t.selectFrom(\"users\")\n\t\t\t\t.select((eb) => eb.fn.countAll<number>().as(\"count\"))\n\t\t\t\t.executeTakeFirstOrThrow();\n\n\t\t\treturn result.count;\n\t\t},\n\n\t\tasync getUsers(options?: {\n\t\t\tsearch?: string;\n\t\t\trole?: number;\n\t\t\tcursor?: string;\n\t\t\tlimit?: number;\n\t\t}): Promise<{\n\t\t\titems: Array<\n\t\t\t\tUser & {\n\t\t\t\t\tlastLogin: Date | null;\n\t\t\t\t\tcredentialCount: number;\n\t\t\t\t\toauthProviders: string[];\n\t\t\t\t}\n\t\t\t>;\n\t\t\tnextCursor?: string;\n\t\t}> {\n\t\t\tconst limit = Math.min(options?.limit ?? 20, 100);\n\n\t\t\tlet query = kdb\n\t\t\t\t.selectFrom(\"users\")\n\t\t\t\t.leftJoin(\"credentials\", \"users.id\", \"credentials.user_id\")\n\t\t\t\t.selectAll(\"users\")\n\t\t\t\t.select((eb) => [\n\t\t\t\t\teb.fn.count<number>(\"credentials.id\").as(\"credential_count\"),\n\t\t\t\t\teb.fn.max(\"credentials.last_used_at\").as(\"last_login\"),\n\t\t\t\t])\n\t\t\t\t.groupBy(\"users.id\")\n\t\t\t\t.orderBy(\"users.created_at\", \"desc\")\n\t\t\t\t.limit(limit + 1);\n\n\t\t\t// Apply filters\n\t\t\tif (options?.search) {\n\t\t\t\tconst searchPattern = `%${options.search}%`;\n\t\t\t\tquery = query.where((eb) =>\n\t\t\t\t\teb.or([\n\t\t\t\t\t\teb(\"users.email\", \"like\", searchPattern),\n\t\t\t\t\t\teb(\"users.name\", \"like\", searchPattern),\n\t\t\t\t\t]),\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tif (options?.role !== undefined) {\n\t\t\t\tquery = query.where(\"users.role\", \"=\", options.role);\n\t\t\t}\n\n\t\t\tif (options?.cursor) {\n\t\t\t\t// Get the cursor user's created_at for pagination\n\t\t\t\tconst cursorUser = await kdb\n\t\t\t\t\t.selectFrom(\"users\")\n\t\t\t\t\t.select(\"created_at\")\n\t\t\t\t\t.where(\"id\", \"=\", options.cursor)\n\t\t\t\t\t.executeTakeFirst();\n\n\t\t\t\tif (cursorUser) {\n\t\t\t\t\tquery = query.where(\"users.created_at\", \"<\", cursorUser.created_at);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst rows = await query.execute();\n\n\t\t\t// Get OAuth providers for all users in this batch\n\t\t\tconst userIds = rows.slice(0, limit).map((r) => r.id);\n\t\t\tconst oauthAccounts =\n\t\t\t\tuserIds.length > 0\n\t\t\t\t\t? await kdb\n\t\t\t\t\t\t\t.selectFrom(\"oauth_accounts\")\n\t\t\t\t\t\t\t.select([\"user_id\", \"provider\"])\n\t\t\t\t\t\t\t.where(\"user_id\", \"in\", userIds)\n\t\t\t\t\t\t\t.execute()\n\t\t\t\t\t: [];\n\n\t\t\t// Group OAuth providers by user\n\t\t\tconst oauthByUser = new Map<string, string[]>();\n\t\t\tfor (const account of oauthAccounts) {\n\t\t\t\tconst providers = oauthByUser.get(account.user_id) ?? [];\n\t\t\t\tproviders.push(account.provider);\n\t\t\t\toauthByUser.set(account.user_id, providers);\n\t\t\t}\n\n\t\t\tconst hasMore = rows.length > limit;\n\t\t\tconst items = rows.slice(0, limit).map((row) => ({\n\t\t\t\tid: row.id,\n\t\t\t\temail: row.email,\n\t\t\t\tname: row.name,\n\t\t\t\tavatarUrl: row.avatar_url,\n\t\t\t\trole: toRoleLevel(row.role),\n\t\t\t\temailVerified: row.email_verified === 1,\n\t\t\t\tdisabled: row.disabled === 1,\n\t\t\t\tdata: row.data ? JSON.parse(row.data) : null,\n\t\t\t\tcreatedAt: new Date(row.created_at),\n\t\t\t\tupdatedAt: new Date(row.updated_at),\n\t\t\t\tlastLogin: row.last_login ? new Date(row.last_login) : null,\n\t\t\t\tcredentialCount: row.credential_count ?? 0,\n\t\t\t\toauthProviders: oauthByUser.get(row.id) ?? [],\n\t\t\t}));\n\n\t\t\treturn {\n\t\t\t\titems,\n\t\t\t\tnextCursor: hasMore ? items.at(-1)?.id : undefined,\n\t\t\t};\n\t\t},\n\n\t\tasync getUserWithDetails(id: string): Promise<{\n\t\t\tuser: User;\n\t\t\tcredentials: Credential[];\n\t\t\toauthAccounts: OAuthAccount[];\n\t\t\tlastLogin: Date | null;\n\t\t} | null> {\n\t\t\tconst user = await kdb\n\t\t\t\t.selectFrom(\"users\")\n\t\t\t\t.selectAll()\n\t\t\t\t.where(\"id\", \"=\", id)\n\t\t\t\t.executeTakeFirst();\n\n\t\t\tif (!user) return null;\n\n\t\t\tconst [credentials, oauthAccounts] = await Promise.all([\n\t\t\t\tkdb\n\t\t\t\t\t.selectFrom(\"credentials\")\n\t\t\t\t\t.selectAll()\n\t\t\t\t\t.where(\"user_id\", \"=\", id)\n\t\t\t\t\t.orderBy(\"created_at\", \"desc\")\n\t\t\t\t\t.execute(),\n\t\t\t\tkdb.selectFrom(\"oauth_accounts\").selectAll().where(\"user_id\", \"=\", id).execute(),\n\t\t\t]);\n\n\t\t\t// Find last login from most recent credential use\n\t\t\tconst lastLogin = credentials.reduce<Date | null>((latest, cred) => {\n\t\t\t\tconst lastUsed = new Date(cred.last_used_at);\n\t\t\t\treturn !latest || lastUsed > latest ? lastUsed : latest;\n\t\t\t}, null);\n\n\t\t\treturn {\n\t\t\t\tuser: rowToUser(user),\n\t\t\t\tcredentials: credentials.map(rowToCredential),\n\t\t\t\toauthAccounts: oauthAccounts.map(rowToOAuthAccount),\n\t\t\t\tlastLogin,\n\t\t\t};\n\t\t},\n\n\t\tasync countAdmins(): Promise<number> {\n\t\t\tconst result = await kdb\n\t\t\t\t.selectFrom(\"users\")\n\t\t\t\t.select((eb) => eb.fn.countAll<number>().as(\"count\"))\n\t\t\t\t.where(\"role\", \"=\", Role.ADMIN)\n\t\t\t\t.where(\"disabled\", \"=\", 0)\n\t\t\t\t.executeTakeFirstOrThrow();\n\n\t\t\treturn result.count;\n\t\t},\n\n\t\t// ========================================================================\n\t\t// Credentials\n\t\t// ========================================================================\n\n\t\tasync getCredentialById(id: string): Promise<Credential | null> {\n\t\t\tconst row = await kdb\n\t\t\t\t.selectFrom(\"credentials\")\n\t\t\t\t.selectAll()\n\t\t\t\t.where(\"id\", \"=\", id)\n\t\t\t\t.executeTakeFirst();\n\n\t\t\treturn row ? rowToCredential(row) : null;\n\t\t},\n\n\t\tasync getCredentialsByUserId(userId: string): Promise<Credential[]> {\n\t\t\tconst rows = await kdb\n\t\t\t\t.selectFrom(\"credentials\")\n\t\t\t\t.selectAll()\n\t\t\t\t.where(\"user_id\", \"=\", userId)\n\t\t\t\t.execute();\n\n\t\t\treturn rows.map(rowToCredential);\n\t\t},\n\n\t\tasync createCredential(credential: NewCredential): Promise<Credential> {\n\t\t\tconst now = new Date().toISOString();\n\n\t\t\tconst row: Insertable<CredentialTable> = {\n\t\t\t\tid: credential.id,\n\t\t\t\tuser_id: credential.userId,\n\t\t\t\tpublic_key: credential.publicKey,\n\t\t\t\tcounter: credential.counter,\n\t\t\t\tdevice_type: credential.deviceType,\n\t\t\t\tbacked_up: credential.backedUp ? 1 : 0,\n\t\t\t\ttransports: credential.transports.length > 0 ? JSON.stringify(credential.transports) : null,\n\t\t\t\tname: credential.name ?? null,\n\t\t\t\tcreated_at: now,\n\t\t\t\tlast_used_at: now,\n\t\t\t};\n\n\t\t\tawait kdb.insertInto(\"credentials\").values(row).execute();\n\n\t\t\treturn {\n\t\t\t\tid: credential.id,\n\t\t\t\tuserId: credential.userId,\n\t\t\t\tpublicKey: credential.publicKey,\n\t\t\t\tcounter: credential.counter,\n\t\t\t\tdeviceType: credential.deviceType,\n\t\t\t\tbackedUp: credential.backedUp,\n\t\t\t\ttransports: credential.transports,\n\t\t\t\tname: credential.name ?? null,\n\t\t\t\tcreatedAt: new Date(now),\n\t\t\t\tlastUsedAt: new Date(now),\n\t\t\t};\n\t\t},\n\n\t\tasync updateCredentialCounter(id: string, counter: number): Promise<void> {\n\t\t\tawait kdb\n\t\t\t\t.updateTable(\"credentials\")\n\t\t\t\t.set({\n\t\t\t\t\tcounter,\n\t\t\t\t\tlast_used_at: new Date().toISOString(),\n\t\t\t\t})\n\t\t\t\t.where(\"id\", \"=\", id)\n\t\t\t\t.execute();\n\t\t},\n\n\t\tasync updateCredentialName(id: string, name: string | null): Promise<void> {\n\t\t\tawait kdb.updateTable(\"credentials\").set({ name }).where(\"id\", \"=\", id).execute();\n\t\t},\n\n\t\tasync deleteCredential(id: string): Promise<void> {\n\t\t\tawait kdb.deleteFrom(\"credentials\").where(\"id\", \"=\", id).execute();\n\t\t},\n\n\t\tasync countCredentialsByUserId(userId: string): Promise<number> {\n\t\t\tconst result = await kdb\n\t\t\t\t.selectFrom(\"credentials\")\n\t\t\t\t.select((eb) => eb.fn.countAll<number>().as(\"count\"))\n\t\t\t\t.where(\"user_id\", \"=\", userId)\n\t\t\t\t.executeTakeFirstOrThrow();\n\n\t\t\treturn result.count;\n\t\t},\n\n\t\t// ========================================================================\n\t\t// Auth Tokens\n\t\t// ========================================================================\n\n\t\tasync createToken(token: NewAuthToken): Promise<void> {\n\t\t\tconst row: Insertable<AuthTokenTable> = {\n\t\t\t\thash: token.hash,\n\t\t\t\tuser_id: token.userId ?? null,\n\t\t\t\temail: token.email ?? null,\n\t\t\t\ttype: token.type,\n\t\t\t\trole: token.role ?? null,\n\t\t\t\tinvited_by: token.invitedBy ?? null,\n\t\t\t\texpires_at: token.expiresAt.toISOString(),\n\t\t\t\tcreated_at: new Date().toISOString(),\n\t\t\t};\n\n\t\t\tawait kdb.insertInto(\"auth_tokens\").values(row).execute();\n\t\t},\n\n\t\tasync getToken(hash: string, type: TokenType): Promise<AuthToken | null> {\n\t\t\tconst row = await kdb\n\t\t\t\t.selectFrom(\"auth_tokens\")\n\t\t\t\t.selectAll()\n\t\t\t\t.where(\"hash\", \"=\", hash)\n\t\t\t\t.where(\"type\", \"=\", type)\n\t\t\t\t.executeTakeFirst();\n\n\t\t\treturn row ? rowToAuthToken(row) : null;\n\t\t},\n\n\t\tasync deleteToken(hash: string): Promise<void> {\n\t\t\tawait kdb.deleteFrom(\"auth_tokens\").where(\"hash\", \"=\", hash).execute();\n\t\t},\n\n\t\tasync deleteExpiredTokens(): Promise<void> {\n\t\t\tawait kdb\n\t\t\t\t.deleteFrom(\"auth_tokens\")\n\t\t\t\t.where(\"expires_at\", \"<\", new Date().toISOString())\n\t\t\t\t.execute();\n\t\t},\n\n\t\t// ========================================================================\n\t\t// OAuth Accounts\n\t\t// ========================================================================\n\n\t\tasync getOAuthAccount(\n\t\t\tprovider: string,\n\t\t\tproviderAccountId: string,\n\t\t): Promise<OAuthAccount | null> {\n\t\t\tconst row = await kdb\n\t\t\t\t.selectFrom(\"oauth_accounts\")\n\t\t\t\t.selectAll()\n\t\t\t\t.where(\"provider\", \"=\", provider)\n\t\t\t\t.where(\"provider_account_id\", \"=\", providerAccountId)\n\t\t\t\t.executeTakeFirst();\n\n\t\t\treturn row ? rowToOAuthAccount(row) : null;\n\t\t},\n\n\t\tasync getOAuthAccountsByUserId(userId: string): Promise<OAuthAccount[]> {\n\t\t\tconst rows = await kdb\n\t\t\t\t.selectFrom(\"oauth_accounts\")\n\t\t\t\t.selectAll()\n\t\t\t\t.where(\"user_id\", \"=\", userId)\n\t\t\t\t.execute();\n\n\t\t\treturn rows.map(rowToOAuthAccount);\n\t\t},\n\n\t\tasync createOAuthAccount(account: NewOAuthAccount): Promise<OAuthAccount> {\n\t\t\tconst now = new Date().toISOString();\n\n\t\t\tconst row: Insertable<OAuthAccountTable> = {\n\t\t\t\tprovider: account.provider,\n\t\t\t\tprovider_account_id: account.providerAccountId,\n\t\t\t\tuser_id: account.userId,\n\t\t\t\tcreated_at: now,\n\t\t\t};\n\n\t\t\tawait kdb.insertInto(\"oauth_accounts\").values(row).execute();\n\n\t\t\treturn {\n\t\t\t\tprovider: account.provider,\n\t\t\t\tproviderAccountId: account.providerAccountId,\n\t\t\t\tuserId: account.userId,\n\t\t\t\tcreatedAt: new Date(now),\n\t\t\t};\n\t\t},\n\n\t\tasync deleteOAuthAccount(provider: string, providerAccountId: string): Promise<void> {\n\t\t\tawait kdb\n\t\t\t\t.deleteFrom(\"oauth_accounts\")\n\t\t\t\t.where(\"provider\", \"=\", provider)\n\t\t\t\t.where(\"provider_account_id\", \"=\", providerAccountId)\n\t\t\t\t.execute();\n\t\t},\n\n\t\t// ========================================================================\n\t\t// Allowed Domains\n\t\t// ========================================================================\n\n\t\tasync getAllowedDomain(domain: string): Promise<AllowedDomain | null> {\n\t\t\tconst row = await kdb\n\t\t\t\t.selectFrom(\"allowed_domains\")\n\t\t\t\t.selectAll()\n\t\t\t\t.where(\"domain\", \"=\", domain.toLowerCase())\n\t\t\t\t.executeTakeFirst();\n\n\t\t\treturn row ? rowToAllowedDomain(row) : null;\n\t\t},\n\n\t\tasync getAllowedDomains(): Promise<AllowedDomain[]> {\n\t\t\tconst rows = await kdb.selectFrom(\"allowed_domains\").selectAll().execute();\n\n\t\t\treturn rows.map(rowToAllowedDomain);\n\t\t},\n\n\t\tasync createAllowedDomain(domain: string, defaultRole: RoleLevel): Promise<AllowedDomain> {\n\t\t\tconst now = new Date().toISOString();\n\n\t\t\tconst row: Insertable<AllowedDomainTable> = {\n\t\t\t\tdomain: domain.toLowerCase(),\n\t\t\t\tdefault_role: defaultRole,\n\t\t\t\tenabled: 1,\n\t\t\t\tcreated_at: now,\n\t\t\t};\n\n\t\t\tawait kdb.insertInto(\"allowed_domains\").values(row).execute();\n\n\t\t\treturn {\n\t\t\t\tdomain: row.domain,\n\t\t\t\tdefaultRole,\n\t\t\t\tenabled: true,\n\t\t\t\tcreatedAt: new Date(now),\n\t\t\t};\n\t\t},\n\n\t\tasync updateAllowedDomain(\n\t\t\tdomain: string,\n\t\t\tenabled: boolean,\n\t\t\tdefaultRole?: RoleLevel,\n\t\t): Promise<void> {\n\t\t\tconst update: Updateable<AllowedDomainTable> = {\n\t\t\t\tenabled: enabled ? 1 : 0,\n\t\t\t};\n\n\t\t\tif (defaultRole !== undefined) {\n\t\t\t\tupdate.default_role = defaultRole;\n\t\t\t}\n\n\t\t\tawait kdb\n\t\t\t\t.updateTable(\"allowed_domains\")\n\t\t\t\t.set(update)\n\t\t\t\t.where(\"domain\", \"=\", domain.toLowerCase())\n\t\t\t\t.execute();\n\t\t},\n\n\t\tasync deleteAllowedDomain(domain: string): Promise<void> {\n\t\t\tawait kdb.deleteFrom(\"allowed_domains\").where(\"domain\", \"=\", domain.toLowerCase()).execute();\n\t\t},\n\t};\n}\n\n// ============================================================================\n// Row converters\n// ============================================================================\n\nfunction rowToUser(row: Selectable<UserTable>): User {\n\treturn {\n\t\tid: row.id,\n\t\temail: row.email,\n\t\tname: row.name,\n\t\tavatarUrl: row.avatar_url,\n\t\trole: toRoleLevel(row.role),\n\t\temailVerified: row.email_verified === 1,\n\t\tdisabled: row.disabled === 1,\n\t\tdata: row.data ? JSON.parse(row.data) : null,\n\t\tcreatedAt: new Date(row.created_at),\n\t\tupdatedAt: new Date(row.updated_at),\n\t};\n}\n\nfunction rowToCredential(row: Selectable<CredentialTable>): Credential {\n\treturn {\n\t\tid: row.id,\n\t\tuserId: row.user_id,\n\t\tpublicKey: row.public_key,\n\t\tcounter: row.counter,\n\t\tdeviceType: toDeviceType(row.device_type),\n\t\tbackedUp: row.backed_up === 1,\n\t\ttransports: row.transports ? JSON.parse(row.transports) : [],\n\t\tname: row.name,\n\t\tcreatedAt: new Date(row.created_at),\n\t\tlastUsedAt: new Date(row.last_used_at),\n\t};\n}\n\nfunction rowToAuthToken(row: Selectable<AuthTokenTable>): AuthToken {\n\treturn {\n\t\thash: row.hash,\n\t\tuserId: row.user_id,\n\t\temail: row.email,\n\t\ttype: toTokenType(row.type),\n\t\trole: row.role != null ? toRoleLevel(row.role) : null,\n\t\tinvitedBy: row.invited_by,\n\t\texpiresAt: new Date(row.expires_at),\n\t\tcreatedAt: new Date(row.created_at),\n\t};\n}\n\nfunction rowToOAuthAccount(row: Selectable<OAuthAccountTable>): OAuthAccount {\n\treturn {\n\t\tprovider: row.provider,\n\t\tproviderAccountId: row.provider_account_id,\n\t\tuserId: row.user_id,\n\t\tcreatedAt: new Date(row.created_at),\n\t};\n}\n\nfunction rowToAllowedDomain(row: Selectable<AllowedDomainTable>): AllowedDomain {\n\treturn {\n\t\tdomain: row.domain,\n\t\tdefaultRole: toRoleLevel(row.default_role),\n\t\tenabled: row.enabled === 1,\n\t\tcreatedAt: new Date(row.created_at),\n\t};\n}\n\n// ============================================================================\n// Migration SQL\n// ============================================================================\n\nexport const AUTH_TABLES_SQL = `\n-- Users (no password_hash)\nCREATE TABLE IF NOT EXISTS users (\n id TEXT PRIMARY KEY,\n email TEXT UNIQUE NOT NULL,\n name TEXT,\n avatar_url TEXT,\n role INTEGER NOT NULL DEFAULT 10,\n email_verified INTEGER NOT NULL DEFAULT 0,\n disabled INTEGER NOT NULL DEFAULT 0,\n data TEXT,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\n\nCREATE INDEX IF NOT EXISTS idx_users_email ON users(email);\n\n-- Passkey credentials\nCREATE TABLE IF NOT EXISTS credentials (\n id TEXT PRIMARY KEY,\n user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,\n public_key BLOB NOT NULL,\n counter INTEGER NOT NULL DEFAULT 0,\n device_type TEXT NOT NULL,\n backed_up INTEGER NOT NULL DEFAULT 0,\n transports TEXT,\n name TEXT,\n created_at TEXT NOT NULL,\n last_used_at TEXT NOT NULL\n);\n\nCREATE INDEX IF NOT EXISTS idx_credentials_user ON credentials(user_id);\n\n-- Auth tokens (magic links, email verification, invites)\nCREATE TABLE IF NOT EXISTS auth_tokens (\n hash TEXT PRIMARY KEY,\n user_id TEXT REFERENCES users(id) ON DELETE CASCADE,\n email TEXT,\n type TEXT NOT NULL,\n role INTEGER,\n invited_by TEXT REFERENCES users(id),\n expires_at TEXT NOT NULL,\n created_at TEXT NOT NULL\n);\n\nCREATE INDEX IF NOT EXISTS idx_auth_tokens_email ON auth_tokens(email);\n\n-- OAuth accounts (external provider links)\nCREATE TABLE IF NOT EXISTS oauth_accounts (\n provider TEXT NOT NULL,\n provider_account_id TEXT NOT NULL,\n user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,\n created_at TEXT NOT NULL,\n PRIMARY KEY (provider, provider_account_id)\n);\n\nCREATE INDEX IF NOT EXISTS idx_oauth_accounts_user ON oauth_accounts(user_id);\n\n-- Allowed domains for self-signup\nCREATE TABLE IF NOT EXISTS allowed_domains (\n domain TEXT PRIMARY KEY,\n default_role INTEGER NOT NULL DEFAULT 20,\n enabled INTEGER NOT NULL DEFAULT 1,\n created_at TEXT NOT NULL\n);\n`;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"kysely.mjs","names":[],"sources":["../../src/adapters/kysely.ts"],"sourcesContent":["/**\n * Kysely database adapter for @emdash-cms/auth\n */\n\nimport type { Kysely, Insertable, Selectable, Updateable } from \"kysely\";\nimport { ulid } from \"ulidx\";\n\nimport {\n\tRole,\n\ttoRoleLevel,\n\ttoDeviceType,\n\ttoTokenType,\n\ttype AuthAdapter,\n\ttype User,\n\ttype NewUser,\n\ttype UpdateUser,\n\ttype Credential,\n\ttype NewCredential,\n\ttype AuthToken,\n\ttype NewAuthToken,\n\ttype TokenType,\n\ttype OAuthAccount,\n\ttype NewOAuthAccount,\n\ttype AllowedDomain,\n\ttype RoleLevel,\n} from \"../types.js\";\n\n// ============================================================================\n// Database schema types\n// ============================================================================\n\nexport interface AuthTables {\n\tusers: UserTable;\n\tcredentials: CredentialTable;\n\tauth_tokens: AuthTokenTable;\n\toauth_accounts: OAuthAccountTable;\n\tallowed_domains: AllowedDomainTable;\n}\n\ninterface UserTable {\n\tid: string;\n\temail: string;\n\tname: string | null;\n\tavatar_url: string | null;\n\trole: number;\n\temail_verified: number;\n\tdisabled: number;\n\tdata: string | null;\n\tcreated_at: string;\n\tupdated_at: string;\n}\n\ninterface CredentialTable {\n\tid: string;\n\tuser_id: string;\n\tpublic_key: Uint8Array;\n\tcounter: number;\n\tdevice_type: string;\n\tbacked_up: number;\n\ttransports: string | null;\n\tname: string | null;\n\tcreated_at: string;\n\tlast_used_at: string;\n}\n\ninterface AuthTokenTable {\n\thash: string;\n\tuser_id: string | null;\n\temail: string | null;\n\ttype: string;\n\trole: number | null;\n\tinvited_by: string | null;\n\texpires_at: string;\n\tcreated_at: string;\n}\n\ninterface OAuthAccountTable {\n\tprovider: string;\n\tprovider_account_id: string;\n\tuser_id: string;\n\tcreated_at: string;\n}\n\ninterface AllowedDomainTable {\n\tdomain: string;\n\tdefault_role: number;\n\tenabled: number;\n\tcreated_at: string;\n}\n\n// ============================================================================\n// Adapter implementation\n// ============================================================================\n\nexport function createKyselyAdapter<T extends AuthTables>(db: Kysely<T>): AuthAdapter {\n\t// Type cast to work with generic Kysely instance\n\t// eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- generic Kysely<T extends AuthTables> narrowed to concrete AuthTables for internal queries\n\tconst kdb = db as unknown as Kysely<AuthTables>;\n\n\treturn {\n\t\t// ========================================================================\n\t\t// Users\n\t\t// ========================================================================\n\n\t\tasync getUserById(id: string): Promise<User | null> {\n\t\t\tconst row = await kdb.selectFrom(\"users\").selectAll().where(\"id\", \"=\", id).executeTakeFirst();\n\n\t\t\treturn row ? rowToUser(row) : null;\n\t\t},\n\n\t\tasync getUserByEmail(email: string): Promise<User | null> {\n\t\t\tconst row = await kdb\n\t\t\t\t.selectFrom(\"users\")\n\t\t\t\t.selectAll()\n\t\t\t\t.where(\"email\", \"=\", email.toLowerCase())\n\t\t\t\t.executeTakeFirst();\n\n\t\t\treturn row ? rowToUser(row) : null;\n\t\t},\n\n\t\tasync createUser(user: NewUser): Promise<User> {\n\t\t\tconst now = new Date().toISOString();\n\t\t\tconst id = ulid();\n\n\t\t\tconst row: Insertable<UserTable> = {\n\t\t\t\tid,\n\t\t\t\temail: user.email.toLowerCase(),\n\t\t\t\tname: user.name ?? null,\n\t\t\t\tavatar_url: user.avatarUrl ?? null,\n\t\t\t\trole: user.role ?? Role.SUBSCRIBER,\n\t\t\t\temail_verified: user.emailVerified ? 1 : 0,\n\t\t\t\tdisabled: 0,\n\t\t\t\tdata: user.data ? JSON.stringify(user.data) : null,\n\t\t\t\tcreated_at: now,\n\t\t\t\tupdated_at: now,\n\t\t\t};\n\n\t\t\tawait kdb.insertInto(\"users\").values(row).execute();\n\n\t\t\treturn {\n\t\t\t\tid,\n\t\t\t\temail: row.email,\n\t\t\t\tname: user.name ?? null,\n\t\t\t\tavatarUrl: user.avatarUrl ?? null,\n\t\t\t\trole: toRoleLevel(row.role),\n\t\t\t\temailVerified: row.email_verified === 1,\n\t\t\t\tdisabled: false,\n\t\t\t\tdata: user.data ?? null,\n\t\t\t\tcreatedAt: new Date(now),\n\t\t\t\tupdatedAt: new Date(now),\n\t\t\t};\n\t\t},\n\n\t\tasync updateUser(id: string, data: UpdateUser): Promise<void> {\n\t\t\tconst update: Updateable<UserTable> = {\n\t\t\t\tupdated_at: new Date().toISOString(),\n\t\t\t};\n\n\t\t\tif (data.email !== undefined) update.email = data.email.toLowerCase();\n\t\t\tif (data.name !== undefined) update.name = data.name;\n\t\t\tif (data.avatarUrl !== undefined) update.avatar_url = data.avatarUrl;\n\t\t\tif (data.role !== undefined) update.role = data.role;\n\t\t\tif (data.emailVerified !== undefined) update.email_verified = data.emailVerified ? 1 : 0;\n\t\t\tif (data.disabled !== undefined) update.disabled = data.disabled ? 1 : 0;\n\t\t\tif (data.data !== undefined) update.data = data.data ? JSON.stringify(data.data) : null;\n\n\t\t\tawait kdb.updateTable(\"users\").set(update).where(\"id\", \"=\", id).execute();\n\t\t},\n\n\t\tasync deleteUser(id: string): Promise<void> {\n\t\t\tawait kdb.deleteFrom(\"users\").where(\"id\", \"=\", id).execute();\n\t\t},\n\n\t\tasync countUsers(): Promise<number> {\n\t\t\tconst result = await kdb\n\t\t\t\t.selectFrom(\"users\")\n\t\t\t\t.select((eb) => eb.fn.countAll<number>().as(\"count\"))\n\t\t\t\t.executeTakeFirstOrThrow();\n\n\t\t\treturn result.count;\n\t\t},\n\n\t\tasync getUsers(options?: {\n\t\t\tsearch?: string;\n\t\t\trole?: number;\n\t\t\tcursor?: string;\n\t\t\tlimit?: number;\n\t\t}): Promise<{\n\t\t\titems: Array<\n\t\t\t\tUser & {\n\t\t\t\t\tlastLogin: Date | null;\n\t\t\t\t\tcredentialCount: number;\n\t\t\t\t\toauthProviders: string[];\n\t\t\t\t}\n\t\t\t>;\n\t\t\tnextCursor?: string;\n\t\t}> {\n\t\t\tconst limit = Math.min(options?.limit ?? 20, 100);\n\n\t\t\tlet query = kdb\n\t\t\t\t.selectFrom(\"users\")\n\t\t\t\t.leftJoin(\"credentials\", \"users.id\", \"credentials.user_id\")\n\t\t\t\t.selectAll(\"users\")\n\t\t\t\t.select((eb) => [\n\t\t\t\t\teb.fn.count<number>(\"credentials.id\").as(\"credential_count\"),\n\t\t\t\t\teb.fn.max(\"credentials.last_used_at\").as(\"last_login\"),\n\t\t\t\t])\n\t\t\t\t.groupBy(\"users.id\")\n\t\t\t\t.orderBy(\"users.created_at\", \"desc\")\n\t\t\t\t.limit(limit + 1);\n\n\t\t\t// Apply filters\n\t\t\tif (options?.search) {\n\t\t\t\tconst searchPattern = `%${options.search}%`;\n\t\t\t\tquery = query.where((eb) =>\n\t\t\t\t\teb.or([\n\t\t\t\t\t\teb(\"users.email\", \"like\", searchPattern),\n\t\t\t\t\t\teb(\"users.name\", \"like\", searchPattern),\n\t\t\t\t\t]),\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tif (options?.role !== undefined) {\n\t\t\t\tquery = query.where(\"users.role\", \"=\", options.role);\n\t\t\t}\n\n\t\t\tif (options?.cursor) {\n\t\t\t\t// Get the cursor user's created_at for pagination\n\t\t\t\tconst cursorUser = await kdb\n\t\t\t\t\t.selectFrom(\"users\")\n\t\t\t\t\t.select(\"created_at\")\n\t\t\t\t\t.where(\"id\", \"=\", options.cursor)\n\t\t\t\t\t.executeTakeFirst();\n\n\t\t\t\tif (cursorUser) {\n\t\t\t\t\tquery = query.where(\"users.created_at\", \"<\", cursorUser.created_at);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst rows = await query.execute();\n\n\t\t\t// Get OAuth providers for all users in this batch\n\t\t\tconst userIds = rows.slice(0, limit).map((r) => r.id);\n\t\t\tconst oauthAccounts =\n\t\t\t\tuserIds.length > 0\n\t\t\t\t\t? await kdb\n\t\t\t\t\t\t\t.selectFrom(\"oauth_accounts\")\n\t\t\t\t\t\t\t.select([\"user_id\", \"provider\"])\n\t\t\t\t\t\t\t.where(\"user_id\", \"in\", userIds)\n\t\t\t\t\t\t\t.execute()\n\t\t\t\t\t: [];\n\n\t\t\t// Group OAuth providers by user\n\t\t\tconst oauthByUser = new Map<string, string[]>();\n\t\t\tfor (const account of oauthAccounts) {\n\t\t\t\tconst providers = oauthByUser.get(account.user_id) ?? [];\n\t\t\t\tproviders.push(account.provider);\n\t\t\t\toauthByUser.set(account.user_id, providers);\n\t\t\t}\n\n\t\t\tconst hasMore = rows.length > limit;\n\t\t\tconst items = rows.slice(0, limit).map((row) => ({\n\t\t\t\tid: row.id,\n\t\t\t\temail: row.email,\n\t\t\t\tname: row.name,\n\t\t\t\tavatarUrl: row.avatar_url,\n\t\t\t\trole: toRoleLevel(row.role),\n\t\t\t\temailVerified: row.email_verified === 1,\n\t\t\t\tdisabled: row.disabled === 1,\n\t\t\t\tdata: row.data ? JSON.parse(row.data) : null,\n\t\t\t\tcreatedAt: new Date(row.created_at),\n\t\t\t\tupdatedAt: new Date(row.updated_at),\n\t\t\t\tlastLogin: row.last_login ? new Date(row.last_login) : null,\n\t\t\t\tcredentialCount: row.credential_count ?? 0,\n\t\t\t\toauthProviders: oauthByUser.get(row.id) ?? [],\n\t\t\t}));\n\n\t\t\treturn {\n\t\t\t\titems,\n\t\t\t\tnextCursor: hasMore ? items.at(-1)?.id : undefined,\n\t\t\t};\n\t\t},\n\n\t\tasync getUserWithDetails(id: string): Promise<{\n\t\t\tuser: User;\n\t\t\tcredentials: Credential[];\n\t\t\toauthAccounts: OAuthAccount[];\n\t\t\tlastLogin: Date | null;\n\t\t} | null> {\n\t\t\tconst user = await kdb\n\t\t\t\t.selectFrom(\"users\")\n\t\t\t\t.selectAll()\n\t\t\t\t.where(\"id\", \"=\", id)\n\t\t\t\t.executeTakeFirst();\n\n\t\t\tif (!user) return null;\n\n\t\t\tconst [credentials, oauthAccounts] = await Promise.all([\n\t\t\t\tkdb\n\t\t\t\t\t.selectFrom(\"credentials\")\n\t\t\t\t\t.selectAll()\n\t\t\t\t\t.where(\"user_id\", \"=\", id)\n\t\t\t\t\t.orderBy(\"created_at\", \"desc\")\n\t\t\t\t\t.execute(),\n\t\t\t\tkdb.selectFrom(\"oauth_accounts\").selectAll().where(\"user_id\", \"=\", id).execute(),\n\t\t\t]);\n\n\t\t\t// Find last login from most recent credential use\n\t\t\tconst lastLogin = credentials.reduce<Date | null>((latest, cred) => {\n\t\t\t\tconst lastUsed = new Date(cred.last_used_at);\n\t\t\t\treturn !latest || lastUsed > latest ? lastUsed : latest;\n\t\t\t}, null);\n\n\t\t\treturn {\n\t\t\t\tuser: rowToUser(user),\n\t\t\t\tcredentials: credentials.map(rowToCredential),\n\t\t\t\toauthAccounts: oauthAccounts.map(rowToOAuthAccount),\n\t\t\t\tlastLogin,\n\t\t\t};\n\t\t},\n\n\t\tasync countAdmins(): Promise<number> {\n\t\t\tconst result = await kdb\n\t\t\t\t.selectFrom(\"users\")\n\t\t\t\t.select((eb) => eb.fn.countAll<number>().as(\"count\"))\n\t\t\t\t.where(\"role\", \"=\", Role.ADMIN)\n\t\t\t\t.where(\"disabled\", \"=\", 0)\n\t\t\t\t.executeTakeFirstOrThrow();\n\n\t\t\treturn result.count;\n\t\t},\n\n\t\t// ========================================================================\n\t\t// Credentials\n\t\t// ========================================================================\n\n\t\tasync getCredentialById(id: string): Promise<Credential | null> {\n\t\t\tconst row = await kdb\n\t\t\t\t.selectFrom(\"credentials\")\n\t\t\t\t.selectAll()\n\t\t\t\t.where(\"id\", \"=\", id)\n\t\t\t\t.executeTakeFirst();\n\n\t\t\treturn row ? rowToCredential(row) : null;\n\t\t},\n\n\t\tasync getCredentialsByUserId(userId: string): Promise<Credential[]> {\n\t\t\tconst rows = await kdb\n\t\t\t\t.selectFrom(\"credentials\")\n\t\t\t\t.selectAll()\n\t\t\t\t.where(\"user_id\", \"=\", userId)\n\t\t\t\t.execute();\n\n\t\t\treturn rows.map(rowToCredential);\n\t\t},\n\n\t\tasync createCredential(credential: NewCredential): Promise<Credential> {\n\t\t\tconst now = new Date().toISOString();\n\n\t\t\tconst row: Insertable<CredentialTable> = {\n\t\t\t\tid: credential.id,\n\t\t\t\tuser_id: credential.userId,\n\t\t\t\tpublic_key: credential.publicKey,\n\t\t\t\tcounter: credential.counter,\n\t\t\t\tdevice_type: credential.deviceType,\n\t\t\t\tbacked_up: credential.backedUp ? 1 : 0,\n\t\t\t\ttransports: credential.transports.length > 0 ? JSON.stringify(credential.transports) : null,\n\t\t\t\tname: credential.name ?? null,\n\t\t\t\tcreated_at: now,\n\t\t\t\tlast_used_at: now,\n\t\t\t};\n\n\t\t\tawait kdb.insertInto(\"credentials\").values(row).execute();\n\n\t\t\treturn {\n\t\t\t\tid: credential.id,\n\t\t\t\tuserId: credential.userId,\n\t\t\t\tpublicKey: credential.publicKey,\n\t\t\t\tcounter: credential.counter,\n\t\t\t\tdeviceType: credential.deviceType,\n\t\t\t\tbackedUp: credential.backedUp,\n\t\t\t\ttransports: credential.transports,\n\t\t\t\tname: credential.name ?? null,\n\t\t\t\tcreatedAt: new Date(now),\n\t\t\t\tlastUsedAt: new Date(now),\n\t\t\t};\n\t\t},\n\n\t\tasync updateCredentialCounter(id: string, counter: number): Promise<void> {\n\t\t\tawait kdb\n\t\t\t\t.updateTable(\"credentials\")\n\t\t\t\t.set({\n\t\t\t\t\tcounter,\n\t\t\t\t\tlast_used_at: new Date().toISOString(),\n\t\t\t\t})\n\t\t\t\t.where(\"id\", \"=\", id)\n\t\t\t\t.execute();\n\t\t},\n\n\t\tasync updateCredentialName(id: string, name: string | null): Promise<void> {\n\t\t\tawait kdb.updateTable(\"credentials\").set({ name }).where(\"id\", \"=\", id).execute();\n\t\t},\n\n\t\tasync deleteCredential(id: string): Promise<void> {\n\t\t\tawait kdb.deleteFrom(\"credentials\").where(\"id\", \"=\", id).execute();\n\t\t},\n\n\t\tasync countCredentialsByUserId(userId: string): Promise<number> {\n\t\t\tconst result = await kdb\n\t\t\t\t.selectFrom(\"credentials\")\n\t\t\t\t.select((eb) => eb.fn.countAll<number>().as(\"count\"))\n\t\t\t\t.where(\"user_id\", \"=\", userId)\n\t\t\t\t.executeTakeFirstOrThrow();\n\n\t\t\treturn result.count;\n\t\t},\n\n\t\t// ========================================================================\n\t\t// Auth Tokens\n\t\t// ========================================================================\n\n\t\tasync createToken(token: NewAuthToken): Promise<void> {\n\t\t\tconst row: Insertable<AuthTokenTable> = {\n\t\t\t\thash: token.hash,\n\t\t\t\tuser_id: token.userId ?? null,\n\t\t\t\temail: token.email ?? null,\n\t\t\t\ttype: token.type,\n\t\t\t\trole: token.role ?? null,\n\t\t\t\tinvited_by: token.invitedBy ?? null,\n\t\t\t\texpires_at: token.expiresAt.toISOString(),\n\t\t\t\tcreated_at: new Date().toISOString(),\n\t\t\t};\n\n\t\t\tawait kdb.insertInto(\"auth_tokens\").values(row).execute();\n\t\t},\n\n\t\tasync getToken(hash: string, type: TokenType): Promise<AuthToken | null> {\n\t\t\tconst row = await kdb\n\t\t\t\t.selectFrom(\"auth_tokens\")\n\t\t\t\t.selectAll()\n\t\t\t\t.where(\"hash\", \"=\", hash)\n\t\t\t\t.where(\"type\", \"=\", type)\n\t\t\t\t.executeTakeFirst();\n\n\t\t\treturn row ? rowToAuthToken(row) : null;\n\t\t},\n\n\t\tasync deleteToken(hash: string): Promise<void> {\n\t\t\tawait kdb.deleteFrom(\"auth_tokens\").where(\"hash\", \"=\", hash).execute();\n\t\t},\n\n\t\tasync deleteExpiredTokens(): Promise<void> {\n\t\t\tawait kdb\n\t\t\t\t.deleteFrom(\"auth_tokens\")\n\t\t\t\t.where(\"expires_at\", \"<\", new Date().toISOString())\n\t\t\t\t.execute();\n\t\t},\n\n\t\t// ========================================================================\n\t\t// OAuth Accounts\n\t\t// ========================================================================\n\n\t\tasync getOAuthAccount(\n\t\t\tprovider: string,\n\t\t\tproviderAccountId: string,\n\t\t): Promise<OAuthAccount | null> {\n\t\t\tconst row = await kdb\n\t\t\t\t.selectFrom(\"oauth_accounts\")\n\t\t\t\t.selectAll()\n\t\t\t\t.where(\"provider\", \"=\", provider)\n\t\t\t\t.where(\"provider_account_id\", \"=\", providerAccountId)\n\t\t\t\t.executeTakeFirst();\n\n\t\t\treturn row ? rowToOAuthAccount(row) : null;\n\t\t},\n\n\t\tasync getOAuthAccountsByUserId(userId: string): Promise<OAuthAccount[]> {\n\t\t\tconst rows = await kdb\n\t\t\t\t.selectFrom(\"oauth_accounts\")\n\t\t\t\t.selectAll()\n\t\t\t\t.where(\"user_id\", \"=\", userId)\n\t\t\t\t.execute();\n\n\t\t\treturn rows.map(rowToOAuthAccount);\n\t\t},\n\n\t\tasync createOAuthAccount(account: NewOAuthAccount): Promise<OAuthAccount> {\n\t\t\tconst now = new Date().toISOString();\n\n\t\t\tconst row: Insertable<OAuthAccountTable> = {\n\t\t\t\tprovider: account.provider,\n\t\t\t\tprovider_account_id: account.providerAccountId,\n\t\t\t\tuser_id: account.userId,\n\t\t\t\tcreated_at: now,\n\t\t\t};\n\n\t\t\tawait kdb.insertInto(\"oauth_accounts\").values(row).execute();\n\n\t\t\treturn {\n\t\t\t\tprovider: account.provider,\n\t\t\t\tproviderAccountId: account.providerAccountId,\n\t\t\t\tuserId: account.userId,\n\t\t\t\tcreatedAt: new Date(now),\n\t\t\t};\n\t\t},\n\n\t\tasync deleteOAuthAccount(provider: string, providerAccountId: string): Promise<void> {\n\t\t\tawait kdb\n\t\t\t\t.deleteFrom(\"oauth_accounts\")\n\t\t\t\t.where(\"provider\", \"=\", provider)\n\t\t\t\t.where(\"provider_account_id\", \"=\", providerAccountId)\n\t\t\t\t.execute();\n\t\t},\n\n\t\t// ========================================================================\n\t\t// Allowed Domains\n\t\t// ========================================================================\n\n\t\tasync getAllowedDomain(domain: string): Promise<AllowedDomain | null> {\n\t\t\tconst row = await kdb\n\t\t\t\t.selectFrom(\"allowed_domains\")\n\t\t\t\t.selectAll()\n\t\t\t\t.where(\"domain\", \"=\", domain.toLowerCase())\n\t\t\t\t.executeTakeFirst();\n\n\t\t\treturn row ? rowToAllowedDomain(row) : null;\n\t\t},\n\n\t\tasync getAllowedDomains(): Promise<AllowedDomain[]> {\n\t\t\tconst rows = await kdb.selectFrom(\"allowed_domains\").selectAll().execute();\n\n\t\t\treturn rows.map(rowToAllowedDomain);\n\t\t},\n\n\t\tasync createAllowedDomain(domain: string, defaultRole: RoleLevel): Promise<AllowedDomain> {\n\t\t\tconst now = new Date().toISOString();\n\n\t\t\tconst row: Insertable<AllowedDomainTable> = {\n\t\t\t\tdomain: domain.toLowerCase(),\n\t\t\t\tdefault_role: defaultRole,\n\t\t\t\tenabled: 1,\n\t\t\t\tcreated_at: now,\n\t\t\t};\n\n\t\t\tawait kdb.insertInto(\"allowed_domains\").values(row).execute();\n\n\t\t\treturn {\n\t\t\t\tdomain: row.domain,\n\t\t\t\tdefaultRole,\n\t\t\t\tenabled: true,\n\t\t\t\tcreatedAt: new Date(now),\n\t\t\t};\n\t\t},\n\n\t\tasync updateAllowedDomain(\n\t\t\tdomain: string,\n\t\t\tenabled: boolean,\n\t\t\tdefaultRole?: RoleLevel,\n\t\t): Promise<void> {\n\t\t\tconst update: Updateable<AllowedDomainTable> = {\n\t\t\t\tenabled: enabled ? 1 : 0,\n\t\t\t};\n\n\t\t\tif (defaultRole !== undefined) {\n\t\t\t\tupdate.default_role = defaultRole;\n\t\t\t}\n\n\t\t\tawait kdb\n\t\t\t\t.updateTable(\"allowed_domains\")\n\t\t\t\t.set(update)\n\t\t\t\t.where(\"domain\", \"=\", domain.toLowerCase())\n\t\t\t\t.execute();\n\t\t},\n\n\t\tasync deleteAllowedDomain(domain: string): Promise<void> {\n\t\t\tawait kdb.deleteFrom(\"allowed_domains\").where(\"domain\", \"=\", domain.toLowerCase()).execute();\n\t\t},\n\t};\n}\n\n// ============================================================================\n// Row converters\n// ============================================================================\n\nfunction rowToUser(row: Selectable<UserTable>): User {\n\treturn {\n\t\tid: row.id,\n\t\temail: row.email,\n\t\tname: row.name,\n\t\tavatarUrl: row.avatar_url,\n\t\trole: toRoleLevel(row.role),\n\t\temailVerified: row.email_verified === 1,\n\t\tdisabled: row.disabled === 1,\n\t\tdata: row.data ? JSON.parse(row.data) : null,\n\t\tcreatedAt: new Date(row.created_at),\n\t\tupdatedAt: new Date(row.updated_at),\n\t};\n}\n\nfunction rowToCredential(row: Selectable<CredentialTable>): Credential {\n\treturn {\n\t\tid: row.id,\n\t\tuserId: row.user_id,\n\t\tpublicKey: row.public_key,\n\t\tcounter: row.counter,\n\t\tdeviceType: toDeviceType(row.device_type),\n\t\tbackedUp: row.backed_up === 1,\n\t\ttransports: row.transports ? JSON.parse(row.transports) : [],\n\t\tname: row.name,\n\t\tcreatedAt: new Date(row.created_at),\n\t\tlastUsedAt: new Date(row.last_used_at),\n\t};\n}\n\nfunction rowToAuthToken(row: Selectable<AuthTokenTable>): AuthToken {\n\treturn {\n\t\thash: row.hash,\n\t\tuserId: row.user_id,\n\t\temail: row.email,\n\t\ttype: toTokenType(row.type),\n\t\trole: row.role != null ? toRoleLevel(row.role) : null,\n\t\tinvitedBy: row.invited_by,\n\t\texpiresAt: new Date(row.expires_at),\n\t\tcreatedAt: new Date(row.created_at),\n\t};\n}\n\nfunction rowToOAuthAccount(row: Selectable<OAuthAccountTable>): OAuthAccount {\n\treturn {\n\t\tprovider: row.provider,\n\t\tproviderAccountId: row.provider_account_id,\n\t\tuserId: row.user_id,\n\t\tcreatedAt: new Date(row.created_at),\n\t};\n}\n\nfunction rowToAllowedDomain(row: Selectable<AllowedDomainTable>): AllowedDomain {\n\treturn {\n\t\tdomain: row.domain,\n\t\tdefaultRole: toRoleLevel(row.default_role),\n\t\tenabled: row.enabled === 1,\n\t\tcreatedAt: new Date(row.created_at),\n\t};\n}\n\n// ============================================================================\n// Migration SQL\n// ============================================================================\n\nexport const AUTH_TABLES_SQL = `\n-- Users (no password_hash)\nCREATE TABLE IF NOT EXISTS users (\n id TEXT PRIMARY KEY,\n email TEXT UNIQUE NOT NULL,\n name TEXT,\n avatar_url TEXT,\n role INTEGER NOT NULL DEFAULT 10,\n email_verified INTEGER NOT NULL DEFAULT 0,\n disabled INTEGER NOT NULL DEFAULT 0,\n data TEXT,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\n\nCREATE INDEX IF NOT EXISTS idx_users_email ON users(email);\n\n-- Passkey credentials\nCREATE TABLE IF NOT EXISTS credentials (\n id TEXT PRIMARY KEY,\n user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,\n public_key BLOB NOT NULL,\n counter INTEGER NOT NULL DEFAULT 0,\n device_type TEXT NOT NULL,\n backed_up INTEGER NOT NULL DEFAULT 0,\n transports TEXT,\n name TEXT,\n created_at TEXT NOT NULL,\n last_used_at TEXT NOT NULL\n);\n\nCREATE INDEX IF NOT EXISTS idx_credentials_user ON credentials(user_id);\n\n-- Auth tokens (magic links, email verification, invites)\nCREATE TABLE IF NOT EXISTS auth_tokens (\n hash TEXT PRIMARY KEY,\n user_id TEXT REFERENCES users(id) ON DELETE CASCADE,\n email TEXT,\n type TEXT NOT NULL,\n role INTEGER,\n invited_by TEXT REFERENCES users(id),\n expires_at TEXT NOT NULL,\n created_at TEXT NOT NULL\n);\n\nCREATE INDEX IF NOT EXISTS idx_auth_tokens_email ON auth_tokens(email);\n\n-- OAuth accounts (external provider links)\nCREATE TABLE IF NOT EXISTS oauth_accounts (\n provider TEXT NOT NULL,\n provider_account_id TEXT NOT NULL,\n user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,\n created_at TEXT NOT NULL,\n PRIMARY KEY (provider, provider_account_id)\n);\n\nCREATE INDEX IF NOT EXISTS idx_oauth_accounts_user ON oauth_accounts(user_id);\n\n-- Allowed domains for self-signup\nCREATE TABLE IF NOT EXISTS allowed_domains (\n domain TEXT PRIMARY KEY,\n default_role INTEGER NOT NULL DEFAULT 20,\n enabled INTEGER NOT NULL DEFAULT 1,\n created_at TEXT NOT NULL\n);\n`;\n"],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { T as User, c as DeviceType, n as AuthAdapter, o as AuthenticatorTransport, s as Credential } from "./types-
|
|
1
|
+
import { T as User, c as DeviceType, n as AuthAdapter, o as AuthenticatorTransport, s as Credential } from "./types-ByJGa0Mk.mjs";
|
|
2
2
|
|
|
3
3
|
//#region src/passkey/types.d.ts
|
|
4
4
|
interface RegistrationOptions {
|
|
@@ -121,4 +121,4 @@ declare function verifyAuthenticationResponse(config: PasskeyConfig, response: A
|
|
|
121
121
|
declare function authenticateWithPasskey(config: PasskeyConfig, adapter: AuthAdapter, response: AuthenticationResponse, challengeStore: ChallengeStore): Promise<User>;
|
|
122
122
|
//#endregion
|
|
123
123
|
export { registerPasskey as a, AuthenticationResponse as c, PasskeyConfig as d, RegistrationOptions as f, VerifiedRegistration as h, generateRegistrationOptions as i, ChallengeData as l, VerifiedAuthentication as m, generateAuthenticationOptions as n, verifyRegistrationResponse as o, RegistrationResponse as p, verifyAuthenticationResponse as r, AuthenticationOptions as s, authenticateWithPasskey as t, ChallengeStore as u };
|
|
124
|
-
//# sourceMappingURL=authenticate-
|
|
124
|
+
//# sourceMappingURL=authenticate-AIvzeyyc.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"authenticate-
|
|
1
|
+
{"version":3,"file":"authenticate-AIvzeyyc.d.mts","names":[],"sources":["../src/passkey/types.ts","../src/passkey/register.ts","../src/passkey/authenticate.ts"],"mappings":";;;UAUiB,mBAAA;EAChB,SAAA;EACA,EAAA;IACC,IAAA;IACA,EAAA;EAAA;EAED,IAAA;IACC,EAAA;IACA,IAAA;IACA,WAAA;EAAA;EAED,gBAAA,EAAkB,KAAA;IACjB,IAAA;IACA,GAAA;EAAA;EAED,OAAA;EACA,WAAA;EACA,sBAAA;IACC,uBAAA;IACA,WAAA;IACA,kBAAA;IACA,gBAAA;EAAA;EAED,kBAAA,GAAqB,KAAA;IACpB,IAAA;IACA,EAAA;IACA,UAAA,GAAa,sBAAA;EAAA;AAAA;AAAA,UAIE,oBAAA;EAChB,EAAA;EACA,KAAA;EACA,IAAA;EACA,QAAA;IACC,cAAA;IACA,iBAAA;IACA,UAAA,GAAa,sBAAA;EAAA;EAEd,uBAAA;AAAA;AAAA,UAGgB,oBAAA;EAChB,YAAA;EACA,SAAA,EAAW,UAAA;EACX,OAAA;EACA,UAAA,EAAY,UAAA;EACZ,QAAA;EACA,UAAA,EAAY,sBAAA;AAAA;AAAA,UAOI,qBAAA;EAChB,SAAA;EACA,IAAA;EACA,OAAA;EACA,gBAAA;EACA,gBAAA,GAAmB,KAAA;IAClB,IAAA;IACA,EAAA;IACA,UAAA,GAAa,sBAAA;EAAA;AAAA;AAAA,UAIE,sBAAA;EAChB,EAAA;EACA,KAAA;EACA,IAAA;EACA,QAAA;IACC,cAAA;IACA,iBAAA;IACA,SAAA;IACA,UAAA;EAAA;EAED,uBAAA;AAAA;AAAA,UAGgB,sBAAA;EAChB,YAAA;EACA,UAAA;AAAA;AAAA,UAOgB,cAAA;EAChB,GAAA,CAAI,SAAA,UAAmB,IAAA,EAAM,aAAA,GAAgB,OAAA;EAC7C,GAAA,CAAI,SAAA,WAAoB,OAAA,CAAQ,aAAA;EAChC,MAAA,CAAO,SAAA,WAAoB,OAAA;AAAA;AAAA,UAGX,aAAA;EAChB,IAAA;EACA,MAAA;EACA,SAAA;AAAA;AAAA,UAOgB,aAAA;EAChB,MAAA;EACA,IAAA;EACA,MAAA;AAAA;;;;;;iBCjFqB,2BAAA,CACrB,MAAA,EAAQ,aAAA,EACR,IAAA,EAAM,IAAA,CAAK,IAAA,4BACX,mBAAA,EAAqB,UAAA,IACrB,cAAA,EAAgB,cAAA,GACd,OAAA,CAAQ,mBAAA;;;;iBA8CW,0BAAA,CACrB,MAAA,EAAQ,aAAA,EACR,QAAA,EAAU,oBAAA,EACV,cAAA,EAAgB,cAAA,GACd,OAAA,CAAQ,oBAAA;;;;iBA6GW,eAAA,CACrB,OAAA,EAAS,WAAA,EACT,MAAA,UACA,QAAA,EAAU,oBAAA,EACV,IAAA,YACE,OAAA,CAAQ,UAAA;;;;;;iBCzKW,6BAAA,CACrB,MAAA,EAAQ,aAAA,EACR,WAAA,EAAa,UAAA,IACb,cAAA,EAAgB,cAAA,GACd,OAAA,CAAQ,qBAAA;;;;iBA4BW,4BAAA,CACrB,MAAA,EAAQ,aAAA,EACR,QAAA,EAAU,sBAAA,EACV,UAAA,EAAY,UAAA,EACZ,cAAA,EAAgB,cAAA,GACd,OAAA,CAAQ,sBAAA;;;;iBAmFW,uBAAA,CACrB,MAAA,EAAQ,aAAA,EACR,OAAA,EAAS,WAAA,EACT,QAAA,EAAU,sBAAA,EACV,cAAA,EAAgB,cAAA,GACd,OAAA,CAAQ,IAAA"}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
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
|
-
|
|
127
|
-
const
|
|
128
|
-
const
|
|
129
|
-
|
|
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-
|
|
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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"}
|
package/dist/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { A as toDeviceType, C as TokenType, D as UserWithDetails, E as UserListItem, M as toTokenType, O as roleFromLevel, S as SessionData, T as User, _ as OAuthConnection, a as AuthToken, b as RoleName, c as DeviceType, d as NewAuthToken, f as NewCredential, g as OAuthClient, h as OAuthAccount, i as AuthErrorCode, j as toRoleLevel, k as roleToLevel, l as EmailAdapter, m as NewUser, n as AuthAdapter, o as AuthenticatorTransport, p as NewOAuthAccount, r as AuthError, s as Credential, t as AllowedDomain, u as EmailMessage, v as Role, w as UpdateUser, x as Session, y as RoleLevel } from "./types-
|
|
2
|
-
import { a as registerPasskey, c as AuthenticationResponse, d as PasskeyConfig, f as RegistrationOptions, h as VerifiedRegistration, i as generateRegistrationOptions, l as ChallengeData, m as VerifiedAuthentication, n as generateAuthenticationOptions, o as verifyRegistrationResponse, p as RegistrationResponse, r as verifyAuthenticationResponse, s as AuthenticationOptions, t as authenticateWithPasskey, u as ChallengeStore } from "./authenticate-
|
|
1
|
+
import { A as toDeviceType, C as TokenType, D as UserWithDetails, E as UserListItem, M as toTokenType, O as roleFromLevel, S as SessionData, T as User, _ as OAuthConnection, a as AuthToken, b as RoleName, c as DeviceType, d as NewAuthToken, f as NewCredential, g as OAuthClient, h as OAuthAccount, i as AuthErrorCode, j as toRoleLevel, k as roleToLevel, l as EmailAdapter, m as NewUser, n as AuthAdapter, o as AuthenticatorTransport, p as NewOAuthAccount, r as AuthError, s as Credential, t as AllowedDomain, u as EmailMessage, v as Role, w as UpdateUser, x as Session, y as RoleLevel } from "./types-ByJGa0Mk.mjs";
|
|
2
|
+
import { a as registerPasskey, c as AuthenticationResponse, d as PasskeyConfig, f as RegistrationOptions, h as VerifiedRegistration, i as generateRegistrationOptions, l as ChallengeData, m as VerifiedAuthentication, n as generateAuthenticationOptions, o as verifyRegistrationResponse, p as RegistrationResponse, r as verifyAuthenticationResponse, s as AuthenticationOptions, t as authenticateWithPasskey, u as ChallengeStore } from "./authenticate-AIvzeyyc.mjs";
|
|
3
3
|
import { i as OAuthState, n as OAuthProfile, r as OAuthProvider, t as OAuthConfig } from "./types-Bu4irX9A.mjs";
|
|
4
4
|
import { github } from "./oauth/providers/github.mjs";
|
|
5
5
|
import { google } from "./oauth/providers/google.mjs";
|
package/dist/index.d.mts.map
CHANGED
|
@@ -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":";;;;;;;;;;;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;;;;;;;;;;;
|
|
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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"}
|
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
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-
|
|
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-
|
|
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-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";
|
|
@@ -9,7 +9,7 @@ import { encodeBase64urlNoPadding } from "@oslojs/encoding";
|
|
|
9
9
|
|
|
10
10
|
//#region src/config.ts
|
|
11
11
|
/**
|
|
12
|
-
* Configuration schema for @
|
|
12
|
+
* Configuration schema for @emdash-cms/auth
|
|
13
13
|
*/
|
|
14
14
|
/** Matches http(s) scheme at start of URL */
|
|
15
15
|
const HTTP_SCHEME_RE = /^https?:\/\//i;
|
|
@@ -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 =
|
|
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, {
|
package/dist/index.mjs.map
CHANGED
|
@@ -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 @emdashcms/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(\"&\", \"&\")\n\t\t.replaceAll(\"<\", \"<\")\n\t\t.replaceAll(\">\", \">\")\n\t\t.replaceAll('\"', \""\");\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 * @emdashcms/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 '@emdashcms/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(\"&\", \"&\")\n\t\t.replaceAll(\"<\", \"<\")\n\t\t.replaceAll(\">\", \">\")\n\t\t.replaceAll('\"', \""\");\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":";;;;;;;;;;;;;;AASA,MAAM,iBAAiB;;AAGvB,MAAM,UAAU,EACd,QAAQ,CACR,KAAK,CACL,QAAQ,QAAQ,eAAe,KAAK,IAAI,EAAE,6BAA6B;;;;AAKzE,MAAM,sBAAsB,EAAE,OAAO;CACpC,UAAU,EAAE,QAAQ;CACpB,cAAc,EAAE,QAAQ;CACxB,CAAC;;;;AAKF,MAAa,mBAAmB,EAAE,OAAO;CAKxC,QAAQ,EAAE,QAAQ,CAAC,IAAI,IAAI,6CAA6C;CAKxE,UAAU,EACR,OAAO;EAIP,QAAQ,EAAE,QAAQ;EAIlB,MAAM,EAAE,QAAQ,CAAC,UAAU;EAC3B,CAAC,CACD,UAAU;CAKZ,YAAY,EACV,OAAO;EAIP,SAAS,EAAE,MAAM,EAAE,QAAQ,CAAC;EAI5B,aAAa,EAAE,KAAK;GAAC;GAAc;GAAe;GAAS,CAAU,CAAC,QAAQ,cAAc;EAC5F,CAAC,CACD,UAAU;CAKZ,OAAO,EACL,OAAO;EACP,QAAQ,oBAAoB,UAAU;EACtC,QAAQ,oBAAoB,UAAU;EACtC,CAAC,CACD,UAAU;CAKZ,UAAU,EACR,OAAO;EACP,SAAS,EAAE,SAAS;EAIpB,QAAQ,QAAQ,UAAU;EAC1B,CAAC,CACD,UAAU;CAKZ,KAAK,EACH,OAAO,EACP,SAAS,EAAE,SAAS,EACpB,CAAC,CACD,UAAU;CAKZ,SAAS,EACP,OAAO;EAIP,QAAQ,EAAE,QAAQ,CAAC,QAAQ,MAAU,KAAK,GAAG;EAI7C,SAAS,EAAE,SAAS,CAAC,QAAQ,KAAK;EAClC,CAAC,CACD,UAAU;CACZ,CAAC;AAiDF,MAAM,oBAA+E;CACpF,YAAY;CACZ,aAAa;CACb,QAAQ;CACR;;;;AAKD,SAAgB,cACf,QACA,SACA,UACqB;CACrB,MAAM,MAAM,IAAI,IAAI,QAAQ;AAE5B,QAAO;EACN,QAAQ,OAAO;EACf;EACA;EAEA,UAAU;GACT,QAAQ,OAAO,UAAU,UAAU;GACnC,MAAM,OAAO,UAAU,QAAQ,IAAI;GACnC,QAAQ,IAAI;GACZ;EAED,YAAY,OAAO,aAChB;GACA,SAAS,OAAO,WAAW,QAAQ,KAAK,MAAM,EAAE,aAAa,CAAC;GAC9D,aAAa,kBAAkB,OAAO,WAAW;GACjD,GACA;EAEH,OAAO,OAAO;EAEd,UAAU,OAAO,WACd;GACA,SAAS,OAAO,SAAS;GACzB,QAAQ,OAAO,SAAS,UAAU;GAClC,GACA;EAEH,KAAK,OAAO;EAEZ,SAAS;GACR,QAAQ,OAAO,SAAS,UAAU,MAAU,KAAK;GACjD,SAAS,OAAO,SAAS,WAAW;GACpC;EACD;;;;;;;;AC1MF,MAAa,cAAc;CAE1B,gBAAgB,KAAK;CACrB,kBAAkB,KAAK;CACvB,oBAAoB,KAAK;CACzB,oBAAoB,KAAK;CACzB,sBAAsB,KAAK;CAC3B,sBAAsB,KAAK;CAC3B,uBAAuB,KAAK;CAC5B,uBAAuB,KAAK;CAG5B,cAAc,KAAK;CACnB,gBAAgB,KAAK;CACrB,kBAAkB,KAAK;CACvB,kBAAkB,KAAK;CACvB,oBAAoB,KAAK;CACzB,oBAAoB,KAAK;CAGzB,mBAAmB,KAAK;CACxB,qBAAqB,KAAK;CAG1B,iBAAiB,KAAK;CACtB,qBAAqB,KAAK;CAC1B,mBAAmB,KAAK;CACxB,qBAAqB,KAAK;CAG1B,cAAc,KAAK;CACnB,gBAAgB,KAAK;CAGrB,gBAAgB,KAAK;CACrB,kBAAkB,KAAK;CAGvB,iBAAiB,KAAK;CACtB,mBAAmB,KAAK;CAGxB,kBAAkB,KAAK;CACvB,oBAAoB,KAAK;CAGzB,cAAc,KAAK;CACnB,gBAAgB,KAAK;CACrB,gBAAgB,KAAK;CAGrB,iBAAiB,KAAK;CACtB,mBAAmB,KAAK;CAGxB,eAAe,KAAK;CACpB,iBAAiB,KAAK;CAGtB,gBAAgB,KAAK;CACrB,kBAAkB,KAAK;CAGvB,kBAAkB,KAAK;CAGvB,eAAe,KAAK;CACpB,iBAAiB,KAAK;CAGtB,+BAA+B,KAAK;CACpC,2BAA2B,KAAK;CAChC;;;;AAOD,SAAgB,cACf,MACA,YACU;AACV,KAAI,CAAC,KAAM,QAAO;AAClB,QAAO,KAAK,QAAQ,YAAY;;;;;AAMjC,SAAgB,kBACf,MACA,YACsC;AACtC,KAAI,CAAC,KACJ,OAAM,IAAI,gBAAgB,gBAAgB,0BAA0B;AAErE,KAAI,CAAC,cAAc,MAAM,WAAW,CACnC,OAAM,IAAI,gBAAgB,aAAa,uBAAuB,aAAa;;;;;AAO7E,SAAgB,YACf,MACA,SACA,eACA,eACU;AACV,KAAI,CAAC,KAAM,QAAO;AAClB,KAAI,KAAK,OAAO,QACf,QAAO,cAAc,MAAM,cAAc;AAE1C,QAAO,cAAc,MAAM,cAAc;;;;;AAM1C,SAAgB,4BACf,MACA,SACA,eACA,eACkD;AAClD,KAAI,CAAC,KACJ,OAAM,IAAI,gBAAgB,gBAAgB,0BAA0B;AAErE,KAAI,CAAC,YAAY,MAAM,SAAS,eAAe,cAAc,CAC5D,OAAM,IAAI,gBAAgB,aAAa,uBAAuB,gBAAgB;;AAIhF,IAAa,kBAAb,cAAqC,MAAM;CAC1C,YACC,AAAO,MACP,SACC;AACD,QAAM,QAAQ;EAHP;AAIP,OAAK,OAAO;;;;;;;;;;AAkBd,MAAM,iBAAmD;CACxD,gBAAgB,KAAK;CACrB,iBAAiB,KAAK;CACtB,cAAc,KAAK;CACnB,eAAe,KAAK;CACpB,eAAe,KAAK;CACpB,gBAAgB,KAAK;CACrB,OAAO,KAAK;CACZ;;;;;;;AAQD,SAAgB,cAAc,MAAkC;AAG/D,QADgB,OAAO,QAAQ,eAAe,CAC/B,QAAyB,KAAK,CAAC,OAAO,aAAa;AACjE,MAAI,QAAQ,QAAS,KAAI,KAAK,MAAM;AACpC,SAAO;IACL,EAAE,CAAC;;;;;;;;;AAUP,SAAgB,YAAY,WAAqB,MAA2B;CAC3E,MAAM,UAAU,IAAI,IAAY,cAAc,KAAK,CAAC;AACpD,QAAO,UAAU,QAAQ,MAAM,QAAQ,IAAI,EAAE,CAAC;;;;;;;;;ACnM/C,SAAgB,WAAW,GAAmB;AAC7C,QAAO,EACL,WAAW,KAAK,QAAQ,CACxB,WAAW,KAAK,OAAO,CACvB,WAAW,KAAK,OAAO,CACvB,WAAW,MAAK,SAAS;;AAG5B,MAAMA,oBAAkB,QAAc,KAAK;;;;;;;;AA2B3C,eAAsB,kBACrB,QACA,SACA,OACA,MACA,WAC6B;AAG7B,KADiB,MAAM,QAAQ,eAAe,MAAM,CAEnD,OAAM,IAAI,YAAY,eAAe,wCAAwC;CAI9E,MAAM,EAAE,OAAO,SAAS,uBAAuB;AAG/C,OAAM,QAAQ,YAAY;EACzB;EACA;EACA,MAAM;EACN;EACA;EACA,WAAW,IAAI,KAAK,KAAK,KAAK,GAAGA,kBAAgB;EACjD,CAAC;CAGF,MAAM,MAAM,IAAI,IAAI,mCAAmC,OAAO,QAAQ;AACtE,KAAI,aAAa,IAAI,SAAS,MAAM;AAEpC,QAAO;EAAE,KAAK,IAAI,UAAU;EAAE;EAAO;;;;;AAMtC,SAAS,iBAAiB,WAAmB,OAAe,UAAgC;CAC3F,MAAM,WAAW,WAAW,SAAS;AACrC,QAAO;EACN,IAAI;EACJ,SAAS,0BAA0B;EACnC,MAAM,+BAA+B,SAAS,gDAAgD,UAAU;EACxG,MAAM;;;;;;;;6EAQqE,SAAS;;;eAGvE,UAAU;;;;;EAKvB;;;;;;;;;AAUF,eAAsB,aACrB,QACA,SACA,OACA,MACA,WAC6B;CAC7B,MAAM,SAAS,MAAM,kBAAkB,QAAQ,SAAS,OAAO,MAAM,UAAU;AAG/E,KAAI,OAAO,OAAO;EACjB,MAAM,UAAU,iBAAiB,OAAO,KAAK,OAAO,OAAO,SAAS;AACpE,QAAM,OAAO,MAAM,QAAQ;;AAG5B,QAAO;;;;;AAMR,eAAsB,eACrB,SACA,OAC8C;CAC9C,MAAM,OAAO,UAAU,MAAM;CAE7B,MAAM,YAAY,MAAM,QAAQ,SAAS,MAAM,SAAS;AACxD,KAAI,CAAC,UACJ,OAAM,IAAI,YAAY,iBAAiB,iCAAiC;AAGzE,KAAI,UAAU,4BAAY,IAAI,MAAM,EAAE;AACrC,QAAM,QAAQ,YAAY,KAAK;AAC/B,QAAM,IAAI,YAAY,iBAAiB,0BAA0B;;AAGlE,KAAI,CAAC,UAAU,SAAS,UAAU,SAAS,KAC1C,OAAM,IAAI,YAAY,iBAAiB,sBAAsB;AAG9D,QAAO;EACN,OAAO,UAAU;EACjB,MAAM,UAAU;EAChB;;;;;AAMF,eAAsB,eACrB,SACA,OACA,UAIgB;CAChB,MAAM,OAAO,UAAU,MAAM;CAG7B,MAAM,YAAY,MAAM,QAAQ,SAAS,MAAM,SAAS;AACxD,KAAI,CAAC,aAAa,UAAU,4BAAY,IAAI,MAAM,CACjD,OAAM,IAAI,YAAY,iBAAiB,4BAA4B;AAGpE,KAAI,CAAC,UAAU,SAAS,UAAU,SAAS,KAC1C,OAAM,IAAI,YAAY,iBAAiB,sBAAsB;AAI9D,OAAM,QAAQ,YAAY,KAAK;AAW/B,QARa,MAAM,QAAQ,WAAW;EACrC,OAAO,UAAU;EACjB,MAAM,SAAS;EACf,WAAW,SAAS;EACpB,MAAM,UAAU;EAChB,eAAe;EACf,CAAC;;AAKH,IAAa,cAAb,cAAiC,MAAM;CACtC,YACC,AAAO,MACP,SACC;AACD,QAAM,QAAQ;EAHP;AAIP,OAAK,OAAO;;;;;;;;;AClMd,MAAMC,oBAAkB,MAAU;;;;;AAgBlC,eAAeC,gBAA6B;CAC3C,MAAM,QAAQ,MAAM,KAAK,QAAQ,GAAG;AACpC,OAAM,IAAI,SAAS,YAAY,WAAW,SAAS,MAAM,CAAC;;;;;;;AAQ3D,eAAsB,cACrB,QACA,SACA,OACA,OAAkC,cAClB;AAChB,KAAI,CAAC,OAAO,MACX,OAAM,IAAI,eAAe,wBAAwB,0BAA0B;CAI5E,MAAM,OAAO,MAAM,QAAQ,eAAe,MAAM;AAChD,KAAI,CAAC,MAAM;AAEV,QAAMA,eAAa;AACnB;;CAID,MAAM,EAAE,OAAO,SAAS,uBAAuB;AAG/C,OAAM,QAAQ,YAAY;EACzB;EACA,QAAQ,KAAK;EACb,OAAO,KAAK;EACZ;EACA,WAAW,IAAI,KAAK,KAAK,KAAK,GAAGD,kBAAgB;EACjD,CAAC;CAGF,MAAM,MAAM,IAAI,IAAI,uCAAuC,OAAO,QAAQ;AAC1E,KAAI,aAAa,IAAI,SAAS,MAAM;CAGpC,MAAM,WAAW,WAAW,OAAO,SAAS;AAC5C,OAAM,OAAO,MAAM;EAClB,IAAI,KAAK;EACT,SAAS,cAAc,OAAO;EAC9B,MAAM,iCAAiC,OAAO,SAAS,OAAO,IAAI,UAAU,CAAC;EAC7E,MAAM;;;;;;;;iEAQyD,SAAS;;;eAG3D,IAAI,UAAU,CAAC;;;;;;EAM5B,CAAC;;;;;AAMH,eAAsB,gBAAgB,SAAsB,OAA8B;CACzF,MAAM,OAAO,UAAU,MAAM;CAG7B,MAAM,YAAY,MAAM,QAAQ,SAAS,MAAM,aAAa;AAC5D,KAAI,CAAC,WAAW;EAEf,MAAM,gBAAgB,MAAM,QAAQ,SAAS,MAAM,WAAW;AAC9D,MAAI,CAAC,cACJ,OAAM,IAAI,eAAe,iBAAiB,0BAA0B;AAErE,SAAO,sBAAsB,SAAS,eAAe,KAAK;;AAG3D,QAAO,sBAAsB,SAAS,WAAW,KAAK;;AAGvD,eAAe,sBACd,SACA,WACA,MACgB;AAEhB,KAAI,UAAU,4BAAY,IAAI,MAAM,EAAE;AACrC,QAAM,QAAQ,YAAY,KAAK;AAC/B,QAAM,IAAI,eAAe,iBAAiB,wBAAwB;;AAInE,OAAM,QAAQ,YAAY,KAAK;AAG/B,KAAI,CAAC,UAAU,OACd,OAAM,IAAI,eAAe,iBAAiB,gBAAgB;CAG3D,MAAM,OAAO,MAAM,QAAQ,YAAY,UAAU,OAAO;AACxD,KAAI,CAAC,KACJ,OAAM,IAAI,eAAe,kBAAkB,iBAAiB;AAG7D,QAAO;;AAGR,IAAa,iBAAb,cAAoC,MAAM;CACzC,YACC,AAAO,MACP,SACC;AACD,QAAM,QAAQ;EAHP;AAIP,OAAK,OAAO;;;;;;;;;AC3Id,MAAM,kBAAkB,MAAU;;;;;AASlC,eAAe,cAA6B;CAC3C,MAAM,QAAQ,MAAM,KAAK,QAAQ,GAAG;AACpC,OAAM,IAAI,SAAS,YAAY,WAAW,SAAS,MAAM,CAAC;;;;;AAa3D,eAAsB,UACrB,SACA,OACwD;CACxD,MAAM,SAAS,MAAM,MAAM,IAAI,CAAC,IAAI,aAAa;AACjD,KAAI,CAAC,OAAQ,QAAO;CAEpB,MAAM,gBAAgB,MAAM,QAAQ,iBAAiB,OAAO;AAC5D,KAAI,CAAC,iBAAiB,CAAC,cAAc,QACpC,QAAO;AAGR,QAAO;EACN,SAAS;EACT,MAAM,cAAc;EACpB;;;;;;;AAQF,eAAsB,cACrB,QACA,SACA,OACgB;AAChB,KAAI,CAAC,OAAO,MACX,OAAM,IAAI,YAAY,wBAAwB,0BAA0B;AAKzE,KADiB,MAAM,QAAQ,eAAe,MAAM,EACtC;AAEb,QAAM,aAAa;AACnB;;CAID,MAAM,SAAS,MAAM,UAAU,SAAS,MAAM;AAC9C,KAAI,CAAC,QAAQ;AAEZ,QAAM,aAAa;AACnB;;CAID,MAAM,EAAE,OAAO,SAAS,uBAAuB;AAG/C,OAAM,QAAQ,YAAY;EACzB;EACA;EACA,MAAM;EACN,MAAM,OAAO;EACb,WAAW,IAAI,KAAK,KAAK,KAAK,GAAG,gBAAgB;EACjD,CAAC;CAGF,MAAM,MAAM,IAAI,IAAI,mCAAmC,OAAO,QAAQ;AACtE,KAAI,aAAa,IAAI,SAAS,MAAM;CAGpC,MAAM,WAAW,WAAW,OAAO,SAAS;AAC5C,OAAM,OAAO,MAAM;EAClB,IAAI;EACJ,SAAS,yBAAyB,OAAO;EACzC,MAAM,oEAAoE,IAAI,UAAU,CAAC;EACzF,MAAM;;;;;;;;;mEAS2D,SAAS;;eAE7D,IAAI,UAAU,CAAC;;;;;;EAM5B,CAAC;;;;;AAMH,eAAsB,oBACrB,SACA,OAC8C;CAC9C,MAAM,OAAO,UAAU,MAAM;CAE7B,MAAM,YAAY,MAAM,QAAQ,SAAS,MAAM,eAAe;AAC9D,KAAI,CAAC,UACJ,OAAM,IAAI,YAAY,iBAAiB,uCAAuC;AAG/E,KAAI,UAAU,4BAAY,IAAI,MAAM,EAAE;AACrC,QAAM,QAAQ,YAAY,KAAK;AAC/B,QAAM,IAAI,YAAY,iBAAiB,wBAAwB;;AAGhE,KAAI,CAAC,UAAU,SAAS,UAAU,SAAS,KAC1C,OAAM,IAAI,YAAY,iBAAiB,qBAAqB;AAG7D,QAAO;EACN,OAAO,UAAU;EACjB,MAAM,UAAU;EAChB;;;;;AAMF,eAAsB,eACrB,SACA,OACA,UAIgB;CAChB,MAAM,OAAO,UAAU,MAAM;CAG7B,MAAM,YAAY,MAAM,QAAQ,SAAS,MAAM,eAAe;AAC9D,KAAI,CAAC,aAAa,UAAU,4BAAY,IAAI,MAAM,CACjD,OAAM,IAAI,YAAY,iBAAiB,kCAAkC;AAG1E,KAAI,CAAC,UAAU,SAAS,UAAU,SAAS,KAC1C,OAAM,IAAI,YAAY,iBAAiB,qBAAqB;AAK7D,KADiB,MAAM,QAAQ,eAAe,UAAU,MAAM,EAChD;AACb,QAAM,QAAQ,YAAY,KAAK;AAC/B,QAAM,IAAI,YAAY,eAAe,4CAA4C;;AAIlF,OAAM,QAAQ,YAAY,KAAK;AAW/B,QARa,MAAM,QAAQ,WAAW;EACrC,OAAO,UAAU;EACjB,MAAM,SAAS;EACf,WAAW,SAAS;EACpB,MAAM,UAAU;EAChB,eAAe;EACf,CAAC;;AAKH,IAAa,cAAb,cAAiC,MAAM;CACtC,YACC,AAAO,MAMP,SACC;AACD,QAAM,QAAQ;EARP;AASP,OAAK,OAAO;;;;;;;;;;;;ACjLd,eAAsB,uBACrB,QACA,cACA,YAC0C;CAC1C,MAAM,iBAAiB,OAAO,UAAU;AACxC,KAAI,CAAC,eACJ,OAAM,IAAI,MAAM,kBAAkB,aAAa,iBAAiB;CAGjE,MAAM,WAAW,YAAY,aAAa;CAC1C,MAAM,QAAQ,eAAe;CAC7B,MAAM,cAAc,IAAI,IACvB,2BAA2B,aAAa,YACxC,OAAO,QACP,CAAC,UAAU;CAGZ,MAAM,eAAe,sBAAsB;CAC3C,MAAM,gBAAgB,MAAM,sBAAsB,aAAa;AAG/D,OAAM,WAAW,IAAI,OAAO;EAC3B,UAAU;EACV;EACA;EACA,CAAC;CAGF,MAAM,MAAM,IAAI,IAAI,SAAS,aAAa;AAC1C,KAAI,aAAa,IAAI,aAAa,eAAe,SAAS;AAC1D,KAAI,aAAa,IAAI,gBAAgB,YAAY;AACjD,KAAI,aAAa,IAAI,iBAAiB,OAAO;AAC7C,KAAI,aAAa,IAAI,SAAS,SAAS,OAAO,KAAK,IAAI,CAAC;AACxD,KAAI,aAAa,IAAI,SAAS,MAAM;AAGpC,KAAI,aAAa,IAAI,kBAAkB,cAAc;AACrD,KAAI,aAAa,IAAI,yBAAyB,OAAO;AAErD,QAAO;EAAE,KAAK,IAAI,UAAU;EAAE;EAAO;;;;;AAMtC,eAAsB,oBACrB,QACA,SACA,cACA,MACA,OACA,YACgB;CAChB,MAAM,iBAAiB,OAAO,UAAU;AACxC,KAAI,CAAC,eACJ,OAAM,IAAI,MAAM,kBAAkB,aAAa,iBAAiB;CAIjE,MAAM,cAAc,MAAM,WAAW,IAAI,MAAM;AAC/C,KAAI,CAAC,eAAe,YAAY,aAAa,aAC5C,OAAM,IAAI,WAAW,iBAAiB,sBAAsB;AAI7D,OAAM,WAAW,OAAO,MAAM;CAE9B,MAAM,WAAW,YAAY,aAAa;AAe1C,QAAO,iBAAiB,QAAQ,SAAS,cAHzB,MAAM,aAAa,WATpB,MAAM,aACpB,UACA,gBACA,MACA,YAAY,aACZ,YAAY,aACZ,EAGmD,aAAa,aAAa,CAGf;;;;;AAMhE,eAAe,aACd,UACA,QACA,MACA,aACA,cACqD;CACrD,MAAM,OAAO,IAAI,gBAAgB;EAChC,YAAY;EACZ;EACA,cAAc;EACd,WAAW,OAAO;EAClB,eAAe,OAAO;EACtB,CAAC;AAEF,KAAI,aACH,MAAK,IAAI,iBAAiB,aAAa;CAGxC,MAAM,WAAW,MAAM,MAAM,SAAS,UAAU;EAC/C,QAAQ;EACR,SAAS;GACR,gBAAgB;GAChB,QAAQ;GACR;EACD;EACA,CAAC;AAEF,KAAI,CAAC,SAAS,GAEb,OAAM,IAAI,WAAW,yBAAyB,0BADhC,MAAM,SAAS,MAAM,GAC6C;CAGjF,MAAM,OAAgB,MAAM,SAAS,MAAM;CAC3C,MAAM,OAAO,EACX,OAAO;EACP,cAAc,EAAE,QAAQ;EACxB,UAAU,EAAE,QAAQ,CAAC,UAAU;EAC/B,CAAC,CACD,MAAM,KAAK;AAEb,QAAO;EACN,aAAa,KAAK;EAClB,SAAS,KAAK;EACd;;;;;AAMF,eAAe,aACd,UACA,aACA,cACwB;AACxB,KAAI,CAAC,SAAS,YACb,OAAM,IAAI,MAAM,sCAAsC;CAGvD,MAAM,WAAW,MAAM,MAAM,SAAS,aAAa,EAClD,SAAS;EACR,eAAe,UAAU;EACzB,QAAQ;EACR,EACD,CAAC;AAEF,KAAI,CAAC,SAAS,GACb,OAAM,IAAI,WAAW,wBAAwB,4BAA4B,SAAS,SAAS;CAG5F,MAAM,OAAO,MAAM,SAAS,MAAM;CAClC,MAAM,UAAU,SAAS,aAAa,KAAK;AAG3C,KAAI,iBAAiB,YAAY,CAAC,QAAQ,MACzC,SAAQ,QAAQ,MAAM,iBAAiB,YAAY;AAGpD,QAAO;;;;;AAMR,eAAe,iBACd,QACA,SACA,cACA,SACgB;CAEhB,MAAM,kBAAkB,MAAM,QAAQ,gBAAgB,cAAc,QAAQ,GAAG;AAC/E,KAAI,iBAAiB;EACpB,MAAM,OAAO,MAAM,QAAQ,YAAY,gBAAgB,OAAO;AAC9D,MAAI,CAAC,KACJ,OAAM,IAAI,WAAW,kBAAkB,wBAAwB;AAEhE,SAAO;;CAMR,MAAM,eAAe,MAAM,QAAQ,eAAe,QAAQ,MAAM;AAChE,KAAI,cAAc;AACjB,MAAI,CAAC,QAAQ,cACZ,OAAM,IAAI,WACT,sBACA,sDACA;AAEF,QAAM,QAAQ,mBAAmB;GAChC,UAAU;GACV,mBAAmB,QAAQ;GAC3B,QAAQ,aAAa;GACrB,CAAC;AACF,SAAO;;AAIR,KAAI,OAAO,eAAe;EACzB,MAAM,SAAS,MAAM,OAAO,cAAc,QAAQ,MAAM;AACxD,MAAI,QAAQ,SAAS;GAEpB,MAAM,OAAO,MAAM,QAAQ,WAAW;IACrC,OAAO,QAAQ;IACf,MAAM,QAAQ;IACd,WAAW,QAAQ;IACnB,MAAM,OAAO;IACb,eAAe,QAAQ;IACvB,CAAC;AAGF,SAAM,QAAQ,mBAAmB;IAChC,UAAU;IACV,mBAAmB,QAAQ;IAC3B,QAAQ,KAAK;IACb,CAAC;AAEF,UAAO;;;AAIT,OAAM,IAAI,WAAW,sBAAsB,gDAAgD;;AAG5F,SAAS,YAAY,MAA0C;AAC9D,SAAQ,MAAR;EACC,KAAK,SACJ,QAAO;EACR,KAAK,SACJ,QAAO;;;;;;AAWV,SAAS,gBAAwB;CAChC,MAAM,QAAQ,IAAI,WAAW,GAAG;AAChC,QAAO,gBAAgB,MAAM;AAC7B,QAAO,yBAAyB,MAAM;;AAGvC,SAAS,uBAA+B;CACvC,MAAM,QAAQ,IAAI,WAAW,GAAG;AAChC,QAAO,gBAAgB,MAAM;AAC7B,QAAO,yBAAyB,MAAM;;AAGvC,eAAe,sBAAsB,UAAmC;AAGvE,QAAO,yBADM,OADC,IAAI,aAAa,CAAC,OAAO,SAAS,CACtB,CACW;;AAiBtC,IAAa,aAAb,cAAgC,MAAM;CACrC,YACC,AAAO,MAMP,SACC;AACD,QAAM,QAAQ;EARP;AASP,OAAK,OAAO;;;;;;;;;;;ACrMd,SAAgB,KAAK,QAA4E;CAEhG,MAAM,SAASE,iBAAkB,UAAU,OAAO;AAClD,KAAI,CAAC,OAAO,QACX,OAAM,IAAI,MAAM,wBAAwB,OAAO,MAAM,UAAU;AAEhE,QAAO,OAAO"}
|
package/dist/passkey/index.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as registerPasskey, c as AuthenticationResponse, d as PasskeyConfig, f as RegistrationOptions, h as VerifiedRegistration, i as generateRegistrationOptions, l as ChallengeData, m as VerifiedAuthentication, n as generateAuthenticationOptions, o as verifyRegistrationResponse, p as RegistrationResponse, r as verifyAuthenticationResponse, s as AuthenticationOptions, t as authenticateWithPasskey, u as ChallengeStore } from "../authenticate-
|
|
1
|
+
import { a as registerPasskey, c as AuthenticationResponse, d as PasskeyConfig, f as RegistrationOptions, h as VerifiedRegistration, i as generateRegistrationOptions, l as ChallengeData, m as VerifiedAuthentication, n as generateAuthenticationOptions, o as verifyRegistrationResponse, p as RegistrationResponse, r as verifyAuthenticationResponse, s as AuthenticationOptions, t as authenticateWithPasskey, u as ChallengeStore } from "../authenticate-AIvzeyyc.mjs";
|
|
2
2
|
export { type AuthenticationOptions, type AuthenticationResponse, type ChallengeData, type ChallengeStore, type PasskeyConfig, type RegistrationOptions, type RegistrationResponse, type VerifiedAuthentication, type VerifiedRegistration, authenticateWithPasskey, generateAuthenticationOptions, generateRegistrationOptions, registerPasskey, verifyAuthenticationResponse, verifyRegistrationResponse };
|
package/dist/passkey/index.mjs
CHANGED
|
@@ -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-
|
|
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 };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
//#region src/types.d.ts
|
|
2
2
|
/**
|
|
3
|
-
* Core types for @
|
|
3
|
+
* Core types for @emdash-cms/auth
|
|
4
4
|
*/
|
|
5
5
|
declare const Role: {
|
|
6
6
|
readonly SUBSCRIBER: 10;
|
|
@@ -205,4 +205,4 @@ declare class AuthError extends Error {
|
|
|
205
205
|
type AuthErrorCode = "invalid_credentials" | "invalid_token" | "token_expired" | "user_not_found" | "user_exists" | "credential_exists" | "max_credentials" | "email_not_verified" | "signup_not_allowed" | "domain_not_allowed" | "forbidden" | "unauthorized" | "rate_limited" | "invalid_request" | "internal_error";
|
|
206
206
|
//#endregion
|
|
207
207
|
export { toDeviceType as A, TokenType as C, UserWithDetails as D, UserListItem as E, toTokenType as M, roleFromLevel as O, SessionData as S, User as T, OAuthConnection as _, AuthToken as a, RoleName as b, DeviceType as c, NewAuthToken as d, NewCredential as f, OAuthClient as g, OAuthAccount as h, AuthErrorCode as i, toRoleLevel as j, roleToLevel as k, EmailAdapter as l, NewUser as m, AuthAdapter as n, AuthenticatorTransport as o, NewOAuthAccount as p, AuthError as r, Credential as s, AllowedDomain as t, EmailMessage as u, Role as v, UpdateUser as w, Session as x, RoleLevel as y };
|
|
208
|
-
//# sourceMappingURL=types-
|
|
208
|
+
//# sourceMappingURL=types-ByJGa0Mk.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types-
|
|
1
|
+
{"version":3,"file":"types-ByJGa0Mk.d.mts","names":[],"sources":["../src/types.ts"],"mappings":"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"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
//#region src/types.ts
|
|
2
2
|
/**
|
|
3
|
-
* Core types for @
|
|
3
|
+
* Core types for @emdash-cms/auth
|
|
4
4
|
*/
|
|
5
5
|
const Role = {
|
|
6
6
|
SUBSCRIBER: 10,
|
|
@@ -57,4 +57,4 @@ var AuthError = class extends Error {
|
|
|
57
57
|
|
|
58
58
|
//#endregion
|
|
59
59
|
export { toDeviceType as a, roleToLevel as i, Role as n, toRoleLevel as o, roleFromLevel as r, toTokenType as s, AuthError as t };
|
|
60
|
-
//# sourceMappingURL=types-
|
|
60
|
+
//# sourceMappingURL=types-ndj-bYfi.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types-ndj-bYfi.mjs","names":[],"sources":["../src/types.ts"],"sourcesContent":["/**\n * Core types for @emdash-cms/auth\n */\n\n// ============================================================================\n// Roles & Permissions\n// ============================================================================\n\nexport const Role = {\n\tSUBSCRIBER: 10,\n\tCONTRIBUTOR: 20,\n\tAUTHOR: 30,\n\tEDITOR: 40,\n\tADMIN: 50,\n} as const;\n\nexport type RoleLevel = (typeof Role)[keyof typeof Role];\nexport type RoleName = keyof typeof Role;\n\nexport function roleFromLevel(level: number): RoleName | undefined {\n\tconst entry = Object.entries(Role).find(([, v]) => v === level);\n\tif (!entry) return undefined;\n\tconst name = entry[0];\n\tif (isRoleName(name)) return name;\n\treturn undefined;\n}\n\nfunction isRoleName(value: string): value is RoleName {\n\treturn value in Role;\n}\n\nconst ROLE_LEVEL_MAP = new Map<number, RoleLevel>(Object.values(Role).map((v) => [v, v]));\n\nexport function toRoleLevel(value: number): RoleLevel {\n\tconst level = ROLE_LEVEL_MAP.get(value);\n\tif (level !== undefined) return level;\n\tthrow new Error(`Invalid role level: ${value}`);\n}\n\nconst DEVICE_TYPE_MAP: Record<string, DeviceType | undefined> = {\n\tsingleDevice: \"singleDevice\",\n\tmultiDevice: \"multiDevice\",\n};\n\nexport function toDeviceType(value: string): DeviceType {\n\tconst dt = DEVICE_TYPE_MAP[value];\n\tif (dt !== undefined) return dt;\n\tthrow new Error(`Invalid device type: ${value}`);\n}\n\nconst TOKEN_TYPE_MAP: Record<string, TokenType | undefined> = {\n\tmagic_link: \"magic_link\",\n\temail_verify: \"email_verify\",\n\tinvite: \"invite\",\n\trecovery: \"recovery\",\n};\n\nexport function toTokenType(value: string): TokenType {\n\tconst tt = TOKEN_TYPE_MAP[value];\n\tif (tt !== undefined) return tt;\n\tthrow new Error(`Invalid token type: ${value}`);\n}\n\nexport function roleToLevel(name: RoleName): RoleLevel {\n\treturn Role[name];\n}\n\n// ============================================================================\n// User\n// ============================================================================\n\nexport interface User {\n\tid: string;\n\temail: string;\n\tname: string | null;\n\tavatarUrl: string | null;\n\trole: RoleLevel;\n\temailVerified: boolean;\n\tdisabled: boolean;\n\tdata: Record<string, unknown> | null;\n\tcreatedAt: Date;\n\tupdatedAt: Date;\n}\n\nexport interface NewUser {\n\temail: string;\n\tname?: string | null;\n\tavatarUrl?: string | null;\n\trole?: RoleLevel;\n\temailVerified?: boolean;\n\tdata?: Record<string, unknown> | null;\n}\n\nexport interface UpdateUser {\n\temail?: string;\n\tname?: string | null;\n\tavatarUrl?: string | null;\n\trole?: RoleLevel;\n\temailVerified?: boolean;\n\tdisabled?: boolean;\n\tdata?: Record<string, unknown> | null;\n}\n\n// ============================================================================\n// Credentials (Passkeys)\n// ============================================================================\n\nexport type AuthenticatorTransport = \"usb\" | \"nfc\" | \"ble\" | \"internal\" | \"hybrid\";\nexport type DeviceType = \"singleDevice\" | \"multiDevice\";\n\nexport interface Credential {\n\tid: string; // Base64url credential ID\n\tuserId: string;\n\tpublicKey: Uint8Array; // COSE public key\n\tcounter: number;\n\tdeviceType: DeviceType;\n\tbackedUp: boolean;\n\ttransports: AuthenticatorTransport[];\n\tname: string | null;\n\tcreatedAt: Date;\n\tlastUsedAt: Date;\n}\n\nexport interface NewCredential {\n\tid: string;\n\tuserId: string;\n\tpublicKey: Uint8Array;\n\tcounter: number;\n\tdeviceType: DeviceType;\n\tbackedUp: boolean;\n\ttransports: AuthenticatorTransport[];\n\tname?: string | null;\n}\n\n// ============================================================================\n// Sessions\n// ============================================================================\n\nexport interface Session {\n\tid: string;\n\tuserId: string;\n\texpiresAt: Date;\n\tipAddress: string | null;\n\tuserAgent: string | null;\n\tcreatedAt: Date;\n}\n\nexport interface SessionData {\n\tuserId: string;\n\texpiresAt: number; // Unix timestamp\n}\n\n// ============================================================================\n// Auth Tokens (magic links, invites, etc.)\n// ============================================================================\n\nexport type TokenType = \"magic_link\" | \"email_verify\" | \"invite\" | \"recovery\";\n\nexport interface AuthToken {\n\thash: string; // SHA-256 hash of the raw token\n\tuserId: string | null; // null for pre-user tokens (invite/signup)\n\temail: string | null; // For pre-user tokens\n\ttype: TokenType;\n\trole: RoleLevel | null; // For invites\n\tinvitedBy: string | null;\n\texpiresAt: Date;\n\tcreatedAt: Date;\n}\n\nexport interface NewAuthToken {\n\thash: string;\n\tuserId?: string | null;\n\temail?: string | null;\n\ttype: TokenType;\n\trole?: RoleLevel | null;\n\tinvitedBy?: string | null;\n\texpiresAt: Date;\n}\n\n// ============================================================================\n// OAuth Accounts\n// ============================================================================\n\nexport interface OAuthAccount {\n\tprovider: string;\n\tproviderAccountId: string;\n\tuserId: string;\n\tcreatedAt: Date;\n}\n\nexport interface NewOAuthAccount {\n\tprovider: string;\n\tproviderAccountId: string;\n\tuserId: string;\n}\n\n// ============================================================================\n// OAuth Connections (SSO config)\n// ============================================================================\n\nexport interface OAuthConnection {\n\tid: string;\n\tname: string;\n\tprovider: \"oidc\" | \"github\" | \"google\";\n\tclientId: string;\n\tclientSecretEnc: string; // Encrypted\n\tissuerUrl: string | null;\n\tconfig: Record<string, unknown> | null;\n\tenabled: boolean;\n\tcreatedAt: Date;\n}\n\n// ============================================================================\n// OAuth Clients (when EmDash is provider)\n// ============================================================================\n\nexport interface OAuthClient {\n\tid: string;\n\tname: string;\n\tsecretHash: string;\n\tredirectUris: string[];\n\tscopes: string[];\n\tcreatedAt: Date;\n}\n\n// ============================================================================\n// Allowed Domains (self-signup)\n// ============================================================================\n\nexport interface AllowedDomain {\n\tdomain: string;\n\tdefaultRole: RoleLevel;\n\tenabled: boolean;\n\tcreatedAt: Date;\n}\n\n// ============================================================================\n// User Listing Types (for admin UI)\n// ============================================================================\n\n/** Extended user with list view computed fields */\nexport interface UserListItem extends User {\n\tlastLogin: Date | null;\n\tcredentialCount: number;\n\toauthProviders: string[];\n}\n\n/** User with full details including related data */\nexport interface UserWithDetails {\n\tuser: User;\n\tcredentials: Credential[];\n\toauthAccounts: OAuthAccount[];\n\tlastLogin: Date | null;\n}\n\n// ============================================================================\n// Auth Adapter Interface\n// ============================================================================\n\nexport interface AuthAdapter {\n\t// Users\n\tgetUserById(id: string): Promise<User | null>;\n\tgetUserByEmail(email: string): Promise<User | null>;\n\tcreateUser(user: NewUser): Promise<User>;\n\tupdateUser(id: string, data: UpdateUser): Promise<void>;\n\tdeleteUser(id: string): Promise<void>;\n\tcountUsers(): Promise<number>;\n\n\t// User listing and details (for admin)\n\tgetUsers(options?: {\n\t\tsearch?: string;\n\t\trole?: number;\n\t\tcursor?: string;\n\t\tlimit?: number;\n\t}): Promise<{ items: UserListItem[]; nextCursor?: string }>;\n\tgetUserWithDetails(id: string): Promise<UserWithDetails | null>;\n\tcountAdmins(): Promise<number>;\n\n\t// Credentials\n\tgetCredentialById(id: string): Promise<Credential | null>;\n\tgetCredentialsByUserId(userId: string): Promise<Credential[]>;\n\tcreateCredential(credential: NewCredential): Promise<Credential>;\n\tupdateCredentialCounter(id: string, counter: number): Promise<void>;\n\tupdateCredentialName(id: string, name: string | null): Promise<void>;\n\tdeleteCredential(id: string): Promise<void>;\n\tcountCredentialsByUserId(userId: string): Promise<number>;\n\n\t// Auth Tokens\n\tcreateToken(token: NewAuthToken): Promise<void>;\n\tgetToken(hash: string, type: TokenType): Promise<AuthToken | null>;\n\tdeleteToken(hash: string): Promise<void>;\n\tdeleteExpiredTokens(): Promise<void>;\n\n\t// OAuth Accounts\n\tgetOAuthAccount(provider: string, providerAccountId: string): Promise<OAuthAccount | null>;\n\tgetOAuthAccountsByUserId(userId: string): Promise<OAuthAccount[]>;\n\tcreateOAuthAccount(account: NewOAuthAccount): Promise<OAuthAccount>;\n\tdeleteOAuthAccount(provider: string, providerAccountId: string): Promise<void>;\n\n\t// Allowed Domains\n\tgetAllowedDomain(domain: string): Promise<AllowedDomain | null>;\n\tgetAllowedDomains(): Promise<AllowedDomain[]>;\n\tcreateAllowedDomain(domain: string, defaultRole: RoleLevel): Promise<AllowedDomain>;\n\tupdateAllowedDomain(domain: string, enabled: boolean, defaultRole?: RoleLevel): Promise<void>;\n\tdeleteAllowedDomain(domain: string): Promise<void>;\n}\n\n// ============================================================================\n// Email Adapter Interface\n// ============================================================================\n\nexport interface EmailMessage {\n\tto: string;\n\tsubject: string;\n\ttext: string;\n\thtml?: string;\n}\n\nexport interface EmailAdapter {\n\tsend(message: EmailMessage): Promise<void>;\n}\n\n// ============================================================================\n// Auth Errors\n// ============================================================================\n\nexport class AuthError extends Error {\n\tconstructor(\n\t\tpublic code: AuthErrorCode,\n\t\tmessage?: string,\n\t) {\n\t\tsuper(message ?? code);\n\t\tthis.name = \"AuthError\";\n\t}\n}\n\nexport type AuthErrorCode =\n\t| \"invalid_credentials\"\n\t| \"invalid_token\"\n\t| \"token_expired\"\n\t| \"user_not_found\"\n\t| \"user_exists\"\n\t| \"credential_exists\"\n\t| \"max_credentials\"\n\t| \"email_not_verified\"\n\t| \"signup_not_allowed\"\n\t| \"domain_not_allowed\"\n\t| \"forbidden\"\n\t| \"unauthorized\"\n\t| \"rate_limited\"\n\t| \"invalid_request\"\n\t| \"internal_error\";\n"],"mappings":";;;;AAQA,MAAa,OAAO;CACnB,YAAY;CACZ,aAAa;CACb,QAAQ;CACR,QAAQ;CACR,OAAO;CACP;AAKD,SAAgB,cAAc,OAAqC;CAClE,MAAM,QAAQ,OAAO,QAAQ,KAAK,CAAC,MAAM,GAAG,OAAO,MAAM,MAAM;AAC/D,KAAI,CAAC,MAAO,QAAO;CACnB,MAAM,OAAO,MAAM;AACnB,KAAI,WAAW,KAAK,CAAE,QAAO;;AAI9B,SAAS,WAAW,OAAkC;AACrD,QAAO,SAAS;;AAGjB,MAAM,iBAAiB,IAAI,IAAuB,OAAO,OAAO,KAAK,CAAC,KAAK,MAAM,CAAC,GAAG,EAAE,CAAC,CAAC;AAEzF,SAAgB,YAAY,OAA0B;CACrD,MAAM,QAAQ,eAAe,IAAI,MAAM;AACvC,KAAI,UAAU,OAAW,QAAO;AAChC,OAAM,IAAI,MAAM,uBAAuB,QAAQ;;AAGhD,MAAM,kBAA0D;CAC/D,cAAc;CACd,aAAa;CACb;AAED,SAAgB,aAAa,OAA2B;CACvD,MAAM,KAAK,gBAAgB;AAC3B,KAAI,OAAO,OAAW,QAAO;AAC7B,OAAM,IAAI,MAAM,wBAAwB,QAAQ;;AAGjD,MAAM,iBAAwD;CAC7D,YAAY;CACZ,cAAc;CACd,QAAQ;CACR,UAAU;CACV;AAED,SAAgB,YAAY,OAA0B;CACrD,MAAM,KAAK,eAAe;AAC1B,KAAI,OAAO,OAAW,QAAO;AAC7B,OAAM,IAAI,MAAM,uBAAuB,QAAQ;;AAGhD,SAAgB,YAAY,MAA2B;AACtD,QAAO,KAAK;;AAsQb,IAAa,YAAb,cAA+B,MAAM;CACpC,YACC,AAAO,MACP,SACC;AACD,QAAM,WAAW,KAAK;EAHf;AAIP,OAAK,OAAO"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@emdash-cms/auth",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Passkey-first authentication for EmDash",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.mjs",
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
},
|
|
58
58
|
"repository": {
|
|
59
59
|
"type": "git",
|
|
60
|
-
"url": "git+https://github.com/
|
|
60
|
+
"url": "git+https://github.com/emdash-cms/emdash.git",
|
|
61
61
|
"directory": "packages/auth"
|
|
62
62
|
},
|
|
63
63
|
"author": "Matt Kane",
|
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 };
|
package/src/magic-link/index.ts
CHANGED
|
@@ -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
|
package/src/oauth/consumer.ts
CHANGED
|
@@ -40,7 +40,10 @@ export async function createAuthorizationUrl(
|
|
|
40
40
|
|
|
41
41
|
const provider = getProvider(providerName);
|
|
42
42
|
const state = generateState();
|
|
43
|
-
const redirectUri =
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"types-CiSNpRI9.mjs","names":[],"sources":["../src/types.ts"],"sourcesContent":["/**\n * Core types for @emdashcms/auth\n */\n\n// ============================================================================\n// Roles & Permissions\n// ============================================================================\n\nexport const Role = {\n\tSUBSCRIBER: 10,\n\tCONTRIBUTOR: 20,\n\tAUTHOR: 30,\n\tEDITOR: 40,\n\tADMIN: 50,\n} as const;\n\nexport type RoleLevel = (typeof Role)[keyof typeof Role];\nexport type RoleName = keyof typeof Role;\n\nexport function roleFromLevel(level: number): RoleName | undefined {\n\tconst entry = Object.entries(Role).find(([, v]) => v === level);\n\tif (!entry) return undefined;\n\tconst name = entry[0];\n\tif (isRoleName(name)) return name;\n\treturn undefined;\n}\n\nfunction isRoleName(value: string): value is RoleName {\n\treturn value in Role;\n}\n\nconst ROLE_LEVEL_MAP = new Map<number, RoleLevel>(Object.values(Role).map((v) => [v, v]));\n\nexport function toRoleLevel(value: number): RoleLevel {\n\tconst level = ROLE_LEVEL_MAP.get(value);\n\tif (level !== undefined) return level;\n\tthrow new Error(`Invalid role level: ${value}`);\n}\n\nconst DEVICE_TYPE_MAP: Record<string, DeviceType | undefined> = {\n\tsingleDevice: \"singleDevice\",\n\tmultiDevice: \"multiDevice\",\n};\n\nexport function toDeviceType(value: string): DeviceType {\n\tconst dt = DEVICE_TYPE_MAP[value];\n\tif (dt !== undefined) return dt;\n\tthrow new Error(`Invalid device type: ${value}`);\n}\n\nconst TOKEN_TYPE_MAP: Record<string, TokenType | undefined> = {\n\tmagic_link: \"magic_link\",\n\temail_verify: \"email_verify\",\n\tinvite: \"invite\",\n\trecovery: \"recovery\",\n};\n\nexport function toTokenType(value: string): TokenType {\n\tconst tt = TOKEN_TYPE_MAP[value];\n\tif (tt !== undefined) return tt;\n\tthrow new Error(`Invalid token type: ${value}`);\n}\n\nexport function roleToLevel(name: RoleName): RoleLevel {\n\treturn Role[name];\n}\n\n// ============================================================================\n// User\n// ============================================================================\n\nexport interface User {\n\tid: string;\n\temail: string;\n\tname: string | null;\n\tavatarUrl: string | null;\n\trole: RoleLevel;\n\temailVerified: boolean;\n\tdisabled: boolean;\n\tdata: Record<string, unknown> | null;\n\tcreatedAt: Date;\n\tupdatedAt: Date;\n}\n\nexport interface NewUser {\n\temail: string;\n\tname?: string | null;\n\tavatarUrl?: string | null;\n\trole?: RoleLevel;\n\temailVerified?: boolean;\n\tdata?: Record<string, unknown> | null;\n}\n\nexport interface UpdateUser {\n\temail?: string;\n\tname?: string | null;\n\tavatarUrl?: string | null;\n\trole?: RoleLevel;\n\temailVerified?: boolean;\n\tdisabled?: boolean;\n\tdata?: Record<string, unknown> | null;\n}\n\n// ============================================================================\n// Credentials (Passkeys)\n// ============================================================================\n\nexport type AuthenticatorTransport = \"usb\" | \"nfc\" | \"ble\" | \"internal\" | \"hybrid\";\nexport type DeviceType = \"singleDevice\" | \"multiDevice\";\n\nexport interface Credential {\n\tid: string; // Base64url credential ID\n\tuserId: string;\n\tpublicKey: Uint8Array; // COSE public key\n\tcounter: number;\n\tdeviceType: DeviceType;\n\tbackedUp: boolean;\n\ttransports: AuthenticatorTransport[];\n\tname: string | null;\n\tcreatedAt: Date;\n\tlastUsedAt: Date;\n}\n\nexport interface NewCredential {\n\tid: string;\n\tuserId: string;\n\tpublicKey: Uint8Array;\n\tcounter: number;\n\tdeviceType: DeviceType;\n\tbackedUp: boolean;\n\ttransports: AuthenticatorTransport[];\n\tname?: string | null;\n}\n\n// ============================================================================\n// Sessions\n// ============================================================================\n\nexport interface Session {\n\tid: string;\n\tuserId: string;\n\texpiresAt: Date;\n\tipAddress: string | null;\n\tuserAgent: string | null;\n\tcreatedAt: Date;\n}\n\nexport interface SessionData {\n\tuserId: string;\n\texpiresAt: number; // Unix timestamp\n}\n\n// ============================================================================\n// Auth Tokens (magic links, invites, etc.)\n// ============================================================================\n\nexport type TokenType = \"magic_link\" | \"email_verify\" | \"invite\" | \"recovery\";\n\nexport interface AuthToken {\n\thash: string; // SHA-256 hash of the raw token\n\tuserId: string | null; // null for pre-user tokens (invite/signup)\n\temail: string | null; // For pre-user tokens\n\ttype: TokenType;\n\trole: RoleLevel | null; // For invites\n\tinvitedBy: string | null;\n\texpiresAt: Date;\n\tcreatedAt: Date;\n}\n\nexport interface NewAuthToken {\n\thash: string;\n\tuserId?: string | null;\n\temail?: string | null;\n\ttype: TokenType;\n\trole?: RoleLevel | null;\n\tinvitedBy?: string | null;\n\texpiresAt: Date;\n}\n\n// ============================================================================\n// OAuth Accounts\n// ============================================================================\n\nexport interface OAuthAccount {\n\tprovider: string;\n\tproviderAccountId: string;\n\tuserId: string;\n\tcreatedAt: Date;\n}\n\nexport interface NewOAuthAccount {\n\tprovider: string;\n\tproviderAccountId: string;\n\tuserId: string;\n}\n\n// ============================================================================\n// OAuth Connections (SSO config)\n// ============================================================================\n\nexport interface OAuthConnection {\n\tid: string;\n\tname: string;\n\tprovider: \"oidc\" | \"github\" | \"google\";\n\tclientId: string;\n\tclientSecretEnc: string; // Encrypted\n\tissuerUrl: string | null;\n\tconfig: Record<string, unknown> | null;\n\tenabled: boolean;\n\tcreatedAt: Date;\n}\n\n// ============================================================================\n// OAuth Clients (when EmDash is provider)\n// ============================================================================\n\nexport interface OAuthClient {\n\tid: string;\n\tname: string;\n\tsecretHash: string;\n\tredirectUris: string[];\n\tscopes: string[];\n\tcreatedAt: Date;\n}\n\n// ============================================================================\n// Allowed Domains (self-signup)\n// ============================================================================\n\nexport interface AllowedDomain {\n\tdomain: string;\n\tdefaultRole: RoleLevel;\n\tenabled: boolean;\n\tcreatedAt: Date;\n}\n\n// ============================================================================\n// User Listing Types (for admin UI)\n// ============================================================================\n\n/** Extended user with list view computed fields */\nexport interface UserListItem extends User {\n\tlastLogin: Date | null;\n\tcredentialCount: number;\n\toauthProviders: string[];\n}\n\n/** User with full details including related data */\nexport interface UserWithDetails {\n\tuser: User;\n\tcredentials: Credential[];\n\toauthAccounts: OAuthAccount[];\n\tlastLogin: Date | null;\n}\n\n// ============================================================================\n// Auth Adapter Interface\n// ============================================================================\n\nexport interface AuthAdapter {\n\t// Users\n\tgetUserById(id: string): Promise<User | null>;\n\tgetUserByEmail(email: string): Promise<User | null>;\n\tcreateUser(user: NewUser): Promise<User>;\n\tupdateUser(id: string, data: UpdateUser): Promise<void>;\n\tdeleteUser(id: string): Promise<void>;\n\tcountUsers(): Promise<number>;\n\n\t// User listing and details (for admin)\n\tgetUsers(options?: {\n\t\tsearch?: string;\n\t\trole?: number;\n\t\tcursor?: string;\n\t\tlimit?: number;\n\t}): Promise<{ items: UserListItem[]; nextCursor?: string }>;\n\tgetUserWithDetails(id: string): Promise<UserWithDetails | null>;\n\tcountAdmins(): Promise<number>;\n\n\t// Credentials\n\tgetCredentialById(id: string): Promise<Credential | null>;\n\tgetCredentialsByUserId(userId: string): Promise<Credential[]>;\n\tcreateCredential(credential: NewCredential): Promise<Credential>;\n\tupdateCredentialCounter(id: string, counter: number): Promise<void>;\n\tupdateCredentialName(id: string, name: string | null): Promise<void>;\n\tdeleteCredential(id: string): Promise<void>;\n\tcountCredentialsByUserId(userId: string): Promise<number>;\n\n\t// Auth Tokens\n\tcreateToken(token: NewAuthToken): Promise<void>;\n\tgetToken(hash: string, type: TokenType): Promise<AuthToken | null>;\n\tdeleteToken(hash: string): Promise<void>;\n\tdeleteExpiredTokens(): Promise<void>;\n\n\t// OAuth Accounts\n\tgetOAuthAccount(provider: string, providerAccountId: string): Promise<OAuthAccount | null>;\n\tgetOAuthAccountsByUserId(userId: string): Promise<OAuthAccount[]>;\n\tcreateOAuthAccount(account: NewOAuthAccount): Promise<OAuthAccount>;\n\tdeleteOAuthAccount(provider: string, providerAccountId: string): Promise<void>;\n\n\t// Allowed Domains\n\tgetAllowedDomain(domain: string): Promise<AllowedDomain | null>;\n\tgetAllowedDomains(): Promise<AllowedDomain[]>;\n\tcreateAllowedDomain(domain: string, defaultRole: RoleLevel): Promise<AllowedDomain>;\n\tupdateAllowedDomain(domain: string, enabled: boolean, defaultRole?: RoleLevel): Promise<void>;\n\tdeleteAllowedDomain(domain: string): Promise<void>;\n}\n\n// ============================================================================\n// Email Adapter Interface\n// ============================================================================\n\nexport interface EmailMessage {\n\tto: string;\n\tsubject: string;\n\ttext: string;\n\thtml?: string;\n}\n\nexport interface EmailAdapter {\n\tsend(message: EmailMessage): Promise<void>;\n}\n\n// ============================================================================\n// Auth Errors\n// ============================================================================\n\nexport class AuthError extends Error {\n\tconstructor(\n\t\tpublic code: AuthErrorCode,\n\t\tmessage?: string,\n\t) {\n\t\tsuper(message ?? code);\n\t\tthis.name = \"AuthError\";\n\t}\n}\n\nexport type AuthErrorCode =\n\t| \"invalid_credentials\"\n\t| \"invalid_token\"\n\t| \"token_expired\"\n\t| \"user_not_found\"\n\t| \"user_exists\"\n\t| \"credential_exists\"\n\t| \"max_credentials\"\n\t| \"email_not_verified\"\n\t| \"signup_not_allowed\"\n\t| \"domain_not_allowed\"\n\t| \"forbidden\"\n\t| \"unauthorized\"\n\t| \"rate_limited\"\n\t| \"invalid_request\"\n\t| \"internal_error\";\n"],"mappings":";;;;AAQA,MAAa,OAAO;CACnB,YAAY;CACZ,aAAa;CACb,QAAQ;CACR,QAAQ;CACR,OAAO;CACP;AAKD,SAAgB,cAAc,OAAqC;CAClE,MAAM,QAAQ,OAAO,QAAQ,KAAK,CAAC,MAAM,GAAG,OAAO,MAAM,MAAM;AAC/D,KAAI,CAAC,MAAO,QAAO;CACnB,MAAM,OAAO,MAAM;AACnB,KAAI,WAAW,KAAK,CAAE,QAAO;;AAI9B,SAAS,WAAW,OAAkC;AACrD,QAAO,SAAS;;AAGjB,MAAM,iBAAiB,IAAI,IAAuB,OAAO,OAAO,KAAK,CAAC,KAAK,MAAM,CAAC,GAAG,EAAE,CAAC,CAAC;AAEzF,SAAgB,YAAY,OAA0B;CACrD,MAAM,QAAQ,eAAe,IAAI,MAAM;AACvC,KAAI,UAAU,OAAW,QAAO;AAChC,OAAM,IAAI,MAAM,uBAAuB,QAAQ;;AAGhD,MAAM,kBAA0D;CAC/D,cAAc;CACd,aAAa;CACb;AAED,SAAgB,aAAa,OAA2B;CACvD,MAAM,KAAK,gBAAgB;AAC3B,KAAI,OAAO,OAAW,QAAO;AAC7B,OAAM,IAAI,MAAM,wBAAwB,QAAQ;;AAGjD,MAAM,iBAAwD;CAC7D,YAAY;CACZ,cAAc;CACd,QAAQ;CACR,UAAU;CACV;AAED,SAAgB,YAAY,OAA0B;CACrD,MAAM,KAAK,eAAe;AAC1B,KAAI,OAAO,OAAW,QAAO;AAC7B,OAAM,IAAI,MAAM,uBAAuB,QAAQ;;AAGhD,SAAgB,YAAY,MAA2B;AACtD,QAAO,KAAK;;AAsQb,IAAa,YAAb,cAA+B,MAAM;CACpC,YACC,AAAO,MACP,SACC;AACD,QAAM,WAAW,KAAK;EAHf;AAIP,OAAK,OAAO"}
|