import { type Kysely } from 'kysely'; import type { Database, User, UsersTable } from '../db/schema.js'; import { type BatchTarget } from '../db/batch.js'; export type UserRole = UsersTable['role']; export interface CreateUserInput { email: string; name: string; role?: UserRole; avatarUrl?: string | null; /** When present, a credential row is written so the user can sign in with a password. */ password?: string; } export declare function findUserByEmail(db: Kysely, email: string): Promise; export declare function findUserById(db: Kysely, id: string): Promise; export declare function createUser(db: Kysely, input: CreateUserInput): Promise; export declare function setPassword(db: Kysely, userId: string, password: string): Promise; /** * Verify an email/password pair. * * Returns `undefined` for every failure mode — unknown user, no password set, wrong password — * so the caller cannot accidentally surface which one occurred. A dummy hash is verified when the * user does not exist so that the response time does not reveal whether an account is registered. */ export declare function verifyCredentials(db: Kysely, email: string, password: string): Promise; /** * Find or create the local user behind an OAuth identity. * * Links to an existing account by verified email so a user who was invited by email and then signs * in with Google lands in the same account instead of getting a duplicate. */ export declare function upsertOAuthUser(db: Kysely, params: { provider: 'google' | 'github' | 'microsoft'; providerUserId: string; email: string; name: string; avatarUrl?: string | null; /** Role for a first-time user. Existing users keep the role they already have. */ defaultRole?: UserRole; }): Promise; export declare function normalizeEmail(email: string): string; export declare function countUsers(db: Kysely): Promise; export declare function listUsers(db: Kysely): Promise; /** * Create the very first admin, and only if there is no user at all. * * This backs the first-run setup screen, which is the one **unauthenticated write path** in the * whole admin — it exists because a fresh deployment with password sign-in has no OAuth land-grab * to bootstrap from and no account to sign in with. Everything about it has to be exactly right. * * The check and the insert are one statement. A `count()` followed by an `insert` is a race with a * window wide enough to matter here: the screen is reachable by anyone who finds the URL in the * seconds after a deploy, and two requests arriving together would both read zero and both create * an admin — one of them an attacker's. `INSERT ... SELECT ... WHERE NOT EXISTS` cannot do that. * The loser learns it lost by reading back: its own id is absent, because the insert was * conditional on the table being empty and somebody else had already filled it. (It used to read * the affected-row count, which a batch does not report — the guarantee is unchanged, only how the * answer is obtained.) * * Raw SQL rather than the query builder because this shape has no Kysely spelling that stays * readable, and it is identical on both drivers. */ export declare function createFirstAdmin(target: BatchTarget, input: { email: string; name: string; password: string; }): Promise; /** * Change a user's role, refusing to remove the last admin. * * A CMS with no administrator cannot be administered back into having one — every screen that * could fix it is behind the role that just went away, and the setup screen refuses to help * because users exist. Demoting yourself by accident is an easy click; recovering from it means a * database console. */ export declare function setUserRole(db: Kysely, userId: string, role: UserRole): Promise; /** Deactivate or reactivate. Deactivating drops the user's sessions immediately. */ export declare function setUserActive(db: Kysely, userId: string, isActive: boolean): Promise; export declare class UserError extends Error { readonly code: 'duplicate_email' | 'last_admin' | 'not_found'; name: string; constructor(message: string, code: 'duplicate_email' | 'last_admin' | 'not_found'); }