@emdash-cms/auth 0.0.1 → 0.1.0
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/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/index.d.mts +2 -2
- package/dist/index.mjs +2 -2
- package/dist/index.mjs.map +1 -1
- package/dist/passkey/index.d.mts +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/dist/types-CiSNpRI9.mjs.map +0 -1
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":";;;;AA8FA,SAAgB,oBAA0C,IAA4B;CAGrF,MAAM,MAAM;AAEZ,QAAO;EAKN,MAAM,YAAY,IAAkC;GACnD,MAAM,MAAM,MAAM,IAAI,WAAW,QAAQ,CAAC,WAAW,CAAC,MAAM,MAAM,KAAK,GAAG,CAAC,kBAAkB;AAE7F,UAAO,MAAM,UAAU,IAAI,GAAG;;EAG/B,MAAM,eAAe,OAAqC;GACzD,MAAM,MAAM,MAAM,IAChB,WAAW,QAAQ,CACnB,WAAW,CACX,MAAM,SAAS,KAAK,MAAM,aAAa,CAAC,CACxC,kBAAkB;AAEpB,UAAO,MAAM,UAAU,IAAI,GAAG;;EAG/B,MAAM,WAAW,MAA8B;GAC9C,MAAM,uBAAM,IAAI,MAAM,EAAC,aAAa;GACpC,MAAM,KAAK,MAAM;GAEjB,MAAM,MAA6B;IAClC;IACA,OAAO,KAAK,MAAM,aAAa;IAC/B,MAAM,KAAK,QAAQ;IACnB,YAAY,KAAK,aAAa;IAC9B,MAAM,KAAK,QAAQ,KAAK;IACxB,gBAAgB,KAAK,gBAAgB,IAAI;IACzC,UAAU;IACV,MAAM,KAAK,OAAO,KAAK,UAAU,KAAK,KAAK,GAAG;IAC9C,YAAY;IACZ,YAAY;IACZ;AAED,SAAM,IAAI,WAAW,QAAQ,CAAC,OAAO,IAAI,CAAC,SAAS;AAEnD,UAAO;IACN;IACA,OAAO,IAAI;IACX,MAAM,KAAK,QAAQ;IACnB,WAAW,KAAK,aAAa;IAC7B,MAAM,YAAY,IAAI,KAAK;IAC3B,eAAe,IAAI,mBAAmB;IACtC,UAAU;IACV,MAAM,KAAK,QAAQ;IACnB,WAAW,IAAI,KAAK,IAAI;IACxB,WAAW,IAAI,KAAK,IAAI;IACxB;;EAGF,MAAM,WAAW,IAAY,MAAiC;GAC7D,MAAM,SAAgC,EACrC,6BAAY,IAAI,MAAM,EAAC,aAAa,EACpC;AAED,OAAI,KAAK,UAAU,OAAW,QAAO,QAAQ,KAAK,MAAM,aAAa;AACrE,OAAI,KAAK,SAAS,OAAW,QAAO,OAAO,KAAK;AAChD,OAAI,KAAK,cAAc,OAAW,QAAO,aAAa,KAAK;AAC3D,OAAI,KAAK,SAAS,OAAW,QAAO,OAAO,KAAK;AAChD,OAAI,KAAK,kBAAkB,OAAW,QAAO,iBAAiB,KAAK,gBAAgB,IAAI;AACvF,OAAI,KAAK,aAAa,OAAW,QAAO,WAAW,KAAK,WAAW,IAAI;AACvE,OAAI,KAAK,SAAS,OAAW,QAAO,OAAO,KAAK,OAAO,KAAK,UAAU,KAAK,KAAK,GAAG;AAEnF,SAAM,IAAI,YAAY,QAAQ,CAAC,IAAI,OAAO,CAAC,MAAM,MAAM,KAAK,GAAG,CAAC,SAAS;;EAG1E,MAAM,WAAW,IAA2B;AAC3C,SAAM,IAAI,WAAW,QAAQ,CAAC,MAAM,MAAM,KAAK,GAAG,CAAC,SAAS;;EAG7D,MAAM,aAA8B;AAMnC,WALe,MAAM,IACnB,WAAW,QAAQ,CACnB,QAAQ,OAAO,GAAG,GAAG,UAAkB,CAAC,GAAG,QAAQ,CAAC,CACpD,yBAAyB,EAEb;;EAGf,MAAM,SAAS,SAcZ;GACF,MAAM,QAAQ,KAAK,IAAI,SAAS,SAAS,IAAI,IAAI;GAEjD,IAAI,QAAQ,IACV,WAAW,QAAQ,CACnB,SAAS,eAAe,YAAY,sBAAsB,CAC1D,UAAU,QAAQ,CAClB,QAAQ,OAAO,CACf,GAAG,GAAG,MAAc,iBAAiB,CAAC,GAAG,mBAAmB,EAC5D,GAAG,GAAG,IAAI,2BAA2B,CAAC,GAAG,aAAa,CACtD,CAAC,CACD,QAAQ,WAAW,CACnB,QAAQ,oBAAoB,OAAO,CACnC,MAAM,QAAQ,EAAE;AAGlB,OAAI,SAAS,QAAQ;IACpB,MAAM,gBAAgB,IAAI,QAAQ,OAAO;AACzC,YAAQ,MAAM,OAAO,OACpB,GAAG,GAAG,CACL,GAAG,eAAe,QAAQ,cAAc,EACxC,GAAG,cAAc,QAAQ,cAAc,CACvC,CAAC,CACF;;AAGF,OAAI,SAAS,SAAS,OACrB,SAAQ,MAAM,MAAM,cAAc,KAAK,QAAQ,KAAK;AAGrD,OAAI,SAAS,QAAQ;IAEpB,MAAM,aAAa,MAAM,IACvB,WAAW,QAAQ,CACnB,OAAO,aAAa,CACpB,MAAM,MAAM,KAAK,QAAQ,OAAO,CAChC,kBAAkB;AAEpB,QAAI,WACH,SAAQ,MAAM,MAAM,oBAAoB,KAAK,WAAW,WAAW;;GAIrE,MAAM,OAAO,MAAM,MAAM,SAAS;GAGlC,MAAM,UAAU,KAAK,MAAM,GAAG,MAAM,CAAC,KAAK,MAAM,EAAE,GAAG;GACrD,MAAM,gBACL,QAAQ,SAAS,IACd,MAAM,IACL,WAAW,iBAAiB,CAC5B,OAAO,CAAC,WAAW,WAAW,CAAC,CAC/B,MAAM,WAAW,MAAM,QAAQ,CAC/B,SAAS,GACV,EAAE;GAGN,MAAM,8BAAc,IAAI,KAAuB;AAC/C,QAAK,MAAM,WAAW,eAAe;IACpC,MAAM,YAAY,YAAY,IAAI,QAAQ,QAAQ,IAAI,EAAE;AACxD,cAAU,KAAK,QAAQ,SAAS;AAChC,gBAAY,IAAI,QAAQ,SAAS,UAAU;;GAG5C,MAAM,UAAU,KAAK,SAAS;GAC9B,MAAM,QAAQ,KAAK,MAAM,GAAG,MAAM,CAAC,KAAK,SAAS;IAChD,IAAI,IAAI;IACR,OAAO,IAAI;IACX,MAAM,IAAI;IACV,WAAW,IAAI;IACf,MAAM,YAAY,IAAI,KAAK;IAC3B,eAAe,IAAI,mBAAmB;IACtC,UAAU,IAAI,aAAa;IAC3B,MAAM,IAAI,OAAO,KAAK,MAAM,IAAI,KAAK,GAAG;IACxC,WAAW,IAAI,KAAK,IAAI,WAAW;IACnC,WAAW,IAAI,KAAK,IAAI,WAAW;IACnC,WAAW,IAAI,aAAa,IAAI,KAAK,IAAI,WAAW,GAAG;IACvD,iBAAiB,IAAI,oBAAoB;IACzC,gBAAgB,YAAY,IAAI,IAAI,GAAG,IAAI,EAAE;IAC7C,EAAE;AAEH,UAAO;IACN;IACA,YAAY,UAAU,MAAM,GAAG,GAAG,EAAE,KAAK;IACzC;;EAGF,MAAM,mBAAmB,IAKf;GACT,MAAM,OAAO,MAAM,IACjB,WAAW,QAAQ,CACnB,WAAW,CACX,MAAM,MAAM,KAAK,GAAG,CACpB,kBAAkB;AAEpB,OAAI,CAAC,KAAM,QAAO;GAElB,MAAM,CAAC,aAAa,iBAAiB,MAAM,QAAQ,IAAI,CACtD,IACE,WAAW,cAAc,CACzB,WAAW,CACX,MAAM,WAAW,KAAK,GAAG,CACzB,QAAQ,cAAc,OAAO,CAC7B,SAAS,EACX,IAAI,WAAW,iBAAiB,CAAC,WAAW,CAAC,MAAM,WAAW,KAAK,GAAG,CAAC,SAAS,CAChF,CAAC;GAGF,MAAM,YAAY,YAAY,QAAqB,QAAQ,SAAS;IACnE,MAAM,WAAW,IAAI,KAAK,KAAK,aAAa;AAC5C,WAAO,CAAC,UAAU,WAAW,SAAS,WAAW;MAC/C,KAAK;AAER,UAAO;IACN,MAAM,UAAU,KAAK;IACrB,aAAa,YAAY,IAAI,gBAAgB;IAC7C,eAAe,cAAc,IAAI,kBAAkB;IACnD;IACA;;EAGF,MAAM,cAA+B;AAQpC,WAPe,MAAM,IACnB,WAAW,QAAQ,CACnB,QAAQ,OAAO,GAAG,GAAG,UAAkB,CAAC,GAAG,QAAQ,CAAC,CACpD,MAAM,QAAQ,KAAK,KAAK,MAAM,CAC9B,MAAM,YAAY,KAAK,EAAE,CACzB,yBAAyB,EAEb;;EAOf,MAAM,kBAAkB,IAAwC;GAC/D,MAAM,MAAM,MAAM,IAChB,WAAW,cAAc,CACzB,WAAW,CACX,MAAM,MAAM,KAAK,GAAG,CACpB,kBAAkB;AAEpB,UAAO,MAAM,gBAAgB,IAAI,GAAG;;EAGrC,MAAM,uBAAuB,QAAuC;AAOnE,WANa,MAAM,IACjB,WAAW,cAAc,CACzB,WAAW,CACX,MAAM,WAAW,KAAK,OAAO,CAC7B,SAAS,EAEC,IAAI,gBAAgB;;EAGjC,MAAM,iBAAiB,YAAgD;GACtE,MAAM,uBAAM,IAAI,MAAM,EAAC,aAAa;GAEpC,MAAM,MAAmC;IACxC,IAAI,WAAW;IACf,SAAS,WAAW;IACpB,YAAY,WAAW;IACvB,SAAS,WAAW;IACpB,aAAa,WAAW;IACxB,WAAW,WAAW,WAAW,IAAI;IACrC,YAAY,WAAW,WAAW,SAAS,IAAI,KAAK,UAAU,WAAW,WAAW,GAAG;IACvF,MAAM,WAAW,QAAQ;IACzB,YAAY;IACZ,cAAc;IACd;AAED,SAAM,IAAI,WAAW,cAAc,CAAC,OAAO,IAAI,CAAC,SAAS;AAEzD,UAAO;IACN,IAAI,WAAW;IACf,QAAQ,WAAW;IACnB,WAAW,WAAW;IACtB,SAAS,WAAW;IACpB,YAAY,WAAW;IACvB,UAAU,WAAW;IACrB,YAAY,WAAW;IACvB,MAAM,WAAW,QAAQ;IACzB,WAAW,IAAI,KAAK,IAAI;IACxB,YAAY,IAAI,KAAK,IAAI;IACzB;;EAGF,MAAM,wBAAwB,IAAY,SAAgC;AACzE,SAAM,IACJ,YAAY,cAAc,CAC1B,IAAI;IACJ;IACA,+BAAc,IAAI,MAAM,EAAC,aAAa;IACtC,CAAC,CACD,MAAM,MAAM,KAAK,GAAG,CACpB,SAAS;;EAGZ,MAAM,qBAAqB,IAAY,MAAoC;AAC1E,SAAM,IAAI,YAAY,cAAc,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,MAAM,MAAM,KAAK,GAAG,CAAC,SAAS;;EAGlF,MAAM,iBAAiB,IAA2B;AACjD,SAAM,IAAI,WAAW,cAAc,CAAC,MAAM,MAAM,KAAK,GAAG,CAAC,SAAS;;EAGnE,MAAM,yBAAyB,QAAiC;AAO/D,WANe,MAAM,IACnB,WAAW,cAAc,CACzB,QAAQ,OAAO,GAAG,GAAG,UAAkB,CAAC,GAAG,QAAQ,CAAC,CACpD,MAAM,WAAW,KAAK,OAAO,CAC7B,yBAAyB,EAEb;;EAOf,MAAM,YAAY,OAAoC;GACrD,MAAM,MAAkC;IACvC,MAAM,MAAM;IACZ,SAAS,MAAM,UAAU;IACzB,OAAO,MAAM,SAAS;IACtB,MAAM,MAAM;IACZ,MAAM,MAAM,QAAQ;IACpB,YAAY,MAAM,aAAa;IAC/B,YAAY,MAAM,UAAU,aAAa;IACzC,6BAAY,IAAI,MAAM,EAAC,aAAa;IACpC;AAED,SAAM,IAAI,WAAW,cAAc,CAAC,OAAO,IAAI,CAAC,SAAS;;EAG1D,MAAM,SAAS,MAAc,MAA4C;GACxE,MAAM,MAAM,MAAM,IAChB,WAAW,cAAc,CACzB,WAAW,CACX,MAAM,QAAQ,KAAK,KAAK,CACxB,MAAM,QAAQ,KAAK,KAAK,CACxB,kBAAkB;AAEpB,UAAO,MAAM,eAAe,IAAI,GAAG;;EAGpC,MAAM,YAAY,MAA6B;AAC9C,SAAM,IAAI,WAAW,cAAc,CAAC,MAAM,QAAQ,KAAK,KAAK,CAAC,SAAS;;EAGvE,MAAM,sBAAqC;AAC1C,SAAM,IACJ,WAAW,cAAc,CACzB,MAAM,cAAc,sBAAK,IAAI,MAAM,EAAC,aAAa,CAAC,CAClD,SAAS;;EAOZ,MAAM,gBACL,UACA,mBAC+B;GAC/B,MAAM,MAAM,MAAM,IAChB,WAAW,iBAAiB,CAC5B,WAAW,CACX,MAAM,YAAY,KAAK,SAAS,CAChC,MAAM,uBAAuB,KAAK,kBAAkB,CACpD,kBAAkB;AAEpB,UAAO,MAAM,kBAAkB,IAAI,GAAG;;EAGvC,MAAM,yBAAyB,QAAyC;AAOvE,WANa,MAAM,IACjB,WAAW,iBAAiB,CAC5B,WAAW,CACX,MAAM,WAAW,KAAK,OAAO,CAC7B,SAAS,EAEC,IAAI,kBAAkB;;EAGnC,MAAM,mBAAmB,SAAiD;GACzE,MAAM,uBAAM,IAAI,MAAM,EAAC,aAAa;GAEpC,MAAM,MAAqC;IAC1C,UAAU,QAAQ;IAClB,qBAAqB,QAAQ;IAC7B,SAAS,QAAQ;IACjB,YAAY;IACZ;AAED,SAAM,IAAI,WAAW,iBAAiB,CAAC,OAAO,IAAI,CAAC,SAAS;AAE5D,UAAO;IACN,UAAU,QAAQ;IAClB,mBAAmB,QAAQ;IAC3B,QAAQ,QAAQ;IAChB,WAAW,IAAI,KAAK,IAAI;IACxB;;EAGF,MAAM,mBAAmB,UAAkB,mBAA0C;AACpF,SAAM,IACJ,WAAW,iBAAiB,CAC5B,MAAM,YAAY,KAAK,SAAS,CAChC,MAAM,uBAAuB,KAAK,kBAAkB,CACpD,SAAS;;EAOZ,MAAM,iBAAiB,QAA+C;GACrE,MAAM,MAAM,MAAM,IAChB,WAAW,kBAAkB,CAC7B,WAAW,CACX,MAAM,UAAU,KAAK,OAAO,aAAa,CAAC,CAC1C,kBAAkB;AAEpB,UAAO,MAAM,mBAAmB,IAAI,GAAG;;EAGxC,MAAM,oBAA8C;AAGnD,WAFa,MAAM,IAAI,WAAW,kBAAkB,CAAC,WAAW,CAAC,SAAS,EAE9D,IAAI,mBAAmB;;EAGpC,MAAM,oBAAoB,QAAgB,aAAgD;GACzF,MAAM,uBAAM,IAAI,MAAM,EAAC,aAAa;GAEpC,MAAM,MAAsC;IAC3C,QAAQ,OAAO,aAAa;IAC5B,cAAc;IACd,SAAS;IACT,YAAY;IACZ;AAED,SAAM,IAAI,WAAW,kBAAkB,CAAC,OAAO,IAAI,CAAC,SAAS;AAE7D,UAAO;IACN,QAAQ,IAAI;IACZ;IACA,SAAS;IACT,WAAW,IAAI,KAAK,IAAI;IACxB;;EAGF,MAAM,oBACL,QACA,SACA,aACgB;GAChB,MAAM,SAAyC,EAC9C,SAAS,UAAU,IAAI,GACvB;AAED,OAAI,gBAAgB,OACnB,QAAO,eAAe;AAGvB,SAAM,IACJ,YAAY,kBAAkB,CAC9B,IAAI,OAAO,CACX,MAAM,UAAU,KAAK,OAAO,aAAa,CAAC,CAC1C,SAAS;;EAGZ,MAAM,oBAAoB,QAA+B;AACxD,SAAM,IAAI,WAAW,kBAAkB,CAAC,MAAM,UAAU,KAAK,OAAO,aAAa,CAAC,CAAC,SAAS;;EAE7F;;AAOF,SAAS,UAAU,KAAkC;AACpD,QAAO;EACN,IAAI,IAAI;EACR,OAAO,IAAI;EACX,MAAM,IAAI;EACV,WAAW,IAAI;EACf,MAAM,YAAY,IAAI,KAAK;EAC3B,eAAe,IAAI,mBAAmB;EACtC,UAAU,IAAI,aAAa;EAC3B,MAAM,IAAI,OAAO,KAAK,MAAM,IAAI,KAAK,GAAG;EACxC,WAAW,IAAI,KAAK,IAAI,WAAW;EACnC,WAAW,IAAI,KAAK,IAAI,WAAW;EACnC;;AAGF,SAAS,gBAAgB,KAA8C;AACtE,QAAO;EACN,IAAI,IAAI;EACR,QAAQ,IAAI;EACZ,WAAW,IAAI;EACf,SAAS,IAAI;EACb,YAAY,aAAa,IAAI,YAAY;EACzC,UAAU,IAAI,cAAc;EAC5B,YAAY,IAAI,aAAa,KAAK,MAAM,IAAI,WAAW,GAAG,EAAE;EAC5D,MAAM,IAAI;EACV,WAAW,IAAI,KAAK,IAAI,WAAW;EACnC,YAAY,IAAI,KAAK,IAAI,aAAa;EACtC;;AAGF,SAAS,eAAe,KAA4C;AACnE,QAAO;EACN,MAAM,IAAI;EACV,QAAQ,IAAI;EACZ,OAAO,IAAI;EACX,MAAM,YAAY,IAAI,KAAK;EAC3B,MAAM,IAAI,QAAQ,OAAO,YAAY,IAAI,KAAK,GAAG;EACjD,WAAW,IAAI;EACf,WAAW,IAAI,KAAK,IAAI,WAAW;EACnC,WAAW,IAAI,KAAK,IAAI,WAAW;EACnC;;AAGF,SAAS,kBAAkB,KAAkD;AAC5E,QAAO;EACN,UAAU,IAAI;EACd,mBAAmB,IAAI;EACvB,QAAQ,IAAI;EACZ,WAAW,IAAI,KAAK,IAAI,WAAW;EACnC;;AAGF,SAAS,mBAAmB,KAAoD;AAC/E,QAAO;EACN,QAAQ,IAAI;EACZ,aAAa,YAAY,IAAI,aAAa;EAC1C,SAAS,IAAI,YAAY;EACzB,WAAW,IAAI,KAAK,IAAI,WAAW;EACnC;;AAOF,MAAa,kBAAkB"}
|
|
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"}
|
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.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
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-
|
|
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
2
|
import { _ as hasScope, a as registerPasskey, b as secureCompare, c as VALID_SCOPES, d as encrypt, f as generateAuthSecret, g as generateTokenWithHash, h as generateToken, i as generateRegistrationOptions, l as computeS256Challenge, m as generateSessionId, n as generateAuthenticationOptions, o as verifyRegistrationResponse, p as generatePrefixedToken, r as verifyAuthenticationResponse, s as TOKEN_PREFIXES, t as authenticateWithPasskey, u as decrypt, v as hashPrefixedToken, x as validateScopes, y as hashToken } from "./authenticate-j5GayLXB.mjs";
|
|
3
3
|
import "./passkey/index.mjs";
|
|
4
4
|
import { fetchGitHubEmail, github } from "./oauth/providers/github.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;
|
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(\"/api/auth/invite/accept\", config.baseUrl);\n\turl.searchParams.set(\"token\", token);\n\n\treturn { url: url.toString(), email };\n}\n\n/**\n * Build the invite email message.\n */\nfunction buildInviteEmail(inviteUrl: string, email: string, siteName: string): EmailMessage {\n\tconst safeName = escapeHtml(siteName);\n\treturn {\n\t\tto: email,\n\t\tsubject: `You've been invited to ${siteName}`,\n\t\ttext: `You've been invited to join ${siteName}.\\n\\nClick this link to create your account:\\n${inviteUrl}\\n\\nThis link expires in 7 days.`,\n\t\thtml: `\n<!DOCTYPE html>\n<html>\n<head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n</head>\n<body style=\"font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.5; color: #333; max-width: 600px; margin: 0 auto; padding: 20px;\">\n <h1 style=\"font-size: 24px; margin-bottom: 20px;\">You've been invited to ${safeName}</h1>\n <p>Click the button below to create your account:</p>\n <p style=\"margin: 30px 0;\">\n <a href=\"${inviteUrl}\" style=\"background-color: #0066cc; color: white; padding: 12px 24px; text-decoration: none; border-radius: 6px; display: inline-block;\">Accept Invite</a>\n </p>\n <p style=\"color: #666; font-size: 14px;\">This link expires in 7 days.</p>\n</body>\n</html>`,\n\t};\n}\n\n/**\n * Create and send an invite to a new user.\n *\n * When `config.email` is provided, sends the invite email.\n * When omitted, creates the token and returns the invite URL\n * without sending (for the copy-link fallback).\n */\nexport async function createInvite(\n\tconfig: InviteConfig,\n\tadapter: AuthAdapter,\n\temail: string,\n\trole: RoleLevel,\n\tinvitedBy: string,\n): Promise<InviteTokenResult> {\n\tconst result = await createInviteToken(config, adapter, email, role, invitedBy);\n\n\t// Send email if a sender is configured\n\tif (config.email) {\n\t\tconst message = buildInviteEmail(result.url, email, config.siteName);\n\t\tawait config.email(message);\n\t}\n\n\treturn result;\n}\n\n/**\n * Validate an invite token and return the invite data\n */\nexport async function validateInvite(\n\tadapter: AuthAdapter,\n\ttoken: string,\n): Promise<{ email: string; role: RoleLevel }> {\n\tconst hash = hashToken(token);\n\n\tconst authToken = await adapter.getToken(hash, \"invite\");\n\tif (!authToken) {\n\t\tthrow new InviteError(\"invalid_token\", \"Invalid or expired invite link\");\n\t}\n\n\tif (authToken.expiresAt < new Date()) {\n\t\tawait adapter.deleteToken(hash);\n\t\tthrow new InviteError(\"token_expired\", \"This invite has expired\");\n\t}\n\n\tif (!authToken.email || authToken.role === null) {\n\t\tthrow new InviteError(\"invalid_token\", \"Invalid invite data\");\n\t}\n\n\treturn {\n\t\temail: authToken.email,\n\t\trole: authToken.role,\n\t};\n}\n\n/**\n * Complete the invite process (after passkey registration)\n */\nexport async function completeInvite(\n\tadapter: AuthAdapter,\n\ttoken: string,\n\tuserData: {\n\t\tname?: string;\n\t\tavatarUrl?: string;\n\t},\n): Promise<User> {\n\tconst hash = hashToken(token);\n\n\t// Validate token one more time\n\tconst authToken = await adapter.getToken(hash, \"invite\");\n\tif (!authToken || authToken.expiresAt < new Date()) {\n\t\tthrow new InviteError(\"invalid_token\", \"Invalid or expired invite\");\n\t}\n\n\tif (!authToken.email || authToken.role === null) {\n\t\tthrow new InviteError(\"invalid_token\", \"Invalid invite data\");\n\t}\n\n\t// Delete token (single-use)\n\tawait adapter.deleteToken(hash);\n\n\t// Create user\n\tconst user = await adapter.createUser({\n\t\temail: authToken.email,\n\t\tname: userData.name,\n\t\tavatarUrl: userData.avatarUrl,\n\t\trole: authToken.role,\n\t\temailVerified: true, // Email verified by accepting invite\n\t});\n\n\treturn user;\n}\n\nexport class InviteError extends Error {\n\tconstructor(\n\t\tpublic code: \"invalid_token\" | \"token_expired\" | \"user_exists\",\n\t\tmessage: string,\n\t) {\n\t\tsuper(message);\n\t\tthis.name = \"InviteError\";\n\t}\n}\n","/**\n * Magic link authentication\n */\n\nimport { escapeHtml } from \"../invite.js\";\nimport { generateTokenWithHash, hashToken } from \"../tokens.js\";\nimport type { AuthAdapter, User, EmailMessage } from \"../types.js\";\n\nconst TOKEN_EXPIRY_MS = 15 * 60 * 1000; // 15 minutes\n\n/** Function that sends an email (matches the EmailPipeline.send signature) */\nexport type EmailSendFn = (message: EmailMessage) => Promise<void>;\n\nexport interface MagicLinkConfig {\n\tbaseUrl: string;\n\tsiteName: string;\n\t/** Optional email sender. When omitted, magic links cannot be sent. */\n\temail?: EmailSendFn;\n}\n\n/**\n * Add artificial delay with jitter to prevent timing attacks.\n * Range approximates the time for token creation + email send.\n */\nasync function timingDelay(): Promise<void> {\n\tconst delay = 100 + Math.random() * 150; // 100-250ms\n\tawait new Promise((resolve) => setTimeout(resolve, delay));\n}\n\n/**\n * Send a magic link to a user's email.\n *\n * Requires `config.email` to be set. Throws if no email sender is configured.\n */\nexport async function sendMagicLink(\n\tconfig: MagicLinkConfig,\n\tadapter: AuthAdapter,\n\temail: string,\n\ttype: \"magic_link\" | \"recovery\" = \"magic_link\",\n): Promise<void> {\n\tif (!config.email) {\n\t\tthrow new MagicLinkError(\"email_not_configured\", \"Email is not configured\");\n\t}\n\n\t// Find user\n\tconst user = await adapter.getUserByEmail(email);\n\tif (!user) {\n\t\t// Don't reveal whether user exists - add delay to match successful path timing\n\t\tawait timingDelay();\n\t\treturn;\n\t}\n\n\t// Generate token\n\tconst { token, hash } = generateTokenWithHash();\n\n\t// Store token hash\n\tawait adapter.createToken({\n\t\thash,\n\t\tuserId: user.id,\n\t\temail: user.email,\n\t\ttype,\n\t\texpiresAt: new Date(Date.now() + TOKEN_EXPIRY_MS),\n\t});\n\n\t// Build magic link URL\n\tconst url = new URL(\"/api/auth/magic-link/verify\", config.baseUrl);\n\turl.searchParams.set(\"token\", token);\n\n\t// Send email\n\tconst safeName = escapeHtml(config.siteName);\n\tawait config.email({\n\t\tto: user.email,\n\t\tsubject: `Sign in to ${config.siteName}`,\n\t\ttext: `Click this link to sign in to ${config.siteName}:\\n\\n${url.toString()}\\n\\nThis link expires in 15 minutes.\\n\\nIf you didn't request this, you can safely ignore this email.`,\n\t\thtml: `\n<!DOCTYPE html>\n<html>\n<head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n</head>\n<body style=\"font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.5; color: #333; max-width: 600px; margin: 0 auto; padding: 20px;\">\n <h1 style=\"font-size: 24px; margin-bottom: 20px;\">Sign in to ${safeName}</h1>\n <p>Click the button below to sign in:</p>\n <p style=\"margin: 30px 0;\">\n <a href=\"${url.toString()}\" style=\"background-color: #0066cc; color: white; padding: 12px 24px; text-decoration: none; border-radius: 6px; display: inline-block;\">Sign in</a>\n </p>\n <p style=\"color: #666; font-size: 14px;\">This link expires in 15 minutes.</p>\n <p style=\"color: #666; font-size: 14px;\">If you didn't request this, you can safely ignore this email.</p>\n</body>\n</html>`,\n\t});\n}\n\n/**\n * Verify a magic link token and return the user\n */\nexport async function verifyMagicLink(adapter: AuthAdapter, token: string): Promise<User> {\n\tconst hash = hashToken(token);\n\n\t// Find and validate token\n\tconst authToken = await adapter.getToken(hash, \"magic_link\");\n\tif (!authToken) {\n\t\t// Also check for recovery tokens\n\t\tconst recoveryToken = await adapter.getToken(hash, \"recovery\");\n\t\tif (!recoveryToken) {\n\t\t\tthrow new MagicLinkError(\"invalid_token\", \"Invalid or expired link\");\n\t\t}\n\t\treturn verifyTokenAndGetUser(adapter, recoveryToken, hash);\n\t}\n\n\treturn verifyTokenAndGetUser(adapter, authToken, hash);\n}\n\nasync function verifyTokenAndGetUser(\n\tadapter: AuthAdapter,\n\tauthToken: { userId: string | null; expiresAt: Date },\n\thash: string,\n): Promise<User> {\n\t// Check expiry\n\tif (authToken.expiresAt < new Date()) {\n\t\tawait adapter.deleteToken(hash);\n\t\tthrow new MagicLinkError(\"token_expired\", \"This link has expired\");\n\t}\n\n\t// Delete token (single-use)\n\tawait adapter.deleteToken(hash);\n\n\t// Get user\n\tif (!authToken.userId) {\n\t\tthrow new MagicLinkError(\"invalid_token\", \"Invalid token\");\n\t}\n\n\tconst user = await adapter.getUserById(authToken.userId);\n\tif (!user) {\n\t\tthrow new MagicLinkError(\"user_not_found\", \"User not found\");\n\t}\n\n\treturn user;\n}\n\nexport class MagicLinkError extends Error {\n\tconstructor(\n\t\tpublic code: \"invalid_token\" | \"token_expired\" | \"user_not_found\" | \"email_not_configured\",\n\t\tmessage: string,\n\t) {\n\t\tsuper(message);\n\t\tthis.name = \"MagicLinkError\";\n\t}\n}\n","/**\n * Self-signup for allowed email domains\n */\n\nimport { escapeHtml } from \"./invite.js\";\nimport { generateTokenWithHash, hashToken } from \"./tokens.js\";\nimport type { AuthAdapter, RoleLevel, EmailMessage, User } from \"./types.js\";\n\nconst TOKEN_EXPIRY_MS = 15 * 60 * 1000; // 15 minutes\n\n/** Function that sends an email (matches the EmailPipeline.send signature) */\nexport type EmailSendFn = (message: EmailMessage) => Promise<void>;\n\n/**\n * Add artificial delay with jitter to prevent timing attacks.\n * Range approximates the time for token creation + email send.\n */\nasync function timingDelay(): Promise<void> {\n\tconst delay = 100 + Math.random() * 150; // 100-250ms\n\tawait new Promise((resolve) => setTimeout(resolve, delay));\n}\n\nexport interface SignupConfig {\n\tbaseUrl: string;\n\tsiteName: string;\n\t/** Optional email sender. When omitted, signup verification cannot be sent. */\n\temail?: EmailSendFn;\n}\n\n/**\n * Check if an email domain is allowed for self-signup\n */\nexport async function canSignup(\n\tadapter: AuthAdapter,\n\temail: string,\n): Promise<{ allowed: boolean; role: RoleLevel } | null> {\n\tconst domain = email.split(\"@\")[1]?.toLowerCase();\n\tif (!domain) return null;\n\n\tconst allowedDomain = await adapter.getAllowedDomain(domain);\n\tif (!allowedDomain || !allowedDomain.enabled) {\n\t\treturn null;\n\t}\n\n\treturn {\n\t\tallowed: true,\n\t\trole: allowedDomain.defaultRole,\n\t};\n}\n\n/**\n * Request self-signup (sends verification email).\n *\n * Requires `config.email` to be set. Throws if no email sender is configured.\n */\nexport async function requestSignup(\n\tconfig: SignupConfig,\n\tadapter: AuthAdapter,\n\temail: string,\n): Promise<void> {\n\tif (!config.email) {\n\t\tthrow new SignupError(\"email_not_configured\", \"Email is not configured\");\n\t}\n\n\t// Check if user already exists\n\tconst existing = await adapter.getUserByEmail(email);\n\tif (existing) {\n\t\t// Don't reveal that user exists - add delay to match successful path timing\n\t\tawait timingDelay();\n\t\treturn;\n\t}\n\n\t// Check if domain is allowed\n\tconst signup = await canSignup(adapter, email);\n\tif (!signup) {\n\t\t// Don't reveal that domain is not allowed - add delay to match successful path timing\n\t\tawait timingDelay();\n\t\treturn;\n\t}\n\n\t// Generate token\n\tconst { token, hash } = generateTokenWithHash();\n\n\t// Store token with role info\n\tawait adapter.createToken({\n\t\thash,\n\t\temail,\n\t\ttype: \"email_verify\",\n\t\trole: signup.role,\n\t\texpiresAt: new Date(Date.now() + TOKEN_EXPIRY_MS),\n\t});\n\n\t// Build verification URL\n\tconst url = new URL(\"/api/auth/signup/verify\", config.baseUrl);\n\turl.searchParams.set(\"token\", token);\n\n\t// Send email\n\tconst safeName = escapeHtml(config.siteName);\n\tawait config.email({\n\t\tto: email,\n\t\tsubject: `Verify your email for ${config.siteName}`,\n\t\ttext: `Click this link to verify your email and create your account:\\n\\n${url.toString()}\\n\\nThis link expires in 15 minutes.\\n\\nIf you didn't request this, you can safely ignore this email.`,\n\t\thtml: `\n<!DOCTYPE html>\n<html>\n<head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n</head>\n<body style=\"font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.5; color: #333; max-width: 600px; margin: 0 auto; padding: 20px;\">\n <h1 style=\"font-size: 24px; margin-bottom: 20px;\">Verify your email</h1>\n <p>Click the button below to verify your email and create your ${safeName} account:</p>\n <p style=\"margin: 30px 0;\">\n <a href=\"${url.toString()}\" style=\"background-color: #0066cc; color: white; padding: 12px 24px; text-decoration: none; border-radius: 6px; display: inline-block;\">Verify Email</a>\n </p>\n <p style=\"color: #666; font-size: 14px;\">This link expires in 15 minutes.</p>\n <p style=\"color: #666; font-size: 14px;\">If you didn't request this, you can safely ignore this email.</p>\n</body>\n</html>`,\n\t});\n}\n\n/**\n * Validate a signup verification token\n */\nexport async function validateSignupToken(\n\tadapter: AuthAdapter,\n\ttoken: string,\n): Promise<{ email: string; role: RoleLevel }> {\n\tconst hash = hashToken(token);\n\n\tconst authToken = await adapter.getToken(hash, \"email_verify\");\n\tif (!authToken) {\n\t\tthrow new SignupError(\"invalid_token\", \"Invalid or expired verification link\");\n\t}\n\n\tif (authToken.expiresAt < new Date()) {\n\t\tawait adapter.deleteToken(hash);\n\t\tthrow new SignupError(\"token_expired\", \"This link has expired\");\n\t}\n\n\tif (!authToken.email || authToken.role === null) {\n\t\tthrow new SignupError(\"invalid_token\", \"Invalid token data\");\n\t}\n\n\treturn {\n\t\temail: authToken.email,\n\t\trole: authToken.role,\n\t};\n}\n\n/**\n * Complete signup process (after passkey registration)\n */\nexport async function completeSignup(\n\tadapter: AuthAdapter,\n\ttoken: string,\n\tuserData: {\n\t\tname?: string;\n\t\tavatarUrl?: string;\n\t},\n): Promise<User> {\n\tconst hash = hashToken(token);\n\n\t// Validate token one more time\n\tconst authToken = await adapter.getToken(hash, \"email_verify\");\n\tif (!authToken || authToken.expiresAt < new Date()) {\n\t\tthrow new SignupError(\"invalid_token\", \"Invalid or expired verification\");\n\t}\n\n\tif (!authToken.email || authToken.role === null) {\n\t\tthrow new SignupError(\"invalid_token\", \"Invalid token data\");\n\t}\n\n\t// Check user doesn't already exist\n\tconst existing = await adapter.getUserByEmail(authToken.email);\n\tif (existing) {\n\t\tawait adapter.deleteToken(hash);\n\t\tthrow new SignupError(\"user_exists\", \"An account with this email already exists\");\n\t}\n\n\t// Delete token (single-use)\n\tawait adapter.deleteToken(hash);\n\n\t// Create user\n\tconst user = await adapter.createUser({\n\t\temail: authToken.email,\n\t\tname: userData.name,\n\t\tavatarUrl: userData.avatarUrl,\n\t\trole: authToken.role,\n\t\temailVerified: true,\n\t});\n\n\treturn user;\n}\n\nexport class SignupError extends Error {\n\tconstructor(\n\t\tpublic code:\n\t\t\t| \"invalid_token\"\n\t\t\t| \"token_expired\"\n\t\t\t| \"user_exists\"\n\t\t\t| \"domain_not_allowed\"\n\t\t\t| \"email_not_configured\",\n\t\tmessage: string,\n\t) {\n\t\tsuper(message);\n\t\tthis.name = \"SignupError\";\n\t}\n}\n","/**\n * OAuth consumer - \"Login with X\" functionality\n */\n\nimport { sha256 } from \"@oslojs/crypto/sha2\";\nimport { encodeBase64urlNoPadding } from \"@oslojs/encoding\";\nimport { z } from \"zod\";\n\nimport type { AuthAdapter, User, RoleLevel } from \"../types.js\";\nimport { github, fetchGitHubEmail } from \"./providers/github.js\";\nimport { google } from \"./providers/google.js\";\nimport type { OAuthProvider, OAuthConfig, OAuthProfile, OAuthState } from \"./types.js\";\n\nexport { github, google };\n\nexport interface OAuthConsumerConfig {\n\tbaseUrl: string;\n\tproviders: {\n\t\tgithub?: OAuthConfig;\n\t\tgoogle?: OAuthConfig;\n\t};\n\t/**\n\t * Check if self-signup is allowed for this email domain\n\t */\n\tcanSelfSignup?: (email: string) => Promise<{ allowed: boolean; role: RoleLevel } | null>;\n}\n\n/**\n * Generate an OAuth authorization URL\n */\nexport async function createAuthorizationUrl(\n\tconfig: OAuthConsumerConfig,\n\tproviderName: \"github\" | \"google\",\n\tstateStore: StateStore,\n): Promise<{ url: string; state: string }> {\n\tconst providerConfig = config.providers[providerName];\n\tif (!providerConfig) {\n\t\tthrow new Error(`OAuth provider ${providerName} not configured`);\n\t}\n\n\tconst provider = getProvider(providerName);\n\tconst state = generateState();\n\tconst redirectUri = `${config.baseUrl}/api/auth/oauth/${providerName}/callback`;\n\n\t// Generate PKCE code verifier for providers that support it\n\tconst codeVerifier = generateCodeVerifier();\n\tconst codeChallenge = await generateCodeChallenge(codeVerifier);\n\n\t// Store state for verification\n\tawait stateStore.set(state, {\n\t\tprovider: providerName,\n\t\tredirectUri,\n\t\tcodeVerifier,\n\t});\n\n\t// Build authorization URL\n\tconst url = new URL(provider.authorizeUrl);\n\turl.searchParams.set(\"client_id\", providerConfig.clientId);\n\turl.searchParams.set(\"redirect_uri\", redirectUri);\n\turl.searchParams.set(\"response_type\", \"code\");\n\turl.searchParams.set(\"scope\", provider.scopes.join(\" \"));\n\turl.searchParams.set(\"state\", state);\n\n\t// PKCE for all providers (GitHub has supported S256 since 2021)\n\turl.searchParams.set(\"code_challenge\", codeChallenge);\n\turl.searchParams.set(\"code_challenge_method\", \"S256\");\n\n\treturn { url: url.toString(), state };\n}\n\n/**\n * Handle OAuth callback\n */\nexport async function handleOAuthCallback(\n\tconfig: OAuthConsumerConfig,\n\tadapter: AuthAdapter,\n\tproviderName: \"github\" | \"google\",\n\tcode: string,\n\tstate: string,\n\tstateStore: StateStore,\n): Promise<User> {\n\tconst providerConfig = config.providers[providerName];\n\tif (!providerConfig) {\n\t\tthrow new Error(`OAuth provider ${providerName} not configured`);\n\t}\n\n\t// Verify state\n\tconst storedState = await stateStore.get(state);\n\tif (!storedState || storedState.provider !== providerName) {\n\t\tthrow new OAuthError(\"invalid_state\", \"Invalid OAuth state\");\n\t}\n\n\t// Delete state (single-use)\n\tawait stateStore.delete(state);\n\n\tconst provider = getProvider(providerName);\n\n\t// Exchange code for tokens\n\tconst tokens = await exchangeCode(\n\t\tprovider,\n\t\tproviderConfig,\n\t\tcode,\n\t\tstoredState.redirectUri,\n\t\tstoredState.codeVerifier,\n\t);\n\n\t// Fetch user profile\n\tconst profile = await fetchProfile(provider, tokens.accessToken, providerName);\n\n\t// Find or create user\n\treturn findOrCreateUser(config, adapter, providerName, profile);\n}\n\n/**\n * Exchange authorization code for tokens\n */\nasync function exchangeCode(\n\tprovider: OAuthProvider,\n\tconfig: OAuthConfig,\n\tcode: string,\n\tredirectUri: string,\n\tcodeVerifier?: string,\n): Promise<{ accessToken: string; idToken?: string }> {\n\tconst body = new URLSearchParams({\n\t\tgrant_type: \"authorization_code\",\n\t\tcode,\n\t\tredirect_uri: redirectUri,\n\t\tclient_id: config.clientId,\n\t\tclient_secret: config.clientSecret,\n\t});\n\n\tif (codeVerifier) {\n\t\tbody.set(\"code_verifier\", codeVerifier);\n\t}\n\n\tconst response = await fetch(provider.tokenUrl, {\n\t\tmethod: \"POST\",\n\t\theaders: {\n\t\t\t\"Content-Type\": \"application/x-www-form-urlencoded\",\n\t\t\tAccept: \"application/json\",\n\t\t},\n\t\tbody,\n\t});\n\n\tif (!response.ok) {\n\t\tconst error = await response.text();\n\t\tthrow new OAuthError(\"token_exchange_failed\", `Token exchange failed: ${error}`);\n\t}\n\n\tconst json: unknown = await response.json();\n\tconst data = z\n\t\t.object({\n\t\t\taccess_token: z.string(),\n\t\t\tid_token: z.string().optional(),\n\t\t})\n\t\t.parse(json);\n\n\treturn {\n\t\taccessToken: data.access_token,\n\t\tidToken: data.id_token,\n\t};\n}\n\n/**\n * Fetch user profile from OAuth provider\n */\nasync function fetchProfile(\n\tprovider: OAuthProvider,\n\taccessToken: string,\n\tproviderName: string,\n): Promise<OAuthProfile> {\n\tif (!provider.userInfoUrl) {\n\t\tthrow new Error(\"Provider does not have userinfo URL\");\n\t}\n\n\tconst response = await fetch(provider.userInfoUrl, {\n\t\theaders: {\n\t\t\tAuthorization: `Bearer ${accessToken}`,\n\t\t\tAccept: \"application/json\",\n\t\t},\n\t});\n\n\tif (!response.ok) {\n\t\tthrow new OAuthError(\"profile_fetch_failed\", `Failed to fetch profile: ${response.status}`);\n\t}\n\n\tconst data = await response.json();\n\tconst profile = provider.parseProfile(data);\n\n\t// GitHub may not return email in main profile\n\tif (providerName === \"github\" && !profile.email) {\n\t\tprofile.email = await fetchGitHubEmail(accessToken);\n\t}\n\n\treturn profile;\n}\n\n/**\n * Find existing user or create new one (with auto-linking)\n */\nasync function findOrCreateUser(\n\tconfig: OAuthConsumerConfig,\n\tadapter: AuthAdapter,\n\tproviderName: string,\n\tprofile: OAuthProfile,\n): Promise<User> {\n\t// Check if OAuth account already linked\n\tconst existingAccount = await adapter.getOAuthAccount(providerName, profile.id);\n\tif (existingAccount) {\n\t\tconst user = await adapter.getUserById(existingAccount.userId);\n\t\tif (!user) {\n\t\t\tthrow new OAuthError(\"user_not_found\", \"Linked user not found\");\n\t\t}\n\t\treturn user;\n\t}\n\n\t// Check if user with this email exists (auto-link)\n\t// Only auto-link when the provider has verified the email to prevent\n\t// account takeover via unverified email on a third-party provider\n\tconst existingUser = await adapter.getUserByEmail(profile.email);\n\tif (existingUser) {\n\t\tif (!profile.emailVerified) {\n\t\t\tthrow new OAuthError(\n\t\t\t\t\"signup_not_allowed\",\n\t\t\t\t\"Cannot link account: email not verified by provider\",\n\t\t\t);\n\t\t}\n\t\tawait adapter.createOAuthAccount({\n\t\t\tprovider: providerName,\n\t\t\tproviderAccountId: profile.id,\n\t\t\tuserId: existingUser.id,\n\t\t});\n\t\treturn existingUser;\n\t}\n\n\t// Check if self-signup is allowed\n\tif (config.canSelfSignup) {\n\t\tconst signup = await config.canSelfSignup(profile.email);\n\t\tif (signup?.allowed) {\n\t\t\t// Create new user\n\t\t\tconst user = await adapter.createUser({\n\t\t\t\temail: profile.email,\n\t\t\t\tname: profile.name,\n\t\t\t\tavatarUrl: profile.avatarUrl,\n\t\t\t\trole: signup.role,\n\t\t\t\temailVerified: profile.emailVerified,\n\t\t\t});\n\n\t\t\t// Link OAuth account\n\t\t\tawait adapter.createOAuthAccount({\n\t\t\t\tprovider: providerName,\n\t\t\t\tproviderAccountId: profile.id,\n\t\t\t\tuserId: user.id,\n\t\t\t});\n\n\t\t\treturn user;\n\t\t}\n\t}\n\n\tthrow new OAuthError(\"signup_not_allowed\", \"Self-signup not allowed for this email domain\");\n}\n\nfunction getProvider(name: \"github\" | \"google\"): OAuthProvider {\n\tswitch (name) {\n\t\tcase \"github\":\n\t\t\treturn github;\n\t\tcase \"google\":\n\t\t\treturn google;\n\t}\n}\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\n/**\n * Generate a random state string for OAuth CSRF protection\n */\nfunction generateState(): string {\n\tconst bytes = new Uint8Array(32);\n\tcrypto.getRandomValues(bytes);\n\treturn encodeBase64urlNoPadding(bytes);\n}\n\nfunction generateCodeVerifier(): string {\n\tconst bytes = new Uint8Array(32);\n\tcrypto.getRandomValues(bytes);\n\treturn encodeBase64urlNoPadding(bytes);\n}\n\nasync function generateCodeChallenge(verifier: string): Promise<string> {\n\tconst bytes = new TextEncoder().encode(verifier);\n\tconst hash = sha256(bytes);\n\treturn encodeBase64urlNoPadding(hash);\n}\n\n// ============================================================================\n// State storage interface\n// ============================================================================\n\nexport interface StateStore {\n\tset(state: string, data: OAuthState): Promise<void>;\n\tget(state: string): Promise<OAuthState | null>;\n\tdelete(state: string): Promise<void>;\n}\n\n// ============================================================================\n// Errors\n// ============================================================================\n\nexport class OAuthError extends Error {\n\tconstructor(\n\t\tpublic code:\n\t\t\t| \"invalid_state\"\n\t\t\t| \"token_exchange_failed\"\n\t\t\t| \"profile_fetch_failed\"\n\t\t\t| \"user_not_found\"\n\t\t\t| \"signup_not_allowed\",\n\t\tmessage: string,\n\t) {\n\t\tsuper(message);\n\t\tthis.name = \"OAuthError\";\n\t}\n}\n","/**\n * @emdash-cms/auth - Passkey-first authentication for EmDash\n *\n * Email is now handled by the plugin email pipeline (see PLUGIN-EMAIL.md).\n * Auth functions accept an optional `email` send function instead of a\n * hardcoded adapter. The route layer bridges `emdash.email.send()` from\n * the pipeline into the auth functions.\n *\n * @example\n * ```ts\n * import { auth } from '@emdash-cms/auth'\n *\n * export default defineConfig({\n * integrations: [\n * emdash({\n * auth: auth({\n * secret: import.meta.env.EMDASH_AUTH_SECRET,\n * passkeys: { rpName: 'My Site' },\n * }),\n * }),\n * ],\n * })\n * ```\n */\n\n// Types\nexport * from \"./types.js\";\n\n// Config\nimport { authConfigSchema as _authConfigSchema } from \"./config.js\";\nexport {\n\tauthConfigSchema,\n\tresolveConfig,\n\ttype AuthConfig,\n\ttype ResolvedAuthConfig,\n} from \"./config.js\";\n\n// RBAC\nexport {\n\tPermissions,\n\thasPermission,\n\trequirePermission,\n\tcanActOnOwn,\n\trequirePermissionOnResource,\n\tPermissionError,\n\tscopesForRole,\n\tclampScopes,\n\ttype Permission,\n} from \"./rbac.js\";\n\n// Tokens\nexport {\n\tgenerateToken,\n\thashToken,\n\tgenerateTokenWithHash,\n\tgenerateSessionId,\n\tgenerateAuthSecret,\n\tsecureCompare,\n\tencrypt,\n\tdecrypt,\n\t// Prefixed API tokens (ec_pat_, ec_oat_, ec_ort_)\n\tTOKEN_PREFIXES,\n\tgeneratePrefixedToken,\n\thashPrefixedToken,\n\t// Scopes\n\tVALID_SCOPES,\n\tvalidateScopes,\n\thasScope,\n\ttype ApiTokenScope,\n\t// PKCE\n\tcomputeS256Challenge,\n} from \"./tokens.js\";\n\n// Passkey\nexport * from \"./passkey/index.js\";\n\n// Magic Link\nexport {\n\tsendMagicLink,\n\tverifyMagicLink,\n\tMagicLinkError,\n\ttype MagicLinkConfig,\n} from \"./magic-link/index.js\";\n\n// Invite\nexport {\n\tcreateInvite,\n\tcreateInviteToken,\n\tvalidateInvite,\n\tcompleteInvite,\n\tInviteError,\n\tescapeHtml,\n\ttype InviteConfig,\n\ttype InviteTokenResult,\n\ttype EmailSendFn,\n} from \"./invite.js\";\n\n// Signup\nexport {\n\tcanSignup,\n\trequestSignup,\n\tvalidateSignupToken,\n\tcompleteSignup,\n\tSignupError,\n\ttype SignupConfig,\n} from \"./signup.js\";\n\n// OAuth\nexport {\n\tcreateAuthorizationUrl,\n\thandleOAuthCallback,\n\tOAuthError,\n\tgithub,\n\tgoogle,\n\ttype StateStore,\n\ttype OAuthConsumerConfig,\n} from \"./oauth/consumer.js\";\nexport type { OAuthProvider, OAuthConfig, OAuthProfile, OAuthState } from \"./oauth/types.js\";\n\n// Email types (implementations moved to plugin email pipeline)\nexport type { EmailAdapter, EmailMessage } from \"./types.js\";\n\n/**\n * Create an auth configuration\n *\n * This is a helper function that validates the config at runtime.\n */\nexport function auth(config: import(\"./config.js\").AuthConfig): import(\"./config.js\").AuthConfig {\n\t// Validate config\n\tconst result = _authConfigSchema.safeParse(config);\n\tif (!result.success) {\n\t\tthrow new Error(`Invalid auth config: ${result.error.message}`);\n\t}\n\treturn result.data;\n}\n"],"mappings":"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"}
|
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 };
|
|
@@ -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.0
|
|
3
|
+
"version": "0.1.0",
|
|
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",
|
|
@@ -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"}
|