{"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\talgorithm: number;\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/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\talgorithm: credential.algorithm,\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\talgorithm: credential.algorithm,\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\talgorithm: row.algorithm,\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  algorithm INTEGER NOT NULL DEFAULT -7,\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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"}