import argon2 from 'argon2'; import type { AppConfig } from './config.js'; import { getPool, withTransaction } from './db.js'; import { hashToken, randomToken, sameTokenHash } from './crypto.js'; import { BUSINESS_ROUTES, businessRouteById, type BusinessRouteId } from './business-routes.js'; import { diagnosticMetadataKeys, noopDiagnosticLogger, type DiagnosticLogger } from './diagnostics.js'; export type AuthRole = 'admin' | 'team_lead' | 'user'; export interface AuthUser { id: string; organizationId: string; username: string; role: AuthRole; erpAccount: string | null; } export interface PublicAccount { id: string; username: string; role: AuthRole; erp_account: string | null; is_active: boolean; authorized_business_route_ids: BusinessRouteId[]; business_authorization_revision: number; last_login_at: string | null; created_at: string; updated_at: string; } export interface AuthSession { id: string; token: string; csrfToken: string; user: AuthUser; } export interface ActiveSession { id: string; user: AuthUser; csrfTokenHash: Buffer; } export class AuthError extends Error { constructor( public readonly code: string, message: string, public readonly statusCode = 401 ) { super(message); this.name = 'AuthError'; } } function normalizeUsername(value: string): string { return String(value || '').trim().toLowerCase(); } function validateUsername(value: string): string { const normalized = normalizeUsername(value); if (!normalized || normalized.length > 160) { throw new AuthError('username_invalid', '账号必须为 1—160 个字符。', 400); } return normalized; } function validatePassword(value: string): string { const password = String(value || ''); if (!password) { throw new AuthError('password_invalid', '密码不能为空。', 400); } return password; } function normalizeErpAccount(value: unknown): string | null { const normalized = String(value ?? '').trim(); if (!normalized) return null; if (normalized.length > 200) { throw new AuthError('erp_account_invalid', 'ERP 账号必须为 1—200 个字符。', 400); } return normalized; } function validateAccountRouting(role: AuthRole, value: unknown): string | null { const erpAccount = normalizeErpAccount(value); if (role === 'admin') { if (erpAccount) throw new AuthError('admin_erp_account_forbidden', '管理员账号不能绑定员工 ERP 账号。', 409); return null; } if (!erpAccount) { throw new AuthError('erp_account_required', '普通用户或组长必须绑定 ERP 账号。', 400); } return erpAccount; } function normalizeRole(value: unknown): AuthRole { if (value === 'admin' || value === 'team_lead') return value; return 'user'; } function isoOrNull(value: unknown): string | null { if (!value) return null; const date = new Date(String(value)); return Number.isFinite(date.getTime()) ? date.toISOString() : null; } const ALL_BUSINESS_ROUTE_IDS = BUSINESS_ROUTES.map((route) => route.routeId); const BUSINESS_ROUTE_DISPLAY_ORDER = new Map( ALL_BUSINESS_ROUTE_IDS.map((routeId, index) => [routeId, index]) ); function normalizeBusinessRouteIds(values: readonly unknown[] | undefined): BusinessRouteId[] { const normalized = [...new Set((values || []).map((value) => String(value || '').trim()).filter(Boolean))]; const invalid = normalized.find((routeId) => !businessRouteById(routeId)); if (invalid) throw new AuthError('business_route_invalid', `业务类型 ${invalid} 不存在。`, 400); return (normalized as BusinessRouteId[]).sort((left, right) => ( (BUSINESS_ROUTE_DISPLAY_ORDER.get(left) ?? Number.MAX_SAFE_INTEGER) - (BUSINESS_ROUTE_DISPLAY_ORDER.get(right) ?? Number.MAX_SAFE_INTEGER) )); } function mapUser(row: Record): AuthUser { return { id: String(row.id), organizationId: String(row.organization_id), username: String(row.username), role: normalizeRole(row.role), erpAccount: normalizeErpAccount(row.erp_account) }; } function mapAccount(row: Record): PublicAccount { const role = normalizeRole(row.role); const storedRouteIds = normalizeBusinessRouteIds( Array.isArray(row.authorized_business_route_ids) ? row.authorized_business_route_ids : [] ); return { id: String(row.id), username: String(row.username), role, erp_account: normalizeErpAccount(row.erp_account), is_active: row.is_active === true || String(row.is_active) === 'true', authorized_business_route_ids: role === 'admin' ? [...ALL_BUSINESS_ROUTE_IDS] : storedRouteIds, business_authorization_revision: Math.max(0, Number(row.business_authorization_revision || 0)), last_login_at: isoOrNull(row.last_login_at), created_at: isoOrNull(row.created_at) || new Date(0).toISOString(), updated_at: isoOrNull(row.updated_at) || new Date(0).toISOString() }; } async function loadPublicAccount( client: import('pg').PoolClient, organizationId: string, userId: string ): Promise { const result = await client.query( `SELECT u.id, u.username, u.role, u.erp_account, u.is_active, u.business_authorization_revision, u.last_login_at, u.created_at, u.updated_at, COALESCE(ARRAY( SELECT route_grant.route_id FROM user_business_route_authorizations route_grant WHERE route_grant.organization_id = u.organization_id AND route_grant.user_id = u.id ), ARRAY[]::text[]) AS authorized_business_route_ids FROM users u WHERE u.organization_id = $1 AND u.id = $2`, [organizationId, userId] ); return result.rowCount ? mapAccount(result.rows[0] as Record) : null; } export class AuthService { private readonly dummyHashPromise = argon2.hash('ltjt-dummy-password', { type: argon2.argon2id, memoryCost: 19_456, timeCost: 2, parallelism: 1 }); constructor( private readonly config: AppConfig, private readonly logger: DiagnosticLogger = noopDiagnosticLogger ) {} private log(metadata: Record, message: string): void { try { this.logger.info(metadata, message); } catch { // Authentication state and audit persistence never depend on logging. } } async ensureOrganization(): Promise<{ id: string; slug: string; name: string }> { const result = await getPool(this.config).query( `INSERT INTO organizations (slug, name) VALUES ($1, $2) ON CONFLICT (slug) DO UPDATE SET name = EXCLUDED.name, updated_at = now() RETURNING id, slug, name`, [this.config.ORG_SLUG, this.config.ORG_NAME] ); return result.rows[0] as { id: string; slug: string; name: string }; } async getOrganization(): Promise<{ id: string; slug: string; name: string } | null> { const result = await getPool(this.config).query( 'SELECT id, slug, name FROM organizations WHERE slug = $1', [this.config.ORG_SLUG] ); return result.rowCount ? result.rows[0] as { id: string; slug: string; name: string } : null; } async bootstrapAdmin(username: string, password: string, { force = false } = {}): Promise { const normalized = validateUsername(username); const validatedPassword = validatePassword(password); const organization = await this.ensureOrganization(); const passwordHash = await argon2.hash(validatedPassword, { type: argon2.argon2id }); return withTransaction(this.config, async (client) => { const existing = await client.query( 'SELECT id FROM users WHERE organization_id = $1 AND username = $2 FOR UPDATE', [organization.id, normalized] ); if (existing.rowCount && !force) { throw new Error(`administrator ${normalized} already exists; use --force to reset it.`); } const result = existing.rowCount ? await client.query( `UPDATE users SET password_hash = $1, role = 'admin', erp_account = NULL, is_active = true, must_change_password = false, password_changed_at = now(), failed_login_count = 0, locked_until = NULL, updated_at = now() WHERE id = $2 RETURNING id, organization_id, username, role, erp_account`, [passwordHash, existing.rows[0].id] ) : await client.query( `INSERT INTO users (organization_id, username, password_hash, role) VALUES ($1, $2, $3, 'admin') RETURNING id, organization_id, username, role, erp_account`, [organization.id, normalized, passwordHash] ); const user = mapUser(result.rows[0]); if (existing.rowCount && force) { await client.query( `UPDATE user_channels SET owner_user_id = NULL, enabled = false, status = 'disabled', last_error = '绑定账号已重置为管理员,渠道已解除绑定。', updated_at = now() WHERE organization_id = $1 AND owner_user_id = $2`, [organization.id, user.id] ); await client.query( `UPDATE browser_connections SET status = 'superseded', erp_account_verified = false WHERE organization_id = $1 AND user_id = $2 AND status = 'connected'`, [organization.id, user.id] ); await client.query( 'UPDATE sessions SET revoked_at = now() WHERE user_id = $1 AND revoked_at IS NULL', [user.id] ); } return user; }); } async authenticate(username: string, password: string, ipAddress: string, userAgent: string): Promise { const normalized = normalizeUsername(username); const pool = getPool(this.config); const lookup = await pool.query( `SELECT id, organization_id, username, password_hash, role, erp_account, is_active, failed_login_count, locked_until FROM users WHERE organization_id = (SELECT id FROM organizations WHERE slug = $1) AND username = $2`, [this.config.ORG_SLUG, normalized] ); const row = lookup.rows[0] as Record | undefined; const lockedUntil = row?.locked_until ? new Date(String(row.locked_until)) : null; if (lockedUntil && lockedUntil.getTime() > Date.now()) { throw new AuthError('account_locked', '账号暂时锁定,请稍后再试。', 429); } const hash = String(row?.password_hash || await this.dummyHashPromise); const valid = await argon2.verify(hash, password || ''); if (!row || !valid || row.is_active === false) { if (row) { await pool.query( `UPDATE users SET failed_login_count = failed_login_count + 1, locked_until = CASE WHEN failed_login_count + 1 >= 5 THEN now() + interval '15 minutes' ELSE locked_until END, updated_at = now() WHERE id = $1`, [row.id] ); } throw new AuthError('invalid_credentials', '账号或密码错误。'); } const user = mapUser(row); await pool.query( `UPDATE users SET failed_login_count = 0, locked_until = NULL, last_login_at = now(), updated_at = now() WHERE id = $1`, [user.id] ); return this.createSession(user, ipAddress, userAgent); } async resetPassword(username: string, password: string): Promise { const normalized = validateUsername(username); const passwordHash = await argon2.hash(validatePassword(password), { type: argon2.argon2id }); const result = await getPool(this.config).query( `UPDATE users SET password_hash = $1, must_change_password = false, password_changed_at = now(), failed_login_count = 0, locked_until = NULL, updated_at = now() WHERE organization_id = (SELECT id FROM organizations WHERE slug = $2) AND username = $3 RETURNING id`, [passwordHash, this.config.ORG_SLUG, normalized] ); if (!result.rowCount) throw new Error(`administrator ${normalized} was not found.`); await getPool(this.config).query( `UPDATE sessions SET revoked_at = now() WHERE user_id = $1 AND revoked_at IS NULL`, [result.rows[0].id] ); } private requireAdmin(actor: AuthUser): void { if (actor.role !== 'admin') throw new AuthError('admin_required', '需要管理员权限。', 403); } private async accountAudit( client: import('pg').PoolClient, actor: AuthUser, eventType: string, targetUserId: string, requestId: string, metadata: Record = {} ): Promise { await client.query( `INSERT INTO audit_events (organization_id, actor_user_id, event_type, entity_type, entity_id, request_id, metadata) VALUES ($1, $2, $3, 'user', $4, $5, $6)`, [actor.organizationId, actor.id, eventType, targetUserId, requestId, metadata] ); this.log({ diagnostic_event: 'audit.event.staged', diagnostic_stage: 'account_audit', request_id: requestId, domain_event: eventType, entity_type: 'user', actor_present: true, metadata_keys: diagnosticMetadataKeys(metadata) }, 'account audit event persisted'); } async listAccounts(actor: AuthUser): Promise { this.requireAdmin(actor); const result = await getPool(this.config).query( `SELECT u.id, u.username, u.role, u.erp_account, u.is_active, u.business_authorization_revision, u.last_login_at, u.created_at, u.updated_at, COALESCE(ARRAY( SELECT route_grant.route_id FROM user_business_route_authorizations route_grant WHERE route_grant.organization_id = u.organization_id AND route_grant.user_id = u.id ), ARRAY[]::text[]) AS authorized_business_route_ids FROM users u WHERE u.organization_id = $1 ORDER BY u.username ASC`, [actor.organizationId] ); return (result.rows as Record[]).map(mapAccount); } async createAccount( actor: AuthUser, input: { username: string; password: string; role: AuthRole; erpAccount?: string; businessRouteIds?: readonly string[]; }, requestId: string ): Promise { this.requireAdmin(actor); const username = validateUsername(input.username); const passwordHash = await argon2.hash(validatePassword(input.password), { type: argon2.argon2id }); const role = normalizeRole(input.role); const erpAccount = validateAccountRouting(role, input.erpAccount); const businessRouteIds = role === 'admin' ? [] : normalizeBusinessRouteIds(input.businessRouteIds); try { return await withTransaction(this.config, async (client) => { await client.query( `SELECT pg_advisory_xact_lock(hashtextextended($1::text || ':' || $2::text, 0))`, [actor.organizationId, username] ); const existing = await client.query( 'SELECT id FROM users WHERE organization_id = $1 AND username = $2', [actor.organizationId, username] ); if (existing.rowCount) throw new AuthError('account_exists', '该账号已存在。', 409); const created = await client.query( `INSERT INTO users (organization_id, username, password_hash, role, erp_account, password_changed_at) VALUES ($1, $2, $3, $4, $5, now()) RETURNING id`, [actor.organizationId, username, passwordHash, role, erpAccount] ); const accountId = String(created.rows[0].id); if (businessRouteIds.length) { await client.query( `INSERT INTO user_business_route_authorizations (organization_id, user_id, route_id, granted_by) SELECT $1, $2, route_id, $3 FROM unnest($4::text[]) AS route_id`, [actor.organizationId, accountId, actor.id, businessRouteIds] ); } const account = await loadPublicAccount(client, actor.organizationId, accountId); if (!account) throw new AuthError('account_not_found', '账号创建后未能读取。', 500); await this.accountAudit(client, actor, 'account.created', account.id, requestId, { role, erp_account_configured: Boolean(erpAccount), authorized_business_route_ids: account.authorized_business_route_ids }); return account; }); } catch (error) { if (error && typeof error === 'object' && String((error as { code?: unknown }).code || '') === '23505') { const constraint = String((error as { constraint?: unknown }).constraint || ''); if (constraint.includes('erp_account')) { throw new AuthError('erp_account_conflict', '该 ERP 账号已经绑定另一个平台账号。', 409); } } throw error; } } async updateAccount( actor: AuthUser, targetUserId: string, input: { role?: AuthRole; isActive?: boolean; erpAccount?: string | null }, requestId: string ): Promise { this.requireAdmin(actor); if (input.role === undefined && input.isActive === undefined && input.erpAccount === undefined) { throw new AuthError('account_update_empty', '没有需要更新的账号字段。', 400); } try { return await withTransaction(this.config, async (client) => { const target = await client.query( `SELECT id, username, role, erp_account, is_active, last_login_at, created_at, updated_at FROM users WHERE organization_id = $1 AND id = $2 FOR UPDATE`, [actor.organizationId, targetUserId] ); if (!target.rowCount) throw new AuthError('account_not_found', '账号不存在。', 404); const before = mapAccount(target.rows[0] as Record); const role = input.role === undefined ? before.role : normalizeRole(input.role); const isActive = input.isActive === undefined ? before.is_active : input.isActive; const erpAccount = validateAccountRouting( role, role === 'admin' ? null : input.erpAccount === undefined ? before.erp_account : input.erpAccount ); const removesActiveAdmin = before.role === 'admin' && before.is_active && (role !== 'admin' || !isActive); if (actor.id === before.id && (role !== 'admin' || !isActive)) { throw new AuthError('self_lockout_forbidden', '不能停用或降级当前登录的管理员账号。', 409); } if (removesActiveAdmin) { const activeAdmins = await client.query( `SELECT id FROM users WHERE organization_id = $1 AND role = 'admin' AND is_active = true FOR UPDATE`, [actor.organizationId] ); if (activeAdmins.rows.filter((row: Record) => String(row.id) !== before.id).length === 0) { throw new AuthError('last_admin_protected', '必须至少保留一个有效管理员账号。', 409); } } const updated = await client.query( `UPDATE users SET role = $1, is_active = $2, erp_account = $3, updated_at = now() WHERE organization_id = $4 AND id = $5 RETURNING id`, [role, isActive, erpAccount, actor.organizationId, before.id] ); const routingIdentityChanged = before.erp_account !== erpAccount; if (before.role !== role || before.is_active !== isActive || routingIdentityChanged) { await client.query( 'UPDATE sessions SET revoked_at = now() WHERE user_id = $1 AND revoked_at IS NULL', [before.id] ); } if (!isActive || role === 'admin') { await client.query( `UPDATE user_channels SET owner_user_id = NULL, enabled = false, status = 'disabled', last_error = $1, updated_at = now() WHERE organization_id = $2 AND owner_user_id = $3`, [role === 'admin' ? '绑定账号已变更为管理员,渠道已解除绑定。' : '绑定账号已停用,渠道已解除绑定。', actor.organizationId, before.id] ); } if (!isActive || routingIdentityChanged || before.role !== role) { await client.query( `UPDATE browser_connections SET status = 'superseded', erp_account_verified = false WHERE organization_id = $1 AND user_id = $2 AND status = 'connected'`, [actor.organizationId, before.id] ); } await this.accountAudit(client, actor, 'account.updated', before.id, requestId, { previous_role: before.role, role, previous_active: before.is_active, active: isActive, erp_account_changed: routingIdentityChanged, sessions_revoked: before.role !== role || before.is_active !== isActive || routingIdentityChanged }); const account = await loadPublicAccount(client, actor.organizationId, String(updated.rows[0].id)); if (!account) throw new AuthError('account_not_found', '账号更新后未能读取。', 500); return account; }); } catch (error) { if (error && typeof error === 'object' && String((error as { code?: unknown }).code || '') === '23505') { const constraint = String((error as { constraint?: unknown }).constraint || ''); if (constraint.includes('erp_account')) { throw new AuthError('erp_account_conflict', '该 ERP 账号已经绑定另一个平台账号。', 409); } } throw error; } } async setBusinessRouteAuthorizations( actor: AuthUser, targetUserId: string, routeIds: readonly string[], expectedRevision: number, requestId: string ): Promise { this.requireAdmin(actor); const authorizedRouteIds = normalizeBusinessRouteIds(routeIds); return withTransaction(this.config, async (client) => { const target = await client.query( `SELECT id, role, business_authorization_revision FROM users WHERE organization_id = $1 AND id = $2 FOR UPDATE`, [actor.organizationId, targetUserId] ); if (!target.rowCount) throw new AuthError('account_not_found', '账号不存在。', 404); const row = target.rows[0] as Record; if (normalizeRole(row.role) === 'admin') { throw new AuthError('admin_business_authorization_fixed', '管理员固定拥有全部业务权限,无需单独授权。', 409); } const currentRevision = Math.max(0, Number(row.business_authorization_revision || 0)); if (currentRevision !== expectedRevision) { throw new AuthError('business_authorization_revision_conflict', '该账号的业务权限已被其他管理员修改,请刷新后重试。', 409); } const previous = await client.query( `SELECT route_id FROM user_business_route_authorizations WHERE organization_id = $1 AND user_id = $2`, [actor.organizationId, targetUserId] ); const previousRouteIds = normalizeBusinessRouteIds( (previous.rows as Record[]).map((item) => item.route_id) ); await client.query( `DELETE FROM user_business_route_authorizations WHERE organization_id = $1 AND user_id = $2`, [actor.organizationId, targetUserId] ); if (authorizedRouteIds.length) { await client.query( `INSERT INTO user_business_route_authorizations (organization_id, user_id, route_id, granted_by) SELECT $1, $2, route_id, $3 FROM unnest($4::text[]) AS route_id`, [actor.organizationId, targetUserId, actor.id, authorizedRouteIds] ); } await client.query( `UPDATE users SET business_authorization_revision = business_authorization_revision + 1, updated_at = now() WHERE organization_id = $1 AND id = $2`, [actor.organizationId, targetUserId] ); await this.accountAudit( client, actor, 'account.business_authorizations_updated', targetUserId, requestId, { previous_business_route_ids: previousRouteIds, business_route_ids: authorizedRouteIds, previous_revision: currentRevision, revision: currentRevision + 1 } ); const account = await loadPublicAccount(client, actor.organizationId, targetUserId); if (!account) throw new AuthError('account_not_found', '账号权限更新后未能读取。', 500); return account; }); } async resetAccountPassword( actor: AuthUser, targetUserId: string, password: string, requestId: string ): Promise { this.requireAdmin(actor); const passwordHash = await argon2.hash(validatePassword(password), { type: argon2.argon2id }); await withTransaction(this.config, async (client) => { const target = await client.query( 'SELECT id FROM users WHERE organization_id = $1 AND id = $2 FOR UPDATE', [actor.organizationId, targetUserId] ); if (!target.rowCount) throw new AuthError('account_not_found', '账号不存在。', 404); await client.query( `UPDATE users SET password_hash = $1, must_change_password = false, password_changed_at = now(), failed_login_count = 0, locked_until = NULL, updated_at = now() WHERE id = $2`, [passwordHash, targetUserId] ); const revoked = await client.query( 'UPDATE sessions SET revoked_at = now() WHERE user_id = $1 AND revoked_at IS NULL', [targetUserId] ); await this.accountAudit(client, actor, 'account.password_reset', targetUserId, requestId, { sessions_revoked: revoked.rowCount || 0 }); }); } async revokeAccountSessions(actor: AuthUser, targetUserId: string, requestId: string): Promise { this.requireAdmin(actor); return withTransaction(this.config, async (client) => { const target = await client.query( 'SELECT id FROM users WHERE organization_id = $1 AND id = $2 FOR UPDATE', [actor.organizationId, targetUserId] ); if (!target.rowCount) throw new AuthError('account_not_found', '账号不存在。', 404); const revoked = await client.query( 'UPDATE sessions SET revoked_at = now() WHERE user_id = $1 AND revoked_at IS NULL', [targetUserId] ); const count = revoked.rowCount || 0; await this.accountAudit(client, actor, 'account.sessions_revoked', targetUserId, requestId, { sessions_revoked: count }); return count; }); } async changeOwnPassword( session: ActiveSession, currentPassword: string, newPassword: string, requestId: string ): Promise { const validatedNewPassword = validatePassword(newPassword); const newHash = await argon2.hash(validatedNewPassword, { type: argon2.argon2id }); await withTransaction(this.config, async (client) => { const target = await client.query( `SELECT password_hash, is_active FROM users WHERE organization_id = $1 AND id = $2 FOR UPDATE`, [session.user.organizationId, session.user.id] ); const row = target.rows[0] as Record | undefined; if (!row || row.is_active === false || !(await argon2.verify(String(row.password_hash), currentPassword || ''))) { throw new AuthError('current_password_invalid', '当前密码不正确。', 403); } if (await argon2.verify(String(row.password_hash), validatedNewPassword)) { throw new AuthError('password_unchanged', '新密码不能与当前密码相同。', 409); } await client.query( `UPDATE users SET password_hash = $1, must_change_password = false, password_changed_at = now(), failed_login_count = 0, locked_until = NULL, updated_at = now() WHERE id = $2`, [newHash, session.user.id] ); const revoked = await client.query( `UPDATE sessions SET revoked_at = now() WHERE user_id = $1 AND id <> $2 AND revoked_at IS NULL`, [session.user.id, session.id] ); await this.accountAudit(client, session.user, 'account.password_changed', session.user.id, requestId, { other_sessions_revoked: revoked.rowCount || 0 }); }); } async createSession(user: AuthUser, ipAddress: string, userAgent: string): Promise { const token = randomToken(32); const csrfToken = randomToken(24); const result = await getPool(this.config).query( `INSERT INTO sessions (user_id, token_hash, csrf_token_hash, expires_at, idle_expires_at, ip_address, user_agent) VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING id`, [user.id, hashToken(token), hashToken(csrfToken), null, null, ipAddress || null, userAgent || null] ); return { id: String(result.rows[0].id), token, csrfToken, user }; } async getActiveSession(token: string | undefined): Promise { if (!token) return null; const result = await getPool(this.config).query( `SELECT s.id AS session_id, s.csrf_token_hash, u.id, u.organization_id, u.username, u.role, u.erp_account FROM sessions s JOIN users u ON u.id = s.user_id WHERE s.token_hash = $1 AND s.revoked_at IS NULL AND u.is_active = true`, [hashToken(token)] ); if (!result.rowCount) return null; const row = result.rows[0] as Record; await getPool(this.config).query( 'UPDATE sessions SET last_seen_at = now() WHERE id = $1', [row.session_id] ); return { id: String(row.session_id), csrfTokenHash: Buffer.isBuffer(row.csrf_token_hash) ? row.csrf_token_hash : Buffer.from(String(row.csrf_token_hash || ''), 'base64'), user: mapUser({ id: row.id, organization_id: row.organization_id, username: row.username, role: row.role, erp_account: row.erp_account }) }; } async rotateCsrf(sessionId: string): Promise { const token = randomToken(24); await getPool(this.config).query( 'UPDATE sessions SET csrf_token_hash = $1 WHERE id = $2 AND revoked_at IS NULL', [hashToken(token), sessionId] ); return token; } async verifyCsrf(session: ActiveSession, candidate: string | undefined): Promise { if (!candidate) return false; return sameTokenHash(session.csrfTokenHash, hashToken(candidate)); } async revokeSession(token: string | undefined): Promise { if (!token) return; await getPool(this.config).query('UPDATE sessions SET revoked_at = now() WHERE token_hash = $1', [hashToken(token)]); } async revokeAllSessions(userId: string): Promise { await getPool(this.config).query('UPDATE sessions SET revoked_at = now() WHERE user_id = $1 AND revoked_at IS NULL', [userId]); } async recordAudit(organizationId: string, userId: string | null, eventType: string, requestId: string, metadata: Record = {}): Promise { await getPool(this.config).query( `INSERT INTO audit_events (organization_id, actor_user_id, event_type, entity_type, entity_id, request_id, metadata) VALUES ($1, $2, 'auth.' || $3, 'session', $4, $5, $6)`, [organizationId, userId, eventType, userId || '', requestId, metadata] ); this.log({ diagnostic_event: 'audit.event.staged', diagnostic_stage: 'auth_audit', request_id: requestId, domain_event: `auth.${eventType}`, entity_type: 'session', actor_present: Boolean(userId), metadata_keys: diagnosticMetadataKeys(metadata) }, 'authentication audit event persisted'); } }