auth-reliability.ts103 lines · main
1/**
2 * S6 — Auth reliability signals (in-process + Prometheus).
3 *
4 * Lightweight counters so operators can see rate-limit pressure, mailer
5 * failures, and Redis fallback without waiting for a full APM stack.
6 * Exposed via:
7 * - GET /v1/admin/auth-reliability (admin dashboard)
8 * - GET /metrics (Prometheus: briven_auth_* counters)
9 * - getAuthReliabilitySnapshot() embedded in admin health notes
10 */
11
12import { incCounter } from '../lib/metrics.js';
13import { env } from '../env.js';
14import { pingRedis } from '../lib/redis.js';
15
16const startedAt = new Date().toISOString();
17
18const counters = {
19 rateLimitDenied: 0,
20 rateLimitMemoryFallback: 0,
21 mailerFailures: 0,
22 authRoute5xx: 0,
23};
24
25export type AuthReliabilityBackend = 'redis' | 'memory';
26
27export function recordAuthRateLimitDenied(backend: AuthReliabilityBackend): void {
28 counters.rateLimitDenied += 1;
29 incCounter('briven_auth_rate_limit_denied_total', { backend });
30}
31
32export function recordAuthRateLimitMemoryFallback(): void {
33 counters.rateLimitMemoryFallback += 1;
34 incCounter('briven_auth_rate_limit_memory_fallback_total', { reason: 'no_redis' });
35}
36
37export function recordAuthMailerFailure(kind: string): void {
38 counters.mailerFailures += 1;
39 incCounter('briven_auth_mailer_failures_total', { kind: kind.slice(0, 48) });
40}
41
42export function recordAuthRoute5xx(): void {
43 counters.authRoute5xx += 1;
44 incCounter('briven_auth_route_5xx_total', {});
45}
46
47/** Test helper — resets process-local counters only (not Prometheus registry). */
48export function resetAuthReliabilityCountersForTests(): void {
49 counters.rateLimitDenied = 0;
50 counters.rateLimitMemoryFallback = 0;
51 counters.mailerFailures = 0;
52 counters.authRoute5xx = 0;
53}
54
55export interface AuthReliabilitySnapshot {
56 startedAt: string;
57 uptimeSec: number;
58 redisConfigured: boolean;
59 redisOk: boolean | null;
60 counters: {
61 rateLimitDenied: number;
62 rateLimitMemoryFallback: number;
63 mailerFailures: number;
64 authRoute5xx: number;
65 };
66 /** Operator-facing guidance (S6.3). */
67 watch: readonly string[];
68 isolation: {
69 claim: string;
70 proveInDashboard: string;
71 unitTests: string;
72 };
73}
74
75export async function getAuthReliabilitySnapshot(): Promise<AuthReliabilitySnapshot> {
76 const redisConfigured = Boolean(env.BRIVEN_REDIS_URL);
77 let redisOk: boolean | null = null;
78 if (redisConfigured) {
79 redisOk = await pingRedis();
80 }
81
82 return {
83 startedAt,
84 uptimeSec: Math.floor(process.uptime()),
85 redisConfigured,
86 redisOk,
87 counters: { ...counters },
88 watch: [
89 'GET /ready → checks.redis must be ok when BRIVEN_REDIS_URL is set',
90 'briven_auth_rate_limit_denied_total rising under attack = limits working',
91 'briven_auth_rate_limit_memory_fallback_total rising = Redis missing; limits per-process only',
92 'briven_auth_mailer_failures_total rising = magic-link / recovery mail broken',
93 'briven_auth_route_5xx_total rising = auth path internal errors',
94 ],
95 isolation: {
96 claim:
97 'Auth users, sessions, and pk_briven_auth_* keys are scoped per project. Cross-project reads must 401/403/404.',
98 proveInDashboard:
99 'Create two projects, enable Auth on both, sign up on A, confirm A user is absent from B Auth → Users.',
100 unitTests: 'bun test src/services/auth-tenant-isolation.test.ts src/services/auth-rate-limit.test.ts',
101 },
102 };
103}