import { NodePgDatabase } from "drizzle-orm/node-postgres"; import type { RebasePgTable } from "../types"; import { UserRepository, TokenRepository, MfaRepository, AuthRepository, UserData, CreateUserData, RoleData, CreateRoleData, RefreshTokenInfo, RefreshTokenSession, PasswordResetTokenInfo, MagicLinkTokenInfo, UserIdentityData, ListUsersOptions, PaginatedUsersResult, MfaFactor, MfaChallengeInfo, RoleData as Role } from "@rebasepro/server"; export type { Role }; export interface AuthSchemaTables { users: RebasePgTable; refreshTokens: RebasePgTable; passwordResetTokens: RebasePgTable; appConfig: RebasePgTable; userIdentities: RebasePgTable; } /** * The single definition of what an email address looks like in storage. * * Reads have always folded case; writes did not, and normalising was left to * each caller. That asymmetry is only ever one forgotten `.toLowerCase()` away * from a row no lookup can find — the account exists, every sign-in path * reports no such user, and the byte-exact UNIQUE on the column does not stop a * duplicate differing only in case. Applied on both sides here so the guarantee * belongs to the repository rather than to its callers' discipline; the * `lower(email)` unique index added in `ensureAuthTablesExist` is the database * half of the same rule. * * Whitespace goes too: a trailing space survives the fold and reproduces the * problem exactly. * * Re-exported rather than defined here: `@rebasepro/server` and * `@rebasepro/server-mongo` write this column too, and a second copy of this * rule is the defect it exists to prevent. */ import { normalizeEmail } from "@rebasepro/common"; export { normalizeEmail }; /** * PostgreSQL implementation of UserRepository. * Handles all user-related database operations using Drizzle ORM. */ export declare class UserService implements UserRepository { private db; private usersTable; private userIdentitiesTable; constructor(db: NodePgDatabase, tableOrTables?: RebasePgTable | Partial); private getQualifiedUsersTableName; /** * Run a privileged auth write with an explicitly cleared RLS context. * * The auth services run on the base/owner connection, which by design * carries a NULL `app.uid` so the `rebase.uid() IS NULL` server-escape * in the default policies applies. That NULL is normally guaranteed by * `set_config(..., is_local = true)` resetting at transaction end — but a * GUC that survives on a pooled connection (or a connection role that * doesn't bypass RLS: FORCE ROW LEVEL SECURITY, or a non-owner role) * turns the trusted write into an RLS-scoped one and denies it with * SQLSTATE 42501. Clearing the GUCs here, transaction-locally at the * single chokepoint, makes the server context deterministic instead of * trusting whatever state the pool hands us. `rebase.uid()` reads '' as * NULL via NULLIF, so '' is the server context. */ private withServerContext; private mapRowToUser; private mapPayload; /** * @see UserRepository.createUser — an email already in use is a 409. * * The route checks first and answers 409; this is the same answer for the * requests that get past the check, which two clicks on a signup button * are enough to produce. `PersistService` has mapped `23505` to a conflict * for collection writes since the layer that holds the SQLSTATE was made * responsible for saying whose fault a failure is; the auth writes never * got the same treatment and reached the client as "Internal Server Error". */ createUser(data: CreateUserData): Promise; getUserById(id: string): Promise; getUserByEmail(email: string): Promise; getUserByIdentity(provider: string, providerId: string): Promise; getUserIdentities(uid: string): Promise; linkUserIdentity(uid: string, provider: string, providerId: string, profileData?: Record): Promise; updateUser(id: string, data: Partial>): Promise; deleteUser(id: string): Promise; listUsers(): Promise; listUsersPaginated(options?: ListUsersOptions): Promise; /** * Update user's password hash */ updatePassword(id: string, passwordHash: string): Promise; /** * Set email verification status */ setEmailVerified(id: string, verified: boolean): Promise; /** * Set email verification token */ setVerificationToken(id: string, token: string | null): Promise; /** * Find user by email verification token */ getUserByVerificationToken(token: string): Promise; /** * Get roles for a user from database (inline TEXT[] column) */ getUserRoles(uid: string): Promise; /** * Get role IDs for a user */ getUserRoleIds(uid: string): Promise; /** * Set roles for a user (replaces existing roles) */ setUserRoles(uid: string, roleIds: string[]): Promise; /** * Assign a specific role to new user (appends if not present) */ assignDefaultRole(uid: string, roleId: string): Promise; /** * Get user with their roles */ getUserWithRoles(uid: string): Promise<{ user: UserData; roles: Role[]; } | null>; } export declare class RefreshTokenService { private db; private refreshTokensTable; private usersTable; constructor(db: NodePgDatabase, tableOrTables?: RebasePgTable | Partial); /** * Whether the table actually carries a column, so a host application that * supplied its own `refresh_tokens` table — one that predates session * grouping — degrades instead of throwing on every sign-in. */ private has; private col; /** The columns to read back, narrowed to the ones this table has. */ private selection; createToken(uid: string, tokenHash: string, expiresAt: Date, userAgent?: string, ipAddress?: string, session?: RefreshTokenSession): Promise; findByHash(tokenHash: string): Promise; /** * Record that a token was rotated away, keeping the row. * * The row is what lets `/auth/refresh` distinguish "you already used this, * here is a fresh one" from "no idea what this is". Deleting it — which is * what this used to do — collapsed both into a 401 and signed the user out * for the crime of losing a response. */ markRotated(tokenHash: string): Promise; /** Final kill of one sign-in: logout, or revoking a device remotely. */ revokeSession(sessionId: string): Promise; /** * Housekeeping: rotation would otherwise leave a row per refresh forever. * Superseded rows are only needed for as long as a straggler might still * present them, and expired ones are dead weight everywhere. */ prune(uid: string, sessionId: string, supersededBefore: Date): Promise; getTokensValidAfter(uid: string): Promise; setTokensValidAfter(uid: string, at: Date): Promise; deleteByHash(tokenHash: string): Promise; deleteAllForUser(uid: string): Promise; listForUser(uid: string): Promise; deleteById(id: string, uid: string): Promise; } /** * Password reset token service */ export declare class PasswordResetTokenService { private db; private passwordResetTokensTable; constructor(db: NodePgDatabase, tableOrTables?: RebasePgTable | Partial); private getQualifiedPasswordResetTokensTableName; /** * Create a password reset token */ createToken(uid: string, tokenHash: string, expiresAt: Date): Promise; /** * Find a valid (not expired, not used) token by hash */ findValidByHash(tokenHash: string): Promise<{ uid: string; expiresAt: Date; } | null>; /** * Mark token as used */ markAsUsed(tokenHash: string): Promise; /** * Delete all tokens for a user */ deleteAllForUser(uid: string): Promise; /** * Clean up expired tokens */ deleteExpired(): Promise; } /** * Magic link token service. * Handles magic link token storage for passwordless email login. */ export declare class MagicLinkTokenService { private db; private magicLinkTokensTable; constructor(db: NodePgDatabase, tableOrTables?: RebasePgTable | Partial); private getQualifiedTableName; createToken(uid: string, tokenHash: string, expiresAt: Date): Promise; findValidByHash(tokenHash: string): Promise; markAsUsed(tokenHash: string): Promise; } /** * PostgreSQL implementation of TokenRepository. * Combines refresh token and password reset token operations. */ export declare class PostgresTokenRepository implements TokenRepository { private db; private refreshTokenService; private passwordResetTokenService; private magicLinkTokenService; constructor(db: NodePgDatabase, tableOrTables?: RebasePgTable | Partial); createRefreshToken(uid: string, tokenHash: string, expiresAt: Date, userAgent?: string, ipAddress?: string, session?: RefreshTokenSession): Promise; markRefreshTokenRotated(tokenHash: string): Promise; revokeRefreshTokenSession(sessionId: string): Promise; pruneRefreshTokens(uid: string, sessionId: string, supersededBefore: Date): Promise; getTokensValidAfter(uid: string): Promise; setTokensValidAfter(uid: string, at: Date): Promise; findRefreshTokenByHash(tokenHash: string): Promise; deleteRefreshToken(tokenHash: string): Promise; deleteAllRefreshTokensForUser(uid: string): Promise; listRefreshTokensForUser(uid: string): Promise; deleteRefreshTokenById(id: string, uid: string): Promise; createPasswordResetToken(uid: string, tokenHash: string, expiresAt: Date): Promise; findValidPasswordResetToken(tokenHash: string): Promise; markPasswordResetTokenUsed(tokenHash: string): Promise; deleteAllPasswordResetTokensForUser(uid: string): Promise; deleteExpiredTokens(): Promise; createMagicLinkToken(uid: string, tokenHash: string, expiresAt: Date): Promise; findValidMagicLinkToken(tokenHash: string): Promise; markMagicLinkTokenUsed(tokenHash: string): Promise; } /** * PostgreSQL implementation of AuthRepository. * Combines user, role, and token repository operations. * This provides a convenient single-class interface for all auth operations. */ export declare class PostgresAuthRepository implements AuthRepository { private db; private userService; private tokenRepository; constructor(db: NodePgDatabase, tableOrTables?: RebasePgTable | Partial); createUser(data: CreateUserData): Promise; getUserById(id: string): Promise; getUserByEmail(email: string): Promise; getUserByIdentity(provider: string, providerId: string): Promise; getUserIdentities(uid: string): Promise; linkUserIdentity(uid: string, provider: string, providerId: string, profileData?: Record): Promise; updateUser(id: string, data: Partial>): Promise; deleteUser(id: string): Promise; listUsers(): Promise; listUsersPaginated(options?: ListUsersOptions): Promise; updatePassword(id: string, passwordHash: string): Promise; setEmailVerified(id: string, verified: boolean): Promise; setVerificationToken(id: string, token: string | null): Promise; getUserByVerificationToken(token: string): Promise; getUserRoles(uid: string): Promise; getUserRoleIds(uid: string): Promise; setUserRoles(uid: string, roleIds: string[]): Promise; assignDefaultRole(uid: string, roleId: string): Promise; getUserWithRoles(uid: string): Promise<{ user: UserData; roles: RoleData[]; } | null>; getRoleById(id: string): Promise; listRoles(): Promise; createRole(_data: CreateRoleData): Promise; updateRole(id: string, data: Partial>): Promise; deleteRole(_id: string): Promise; createRefreshToken(uid: string, tokenHash: string, expiresAt: Date, userAgent?: string, ipAddress?: string, session?: RefreshTokenSession): Promise; markRefreshTokenRotated(tokenHash: string): Promise; revokeRefreshTokenSession(sessionId: string): Promise; pruneRefreshTokens(uid: string, sessionId: string, supersededBefore: Date): Promise; getTokensValidAfter(uid: string): Promise; setTokensValidAfter(uid: string, at: Date): Promise; findRefreshTokenByHash(tokenHash: string): Promise; deleteRefreshToken(tokenHash: string): Promise; deleteAllRefreshTokensForUser(uid: string): Promise; listRefreshTokensForUser(uid: string): Promise; deleteRefreshTokenById(id: string, uid: string): Promise; createPasswordResetToken(uid: string, tokenHash: string, expiresAt: Date): Promise; findValidPasswordResetToken(tokenHash: string): Promise; markPasswordResetTokenUsed(tokenHash: string): Promise; deleteAllPasswordResetTokensForUser(uid: string): Promise; deleteExpiredTokens(): Promise; createMagicLinkToken(uid: string, tokenHash: string, expiresAt: Date): Promise; findValidMagicLinkToken(tokenHash: string): Promise; markMagicLinkTokenUsed(tokenHash: string): Promise; private _mfaService; private getMfaService; createMfaFactor(uid: string, factorType: "totp", secretEncrypted: string, friendlyName?: string): Promise; getMfaFactors(uid: string): Promise; getMfaFactorById(factorId: string): Promise<(MfaFactor & { secretEncrypted: string; }) | null>; verifyMfaFactor(factorId: string): Promise; updateMfaFactorSecret(factorId: string, secretEncrypted: string): Promise; deleteMfaFactor(factorId: string, uid: string): Promise; createMfaChallenge(factorId: string, ipAddress?: string): Promise; getMfaChallengeById(challengeId: string): Promise; verifyMfaChallenge(challengeId: string): Promise; createRecoveryCodes(uid: string, codeHashes: string[]): Promise; useRecoveryCode(uid: string, codeHash: string): Promise; getUnusedRecoveryCodeCount(uid: string): Promise; deleteAllRecoveryCodes(uid: string): Promise; hasVerifiedMfaFactors(uid: string): Promise; claimMfaFactorCounter(factorId: string, counter: number): Promise; recordMfaChallengeAttempt(challengeId: string): Promise; } /** * PostgreSQL implementation of MfaRepository. * Handles all MFA-related database operations. */ export declare class MfaService implements MfaRepository { private db; private schemaName; constructor(db: NodePgDatabase, schemaName?: string); private qualify; createMfaFactor(uid: string, factorType: "totp", secretEncrypted: string, friendlyName?: string): Promise; getMfaFactors(uid: string): Promise; getMfaFactorById(factorId: string): Promise<(MfaFactor & { secretEncrypted: string; }) | null>; /** * Spend a TOTP time step, once and only once. * * One statement: the `WHERE` is the check, the `UPDATE` is the act, and * `RETURNING` reports which of two concurrent requests carrying the same * six digits won. Reading the counter and then writing it would let both * pass — the exact replay this closes. */ claimMfaFactorCounter(factorId: string, counter: number): Promise; verifyMfaFactor(factorId: string): Promise; updateMfaFactorSecret(factorId: string, secretEncrypted: string): Promise; deleteMfaFactor(factorId: string, uid: string): Promise; createMfaChallenge(factorId: string, ipAddress?: string): Promise; getMfaChallengeById(challengeId: string): Promise; /** * Count one failed guess against a challenge and report the new total. * * Incremented in the database rather than in the route so that guesses * arriving in parallel — the shape any real brute-force takes — cannot * share a single increment. */ recordMfaChallengeAttempt(challengeId: string): Promise; verifyMfaChallenge(challengeId: string): Promise; createRecoveryCodes(uid: string, codeHashes: string[]): Promise; useRecoveryCode(uid: string, codeHash: string): Promise; getUnusedRecoveryCodeCount(uid: string): Promise; deleteAllRecoveryCodes(uid: string): Promise; hasVerifiedMfaFactors(uid: string): Promise; } /** PostgreSQL user repository implementation */ export type PostgresUserRepository = UserService;