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