index.ts283 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 { authRouter } from './routes/auth.js';
26import { authCliRouter } from './routes/auth-cli.js';
27import { authProductRetiredRouter } from './routes/auth-product-retired.js';
28import { authCoreStatusRouter } from './routes/auth-core-status.js';
29import { authCoreFdiRouter } from './routes/auth-core-fdi.js';
30import { authCoreSessionRouter } from './routes/auth-core-session.js';
31import { authCoreLoginMethodsRouter } from './routes/auth-core-loginmethods.js';
32import { authCoreDashboardRouter } from './routes/auth-core-dashboard.js';
33import { authCoreUsersRouter } from './routes/auth-core-users.js';
34import { authCoreRolesRouter } from './routes/auth-core-roles.js';
35import { authCoreKeysRouter } from './routes/auth-core-keys.js';
36import { authCoreM2mRouter } from './routes/auth-core-m2m.js';
37import { authCoreIdpRouter } from './routes/auth-core-idp.js';
38import { authCoreProjectRouter } from './routes/auth-core-project.js';
39import { authCoreSsoRouter } from './routes/auth-core-sso.js';
40import { authCoreRouter } from './routes/auth-core.js';
41import { initAuthCoreSdk } from './services/auth-core/engine.js';
42// Option B Phase 7+: yellow tabs + enterprise SAML/OIDC on briven-engine.
43import { billingRouter } from './routes/billing.js';
44import { brandingPublicRouter } from './routes/branding-public.js';
45import { dbRouter } from './routes/db.js';
46import { deploymentsRouter } from './routes/deployments.js';
47import { exportRouter } from './routes/export.js';
48import { BUILD_AT, BUILD_SHA, healthRouter } from './routes/health.js';
49import { internalRouter } from './routes/internal.js';
50import { invitationsRouter } from './routes/invitations.js';
51import { invokeRouter } from './routes/invoke.js';
52import { logsRouter } from './routes/logs.js';
53import { meRouter } from './routes/me.js';
54import { mitteraWebhookRouter } from './routes/mittera-webhook.js';
55import { orgsRouter } from './routes/orgs.js';
56import { outboundWebhooksRouter } from './routes/outbound-webhooks.js';
57import { projectEnvRouter } from './routes/project-env.js';
58import { projectMcpRouter } from './routes/project-mcp.js';
59import { membersRouter } from './routes/project-members.js';
60import { projectsRouter } from './routes/projects.js';
61import { rootRouter } from './routes/root.js';
62import { schedulesRouter } from './routes/schedules.js';
63import { storageKeysRouter } from './routes/storage-keys.js';
64import { storageRouter } from './routes/storage.js';
65import { studioRouter } from './routes/studio.js';
66import { platformRouter } from './routes/platform.js';
67import { usageRouter } from './routes/usage.js';
68import { incidentsRouter } from './routes/incidents.js';
69import { marketingEventsPublicRouter } from './routes/marketing-events.js';
70import { mcpServerRouter } from './routes/mcp-server.js';
71import { mediaRouter } from './routes/media.js';
72import { contactPublicRouter } from './routes/contact.js';
73import {
74 migrationRequestsPublicRouter,
75 migrationRequestsRouter,
76} from './routes/migration-requests.js';
77import { webhooksAdminRouter } from './routes/webhooks-admin.js';
78import { webhooksPublicRouter } from './routes/webhooks-public.js';
79import { recordDeploy } from './services/deploy-history.js';
80import { startAccountDeletionGc } from './workers/account-deletion-gc.js';
81import { startAutoSnapshotWorker } from './workers/auto-snapshot.js';
82import { startScheduleDispatcher } from './workers/schedule-dispatcher.js';
83import {
84 startAuditRetentionCron,
85 startLogFanoutWorker,
86 startLogRetentionCron,
87 startOutboundWebhookDeliveriesRetentionCron,
88 startWebhookDeliveriesRetentionCron,
89} from './workers/log-fanout.js';
90import { startOutboundWebhookDispatcher } from './workers/outbound-webhook-dispatcher.js';
91import { startPolarMeterPush } from './workers/polar-meter-push.js';
92import { startStorageJanitor } from './workers/storage-janitor.js';
93import { startUsageAggregator } from './workers/usage-aggregator.js';
94
95type AppEnv = {
96 Variables: {
97 requestId: string;
98 user: User | null;
99 session: Session | null;
100 apiKeyId: string | null;
101 };
102};
103
104const app = new Hono<AppEnv>();
105
106app.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
119app.use('*', requestId());
120app.use('*', accessLog());
121app.use('*', metricsMiddleware());
122app.use('*', attachSession());
123app.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.
128app.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.
139app.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.
147app.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.
154app.use('/v1/projects/:id', blockIfProjectSuspended());
155app.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.
160app.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.
166app.route('/', mediaRouter);
167
168app.route('/', rootRouter);
169app.route('/', healthRouter);
170app.route('/', authRouter);
171app.route('/', authCliRouter);
172app.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).
175app.route('/', authCoreStatusRouter);
176app.route('/', authCoreFdiRouter);
177app.route('/', authCoreSessionRouter);
178app.route('/', authCoreLoginMethodsRouter);
179app.route('/', authCoreDashboardRouter);
180app.route('/', authCoreUsersRouter);
181app.route('/', authCoreRolesRouter);
182app.route('/', authCoreKeysRouter);
183app.route('/', authCoreM2mRouter); // M2M client credentials + /oauth/token
184app.route('/', authCoreIdpRouter); // OIDC IdP (Briven as SuperTokens-class provider)
185app.route('/', authCoreProjectRouter);
186app.route('/', authCoreSsoRouter); // SAML + OIDC enterprise SSO
187app.route('/', authCoreRouter); // workspace + enable Auth
188app.route('/', authProductRetiredRouter);
189log.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});
197app.route('/', projectsRouter);
198app.route('/', apiKeysRouter);
199app.route('/', membersRouter);
200app.route('/', deploymentsRouter);
201app.route('/', invokeRouter);
202app.route('/', internalRouter);
203app.route('/', projectEnvRouter);
204app.route('/', projectMcpRouter);
205app.route('/', invitationsRouter);
206app.route('/', adminRouter);
207app.route('/', adminAgentsRouter);
208app.route('/', adminMcpRouter);
209app.route('/', adminRevenueRouter);
210app.route('/', adminManifestRouter);
211app.route('/', adminTimeseriesRouter);
212app.route('/', billingRouter);
213app.route('/', dbRouter);
214app.route('/', logsRouter);
215app.route('/', usageRouter);
216app.route('/', abuseRouter);
217app.route('/', studioRouter);
218app.route('/', platformRouter);
219app.route('/', exportRouter);
220app.route('/', aiRouter);
221app.route('/', orgsRouter);
222app.route('/', mitteraWebhookRouter);
223app.route('/', schedulesRouter);
224app.route('/', storageRouter);
225app.route('/', storageKeysRouter);
226app.route('/', webhooksAdminRouter);
227app.route('/', webhooksPublicRouter);
228app.route('/', incidentsRouter);
229app.route('/', migrationRequestsRouter);
230app.route('/', migrationRequestsPublicRouter);
231app.route('/', contactPublicRouter);
232app.route('/', marketingEventsPublicRouter);
233app.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.
237app.route('/', mcpServerRouter);
238
239// No full authServiceRouter / authV2Router / auth-core product mounts (Phase 1).
240
241app.notFound((c) => c.json({ code: 'not_found', message: 'route not found' }, 404));
242app.onError(errorHandler);
243
244log.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).
248startOriginAllowlist();
249
250// Option B Phase 1: ensure Doltgres `briven_engine` DB + schema (no SuperTokens Core).
251void 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).
257startLogFanoutWorker();
258startLogRetentionCron();
259startAuditRetentionCron();
260startUsageAggregator();
261startPolarMeterPush();
262startAccountDeletionGc();
263startScheduleDispatcher();
264startWebhookDeliveriesRetentionCron();
265startOutboundWebhookDispatcher();
266startOutboundWebhookDeliveriesRetentionCron();
267startStorageJanitor();
268startAutoSnapshotWorker();
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.
273void recordDeploy({
274 service: 'api',
275 buildSha: BUILD_SHA,
276 buildAt: BUILD_AT === 'dev' ? null : BUILD_AT,
277 env: env.BRIVEN_ENV,
278});
279
280export default {
281 port: env.BRIVEN_API_PORT,
282 fetch: app.fetch,
283};