index.ts283 lines · main
| 1 | import { Hono } from 'hono'; |
| 2 | import { cors } from 'hono/cors'; |
| 3 | import { secureHeaders } from 'hono/secure-headers'; |
| 4 | |
| 5 | import { env } from './env.js'; |
| 6 | import { log } from './lib/logger.js'; |
| 7 | import { resolveCorsOrigin, startOriginAllowlist } from './services/auth-origin-allowlist.js'; |
| 8 | import { accessLog } from './middleware/access-log.js'; |
| 9 | import { csrfOriginCheck } from './middleware/csrf.js'; |
| 10 | import { errorHandler } from './middleware/error.js'; |
| 11 | import { maintenanceMode } from './middleware/maintenance.js'; |
| 12 | import { metricsMiddleware } from './middleware/metrics.js'; |
| 13 | import { blockIfProjectSuspended } from './middleware/project-suspended.js'; |
| 14 | import { requestId } from './middleware/request-id.js'; |
| 15 | import { attachSession, type Session, type User } from './middleware/session.js'; |
| 16 | import { abuseRouter } from './routes/abuse.js'; |
| 17 | import { adminRouter } from './routes/admin.js'; |
| 18 | import { adminAgentsRouter } from './routes/admin-agents.js'; |
| 19 | import { adminMcpRouter } from './routes/admin-mcp.js'; |
| 20 | import { adminRevenueRouter } from './routes/admin-revenue.js'; |
| 21 | import { adminManifestRouter } from './routes/admin-manifest.js'; |
| 22 | import { adminTimeseriesRouter } from './routes/admin-timeseries.js'; |
| 23 | import { aiRouter } from './routes/ai.js'; |
| 24 | import { apiKeysRouter } from './routes/api-keys.js'; |
| 25 | import { authRouter } from './routes/auth.js'; |
| 26 | import { authCliRouter } from './routes/auth-cli.js'; |
| 27 | import { authProductRetiredRouter } from './routes/auth-product-retired.js'; |
| 28 | import { authCoreStatusRouter } from './routes/auth-core-status.js'; |
| 29 | import { authCoreFdiRouter } from './routes/auth-core-fdi.js'; |
| 30 | import { authCoreSessionRouter } from './routes/auth-core-session.js'; |
| 31 | import { authCoreLoginMethodsRouter } from './routes/auth-core-loginmethods.js'; |
| 32 | import { authCoreDashboardRouter } from './routes/auth-core-dashboard.js'; |
| 33 | import { authCoreUsersRouter } from './routes/auth-core-users.js'; |
| 34 | import { authCoreRolesRouter } from './routes/auth-core-roles.js'; |
| 35 | import { authCoreKeysRouter } from './routes/auth-core-keys.js'; |
| 36 | import { authCoreM2mRouter } from './routes/auth-core-m2m.js'; |
| 37 | import { authCoreIdpRouter } from './routes/auth-core-idp.js'; |
| 38 | import { authCoreProjectRouter } from './routes/auth-core-project.js'; |
| 39 | import { authCoreSsoRouter } from './routes/auth-core-sso.js'; |
| 40 | import { authCoreRouter } from './routes/auth-core.js'; |
| 41 | import { initAuthCoreSdk } from './services/auth-core/engine.js'; |
| 42 | // Option B Phase 7+: yellow tabs + enterprise SAML/OIDC on briven-engine. |
| 43 | import { billingRouter } from './routes/billing.js'; |
| 44 | import { brandingPublicRouter } from './routes/branding-public.js'; |
| 45 | import { dbRouter } from './routes/db.js'; |
| 46 | import { deploymentsRouter } from './routes/deployments.js'; |
| 47 | import { exportRouter } from './routes/export.js'; |
| 48 | import { BUILD_AT, BUILD_SHA, healthRouter } from './routes/health.js'; |
| 49 | import { internalRouter } from './routes/internal.js'; |
| 50 | import { invitationsRouter } from './routes/invitations.js'; |
| 51 | import { invokeRouter } from './routes/invoke.js'; |
| 52 | import { logsRouter } from './routes/logs.js'; |
| 53 | import { meRouter } from './routes/me.js'; |
| 54 | import { mitteraWebhookRouter } from './routes/mittera-webhook.js'; |
| 55 | import { orgsRouter } from './routes/orgs.js'; |
| 56 | import { outboundWebhooksRouter } from './routes/outbound-webhooks.js'; |
| 57 | import { projectEnvRouter } from './routes/project-env.js'; |
| 58 | import { projectMcpRouter } from './routes/project-mcp.js'; |
| 59 | import { membersRouter } from './routes/project-members.js'; |
| 60 | import { projectsRouter } from './routes/projects.js'; |
| 61 | import { rootRouter } from './routes/root.js'; |
| 62 | import { schedulesRouter } from './routes/schedules.js'; |
| 63 | import { storageKeysRouter } from './routes/storage-keys.js'; |
| 64 | import { storageRouter } from './routes/storage.js'; |
| 65 | import { studioRouter } from './routes/studio.js'; |
| 66 | import { platformRouter } from './routes/platform.js'; |
| 67 | import { usageRouter } from './routes/usage.js'; |
| 68 | import { incidentsRouter } from './routes/incidents.js'; |
| 69 | import { marketingEventsPublicRouter } from './routes/marketing-events.js'; |
| 70 | import { mcpServerRouter } from './routes/mcp-server.js'; |
| 71 | import { mediaRouter } from './routes/media.js'; |
| 72 | import { contactPublicRouter } from './routes/contact.js'; |
| 73 | import { |
| 74 | migrationRequestsPublicRouter, |
| 75 | migrationRequestsRouter, |
| 76 | } from './routes/migration-requests.js'; |
| 77 | import { webhooksAdminRouter } from './routes/webhooks-admin.js'; |
| 78 | import { webhooksPublicRouter } from './routes/webhooks-public.js'; |
| 79 | import { recordDeploy } from './services/deploy-history.js'; |
| 80 | import { startAccountDeletionGc } from './workers/account-deletion-gc.js'; |
| 81 | import { startAutoSnapshotWorker } from './workers/auto-snapshot.js'; |
| 82 | import { startScheduleDispatcher } from './workers/schedule-dispatcher.js'; |
| 83 | import { |
| 84 | startAuditRetentionCron, |
| 85 | startLogFanoutWorker, |
| 86 | startLogRetentionCron, |
| 87 | startOutboundWebhookDeliveriesRetentionCron, |
| 88 | startWebhookDeliveriesRetentionCron, |
| 89 | } from './workers/log-fanout.js'; |
| 90 | import { startOutboundWebhookDispatcher } from './workers/outbound-webhook-dispatcher.js'; |
| 91 | import { startPolarMeterPush } from './workers/polar-meter-push.js'; |
| 92 | import { startStorageJanitor } from './workers/storage-janitor.js'; |
| 93 | import { startUsageAggregator } from './workers/usage-aggregator.js'; |
| 94 | |
| 95 | type AppEnv = { |
| 96 | Variables: { |
| 97 | requestId: string; |
| 98 | user: User | null; |
| 99 | session: Session | null; |
| 100 | apiKeyId: string | null; |
| 101 | }; |
| 102 | }; |
| 103 | |
| 104 | const app = new Hono<AppEnv>(); |
| 105 | |
| 106 | app.use( |
| 107 | '*', |
| 108 | cors({ |
| 109 | // Dynamic guest list: briven's own origins + any project-registered app |
| 110 | // domain (services/auth-origin-allowlist). FAILS SAFE — an empty or errored |
| 111 | // allowlist falls back to briven-own origins only, never an outage. |
| 112 | origin: (origin) => resolveCorsOrigin(origin), |
| 113 | credentials: true, |
| 114 | allowHeaders: ['Content-Type', 'Authorization', 'x-request-id', 'x-briven-project-id'], |
| 115 | exposeHeaders: ['x-request-id'], |
| 116 | }), |
| 117 | ); |
| 118 | |
| 119 | app.use('*', requestId()); |
| 120 | app.use('*', accessLog()); |
| 121 | app.use('*', metricsMiddleware()); |
| 122 | app.use('*', attachSession()); |
| 123 | app.use('*', csrfOriginCheck()); |
| 124 | // Maintenance-mode gate. Reads platform_settings.maintenanceMode and |
| 125 | // returns 503 on everything except /health, /ready, /info, auth, /me, |
| 126 | // and admin routes. Sits AFTER attachSession so admin requests can be |
| 127 | // identified for the whitelist branch. |
| 128 | app.use('*', maintenanceMode()); |
| 129 | |
| 130 | // The public branding logo (served by brandingPublicRouter below) is embedded |
| 131 | // cross-origin via a plain <img src>: the dashboard (briven.tech) and the |
| 132 | // hosted auth pages load it from api.briven.tech. secureHeaders() sets |
| 133 | // `Cross-Origin-Resource-Policy: same-origin` on every response, which makes |
| 134 | // the browser refuse the image (it renders as a broken-image icon even though |
| 135 | // the bytes serve 200/image-png). This path-scoped override is registered |
| 136 | // BEFORE secureHeaders so its post-`next()` write is the OUTERMOST one — it has |
| 137 | // the final say and flips just this one logo route to `cross-origin`, leaving |
| 138 | // every other API response same-origin. |
| 139 | app.use('/v1/projects/:id/auth/branding/logo', async (c, next) => { |
| 140 | await next(); |
| 141 | c.res.headers.set('Cross-Origin-Resource-Policy', 'cross-origin'); |
| 142 | }); |
| 143 | |
| 144 | // Security response headers (HSTS, nosniff, frame deny, etc.) on every |
| 145 | // API response. Placed after the global middleware chain and before the |
| 146 | // route mounts so all handlers inherit it. |
| 147 | app.use('*', secureHeaders()); |
| 148 | |
| 149 | // Block state-changing routes on a suspended project at the app level |
| 150 | // instead of per-router — keeps the abuse-suspension gate from drifting |
| 151 | // when a new mutating route lands without picking up the middleware. The |
| 152 | // middleware short-circuits on GET/HEAD/OPTIONS so dashboards stay |
| 153 | // readable, and on missing :id so the unmounted segments pass through. |
| 154 | app.use('/v1/projects/:id', blockIfProjectSuspended()); |
| 155 | app.use('/v1/projects/:id/*', blockIfProjectSuspended()); |
| 156 | |
| 157 | // Mounted FIRST — before every project-auth guard — so the public branding |
| 158 | // logo stays genuinely public (a hosted login page loads it via <img>). |
| 159 | // See routes/branding-public.ts for why it can't live in authServiceRouter. |
| 160 | app.route('/', brandingPublicRouter); |
| 161 | |
| 162 | // Public media delivery (M3) — serve files marked public from a clean |
| 163 | // `/media/:projectId/:fileId` path with per-tenant CORS. Mounted here (root |
| 164 | // level, before the project-auth guards) so it's genuinely public; it lives |
| 165 | // outside `/v1/projects` so the suspend/auth middleware never touches it. |
| 166 | app.route('/', mediaRouter); |
| 167 | |
| 168 | app.route('/', rootRouter); |
| 169 | app.route('/', healthRouter); |
| 170 | app.route('/', authRouter); |
| 171 | app.route('/', authCliRouter); |
| 172 | app.route('/', meRouter); |
| 173 | // Briven Auth Option B Phase 7: FDI login + yellow dashboard + keys/providers/enterprise. |
| 174 | // Platform operator login stays on authRouter (/v1/auth/* Better Auth for briven.tech). |
| 175 | app.route('/', authCoreStatusRouter); |
| 176 | app.route('/', authCoreFdiRouter); |
| 177 | app.route('/', authCoreSessionRouter); |
| 178 | app.route('/', authCoreLoginMethodsRouter); |
| 179 | app.route('/', authCoreDashboardRouter); |
| 180 | app.route('/', authCoreUsersRouter); |
| 181 | app.route('/', authCoreRolesRouter); |
| 182 | app.route('/', authCoreKeysRouter); |
| 183 | app.route('/', authCoreM2mRouter); // M2M client credentials + /oauth/token |
| 184 | app.route('/', authCoreIdpRouter); // OIDC IdP (Briven as SuperTokens-class provider) |
| 185 | app.route('/', authCoreProjectRouter); |
| 186 | app.route('/', authCoreSsoRouter); // SAML + OIDC enterprise SSO |
| 187 | app.route('/', authCoreRouter); // workspace + enable Auth |
| 188 | app.route('/', authProductRetiredRouter); |
| 189 | log.info('auth_product_idp_m2m_enterprise', { |
| 190 | note: 'briven-engine FDI + dashboard + SSO + M2M + OIDC IdP provider', |
| 191 | engine: 'briven-engine', |
| 192 | appLoginReady: true, |
| 193 | m2mToken: '/v1/auth-core/oauth/token', |
| 194 | oidcIssuer: '/v1/auth-core/oidc', |
| 195 | platformLogin: '/v1/auth/*', |
| 196 | }); |
| 197 | app.route('/', projectsRouter); |
| 198 | app.route('/', apiKeysRouter); |
| 199 | app.route('/', membersRouter); |
| 200 | app.route('/', deploymentsRouter); |
| 201 | app.route('/', invokeRouter); |
| 202 | app.route('/', internalRouter); |
| 203 | app.route('/', projectEnvRouter); |
| 204 | app.route('/', projectMcpRouter); |
| 205 | app.route('/', invitationsRouter); |
| 206 | app.route('/', adminRouter); |
| 207 | app.route('/', adminAgentsRouter); |
| 208 | app.route('/', adminMcpRouter); |
| 209 | app.route('/', adminRevenueRouter); |
| 210 | app.route('/', adminManifestRouter); |
| 211 | app.route('/', adminTimeseriesRouter); |
| 212 | app.route('/', billingRouter); |
| 213 | app.route('/', dbRouter); |
| 214 | app.route('/', logsRouter); |
| 215 | app.route('/', usageRouter); |
| 216 | app.route('/', abuseRouter); |
| 217 | app.route('/', studioRouter); |
| 218 | app.route('/', platformRouter); |
| 219 | app.route('/', exportRouter); |
| 220 | app.route('/', aiRouter); |
| 221 | app.route('/', orgsRouter); |
| 222 | app.route('/', mitteraWebhookRouter); |
| 223 | app.route('/', schedulesRouter); |
| 224 | app.route('/', storageRouter); |
| 225 | app.route('/', storageKeysRouter); |
| 226 | app.route('/', webhooksAdminRouter); |
| 227 | app.route('/', webhooksPublicRouter); |
| 228 | app.route('/', incidentsRouter); |
| 229 | app.route('/', migrationRequestsRouter); |
| 230 | app.route('/', migrationRequestsPublicRouter); |
| 231 | app.route('/', contactPublicRouter); |
| 232 | app.route('/', marketingEventsPublicRouter); |
| 233 | app.route('/', outboundWebhooksRouter); |
| 234 | // mcp.briven.tech — the live MCP server endpoint (Streamable HTTP at /mcp). |
| 235 | // Bearer-authenticated per-project key; the global csrf middleware's |
| 236 | // Bearer carve-out lets the server-to-server POST through. |
| 237 | app.route('/', mcpServerRouter); |
| 238 | |
| 239 | // No full authServiceRouter / authV2Router / auth-core product mounts (Phase 1). |
| 240 | |
| 241 | app.notFound((c) => c.json({ code: 'not_found', message: 'route not found' }, 404)); |
| 242 | app.onError(errorHandler); |
| 243 | |
| 244 | log.info('api_boot', { port: env.BRIVEN_API_PORT, origin: env.BRIVEN_API_ORIGIN }); |
| 245 | |
| 246 | // Warm the per-project allowed-origin allowlist into memory (best-effort; |
| 247 | // the CORS/CSRF gates fall back to briven-own origins until it loads). |
| 248 | startOriginAllowlist(); |
| 249 | |
| 250 | // Option B Phase 1: ensure Doltgres `briven_engine` DB + schema (no SuperTokens Core). |
| 251 | void initAuthCoreSdk().then((ok) => { |
| 252 | log.info('briven_engine_boot', { ok, appLoginReady: false }); |
| 253 | }); |
| 254 | |
| 255 | // Background workers — both degrade gracefully when redis/data-plane |
| 256 | // isn't configured (log-fanout sleeps, retention prunes nothing). |
| 257 | startLogFanoutWorker(); |
| 258 | startLogRetentionCron(); |
| 259 | startAuditRetentionCron(); |
| 260 | startUsageAggregator(); |
| 261 | startPolarMeterPush(); |
| 262 | startAccountDeletionGc(); |
| 263 | startScheduleDispatcher(); |
| 264 | startWebhookDeliveriesRetentionCron(); |
| 265 | startOutboundWebhookDispatcher(); |
| 266 | startOutboundWebhookDeliveriesRetentionCron(); |
| 267 | startStorageJanitor(); |
| 268 | startAutoSnapshotWorker(); |
| 269 | |
| 270 | // Audit-trail behind /info — one row per boot. recordDeploy itself |
| 271 | // short-circuits when buildSha is the "dev" sentinel and never throws, |
| 272 | // so the request path stays alive even if the meta-DB is unreachable. |
| 273 | void recordDeploy({ |
| 274 | service: 'api', |
| 275 | buildSha: BUILD_SHA, |
| 276 | buildAt: BUILD_AT === 'dev' ? null : BUILD_AT, |
| 277 | env: env.BRIVEN_ENV, |
| 278 | }); |
| 279 | |
| 280 | export default { |
| 281 | port: env.BRIVEN_API_PORT, |
| 282 | fetch: app.fetch, |
| 283 | }; |