import { isSessionActive, resolveSessionDeadlines } from './session-policy' import type { TokenDigester } from './token-digest' export interface AuthUserRecord { id: string email: string displayName: string passwordHash: string emailVerified: boolean image: string | null status: 'active' | 'disabled' | 'pending_deletion' createdAt: Date updatedAt: Date } export interface AuthSessionRecord { id: string userId: string tokenHash: string createdAt: Date lastSeenAt: Date idleExpiresAt: Date absoluteExpiresAt: Date revokedAt: Date | null sourceIpHash: string | null userAgentSummary: string | null } export interface CreateAuthSessionRecord { userId: string tokenHash: string createdAt: Date lastSeenAt: Date idleExpiresAt: Date absoluteExpiresAt: Date sourceIpHash: string | null userAgentSummary: string | null } export interface AuthPersistence { findUserById(id: string): Promise findUserByEmail(email: string): Promise updateUser( id: string, update: Partial< Pick< AuthUserRecord, 'displayName' | 'email' | 'emailVerified' | 'image' | 'passwordHash' > >, ): Promise createSession(input: CreateAuthSessionRecord): Promise findSessionByTokenHash(tokenHash: string): Promise touchSession( id: string, input: { lastSeenAt: Date; idleExpiresAt: Date }, ): Promise revokeSessionByTokenHash(tokenHash: string, revokedAt: Date): Promise revokeSessionsForUser(userId: string, revokedAt: Date): Promise } export interface ActiveAuthSession { session: AuthSessionRecord user: AuthUserRecord rawToken: string } export class AuthService { constructor( private readonly persistence: AuthPersistence, private readonly digester: TokenDigester, private readonly now: () => Date = () => new Date(), ) {} findUserById(id: string) { return this.persistence.findUserById(id) } findUserByEmail(email: string) { return this.persistence.findUserByEmail(email.trim().toLowerCase()) } updateUser( id: string, update: Partial< Pick< AuthUserRecord, 'displayName' | 'email' | 'emailVerified' | 'image' | 'passwordHash' > >, ) { return this.persistence.updateUser(id, update) } async createSession(input: { userId: string rawToken: string sourceIpHash?: string | null userAgentSummary?: string | null }): Promise { const user = await this.persistence.findUserById(input.userId) if (!user || user.status !== 'active') return null const now = this.now() const deadlines = resolveSessionDeadlines({ createdAt: now, now }) const session = await this.persistence.createSession({ userId: user.id, tokenHash: this.digester.digest(input.rawToken), createdAt: now, lastSeenAt: now, ...deadlines, sourceIpHash: input.sourceIpHash ?? null, userAgentSummary: input.userAgentSummary ?? null, }) return { session, user, rawToken: input.rawToken } } async findActiveSession(rawToken: string): Promise { const now = this.now() const session = await this.persistence.findSessionByTokenHash( this.digester.digest(rawToken), ) if (!session || !isSessionActive(now, session)) return null const user = await this.persistence.findUserById(session.userId) if (!user || user.status !== 'active') return null const deadlines = resolveSessionDeadlines({ createdAt: session.createdAt, now, absoluteExpiresAt: session.absoluteExpiresAt, }) const touched = await this.persistence.touchSession(session.id, { lastSeenAt: now, idleExpiresAt: deadlines.idleExpiresAt, }) if (!touched) return null return { session: touched, user, rawToken } } revokeSession(rawToken: string): Promise { return this.persistence.revokeSessionByTokenHash( this.digester.digest(rawToken), this.now(), ) } revokeSessionsForUser(userId: string): Promise { return this.persistence.revokeSessionsForUser(userId, this.now()) } }