maintenance.ts65 lines · main
| 1 | import type { MiddlewareHandler } from 'hono'; |
| 2 | |
| 3 | import { getMaintenanceState } from '../services/platform-settings.js'; |
| 4 | |
| 5 | /** |
| 6 | * Platform-wide maintenance gate. Gates on the EFFECTIVE maintenance |
| 7 | * state (manual `maintenanceMode` override OR an active scheduled |
| 8 | * `maintenanceWindow`) — see getMaintenanceState(). When active, every |
| 9 | * request returns 503 except a small whitelist: |
| 10 | * - /health, /ready (so the operator + monitoring still see liveness) |
| 11 | * - /v1/auth/* (Better Auth — admin must be able to sign in) |
| 12 | * - /v1/me, /v1/me/* (the signed-in admin needs /me + /me/step-up) |
| 13 | * - /v1/admin/* (admin actions, including the toggle to flip |
| 14 | * maintenance mode back off) |
| 15 | * - /v1/status/* (public status feed — incidents + maintenance |
| 16 | * state — MUST stay reachable so the marketing |
| 17 | * site can render the maintenance page) |
| 18 | * |
| 19 | * The flag lives in platform_settings (DB-backed, 60s cached) so an |
| 20 | * admin can flip it from the dashboard without an env change or |
| 21 | * deploy. Cache TTL means the change takes effect within ~60s on peer |
| 22 | * instances and immediately on the writer. |
| 23 | * |
| 24 | * Mounted at app level on /v1/* AFTER attachSession + before any |
| 25 | * project-scoped middleware, so /health (no /v1 prefix) is naturally |
| 26 | * exempt and Better Auth's /v1/auth/* responds normally for the |
| 27 | * sign-in path. |
| 28 | */ |
| 29 | export function maintenanceMode(): MiddlewareHandler { |
| 30 | return async (c, next) => { |
| 31 | const path = new URL(c.req.url).pathname; |
| 32 | if (isWhitelisted(path)) { |
| 33 | await next(); |
| 34 | return; |
| 35 | } |
| 36 | const state = await getMaintenanceState(); |
| 37 | if (!state.active) { |
| 38 | await next(); |
| 39 | return; |
| 40 | } |
| 41 | return c.json( |
| 42 | { |
| 43 | code: 'maintenance_mode', |
| 44 | message: |
| 45 | 'briven is in maintenance mode. critical paths (auth, /health, /ready, admin) remain available.', |
| 46 | }, |
| 47 | 503, |
| 48 | ); |
| 49 | }; |
| 50 | } |
| 51 | |
| 52 | function isWhitelisted(path: string): boolean { |
| 53 | if (path === '/health' || path === '/ready') return true; |
| 54 | if (path.startsWith('/v1/auth/')) return true; |
| 55 | if (path === '/v1/me' || path.startsWith('/v1/me/')) return true; |
| 56 | if (path.startsWith('/v1/admin/')) return true; |
| 57 | // Public status feed (incidents + maintenance state). Kept open so the |
| 58 | // marketing site can read maintenance state to render its status page |
| 59 | // DURING maintenance — otherwise the web can't discover the window. |
| 60 | if (path.startsWith('/v1/status/')) return true; |
| 61 | // /info is the build-info probe used by the dashboard footer. Keep |
| 62 | // it open so the operator's own dashboard renders during maintenance. |
| 63 | if (path === '/info') return true; |
| 64 | return false; |
| 65 | } |