mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-02 15:09:26 +00:00
3a20e37625
The canonical middleware-order comment in app.ts carried historical
notes from the index.ts refactor ("before Phase 4 finishes", "moves to
routes/* in Phase 4", "moves here in Phase 5") that are no longer
active-voice descriptions of current state. Replace with plain
descriptions matching the final module layout. The 16-step enumeration
and the invariant paragraph about public routers (metaRouter, authRouter,
mfaRouter, ssoRouter) sitting before the auth gate are preserved.
No behavior change.
130 lines
5.4 KiB
TypeScript
130 lines
5.4 KiB
TypeScript
import express, { type Request, type Response } from 'express';
|
|
import cors from 'cors';
|
|
import cookieParser from 'cookie-parser';
|
|
import compression from 'compression';
|
|
import helmet from 'helmet';
|
|
import { globalApiLimiter, pollingLimiter } from './middleware/rateLimiters';
|
|
import { conditionalJsonParser } from './middleware/jsonParser';
|
|
import { nodeContextMiddleware } from './middleware/nodeContext';
|
|
import './types/express';
|
|
|
|
/**
|
|
* Build an Express app with the full middleware pipeline installed.
|
|
*
|
|
* Canonical middleware order (16 steps). Do not reorder without re-running the
|
|
* regression checklist in `docs/internal/architecture/middleware-order.md`.
|
|
*
|
|
* 1. trust proxy
|
|
* 2. helmet
|
|
* 3. cors
|
|
* 4. compression
|
|
* 5. cookieParser
|
|
* 6. globalApiLimiter (at /api)
|
|
* 7. pollingLimiter (at /api)
|
|
* 8. conditionalJsonParser
|
|
* 9. nodeContextMiddleware
|
|
* 10. authGate (at /api) -- registered in index.ts
|
|
* 11. auditLog (at /api) -- registered in index.ts
|
|
* 12. enforceApiTokenScope (at /api) -- registered in index.ts
|
|
* 13. createRemoteProxyMiddleware -- proxy/remoteNodeProxy.ts, registered in index.ts
|
|
* 14. routes -- registered in index.ts from routes/*
|
|
* 15. static serving + SPA fallback -- registered in index.ts
|
|
* 16. errorHandler -- registered in index.ts
|
|
*
|
|
* Steps 10 to 12 and 14 must run after the public auth routers (meta, auth,
|
|
* mfa, sso) are registered so those routes stay reachable without a session
|
|
* cookie. index.ts mounts those public routers before step 10 to preserve
|
|
* that invariant.
|
|
*/
|
|
export function createApp(): express.Express {
|
|
const app = express();
|
|
|
|
// 1. Trust the first reverse proxy (nginx, Traefik, etc.) for correct
|
|
// req.protocol, req.ip, and secure cookie detection behind a proxy.
|
|
app.set('trust proxy', 1);
|
|
|
|
// 2. Security headers.
|
|
// crossOriginEmbedderPolicy: disabled because Monaco editor workers lack COEP headers.
|
|
// hsts: disabled. HSTS must only be set over HTTPS; enabling over HTTP
|
|
// permanently breaks browser access for 1 year.
|
|
// contentSecurityPolicy.upgradeInsecureRequests: explicitly null. Helmet 8
|
|
// merges custom directives with its defaults, which include this directive.
|
|
// It tells browsers to silently upgrade every HTTP sub-resource fetch to
|
|
// HTTPS; on a plain-HTTP self-hosted deployment this causes every JS/CSS
|
|
// asset to fail with ERR_SSL_PROTOCOL_ERROR, producing a blank page.
|
|
// Setting null is the Helmet 8 API to remove a default directive.
|
|
app.use(helmet({
|
|
crossOriginEmbedderPolicy: false,
|
|
// COOP is only meaningful over HTTPS. Over HTTP the browser logs a warning
|
|
// and ignores it, creating noise in the console with no security benefit.
|
|
crossOriginOpenerPolicy: false,
|
|
// Origin-Agent-Cluster is only meaningful over HTTPS. Over plain HTTP the
|
|
// browser logs a warning and ignores it. Disabling removes console noise.
|
|
originAgentCluster: false,
|
|
hsts: false,
|
|
contentSecurityPolicy: {
|
|
directives: {
|
|
defaultSrc: ["'self'"],
|
|
baseUri: ["'self'"],
|
|
fontSrc: ["'self'", 'https:', 'data:'],
|
|
formAction: ["'self'"],
|
|
frameAncestors: ["'self'"],
|
|
// img-src: 'https:' is required for App Store template icons hosted on
|
|
// external registries (e.g. raw.githubusercontent.com).
|
|
imgSrc: ["'self'", 'data:', 'https:'],
|
|
objectSrc: ["'none'"],
|
|
scriptSrc: ["'self'"],
|
|
scriptSrcAttr: ["'none'"],
|
|
styleSrc: ["'self'", 'https:', "'unsafe-inline'"],
|
|
// connect-src: explicit 'self' covers same-origin fetch/XHR/WebSocket.
|
|
// ws: and wss: are included for WebSocket connections in any scheme context.
|
|
connectSrc: ["'self'", 'ws:', 'wss:'],
|
|
// worker-src: Monaco editor creates Web Workers via blob: URLs for
|
|
// language services (syntax highlighting, intellisense). Without blob:
|
|
// they silently fail.
|
|
workerSrc: ["'self'", 'blob:'],
|
|
upgradeInsecureRequests: null,
|
|
},
|
|
},
|
|
}));
|
|
|
|
// 3. CORS: production restricts to FRONTEND_URL; dev mirrors the request
|
|
// origin so Vite's dev server works.
|
|
const corsOrigin = process.env.NODE_ENV === 'production'
|
|
? (process.env.FRONTEND_URL || false)
|
|
: true;
|
|
app.use(cors({
|
|
origin: corsOrigin,
|
|
credentials: true,
|
|
}));
|
|
|
|
// 4. Compression. SSE streams (Content-Type: text/event-stream) MUST NOT be
|
|
// compressed because compression buffers output and would delay event delivery
|
|
// until a flush, breaking live log and status streams.
|
|
app.use(compression({
|
|
filter: (req: Request, res: Response) => {
|
|
const ct = res.getHeader('Content-Type');
|
|
if (typeof ct === 'string' && ct.includes('text/event-stream')) {
|
|
return false;
|
|
}
|
|
return compression.filter(req, res);
|
|
},
|
|
}));
|
|
|
|
// 5. Cookie parser must run before the rate limiters so the hybrid key
|
|
// generator can read req.cookies for per-user rate limit bucketing.
|
|
app.use(cookieParser());
|
|
|
|
// 6-7. Tiered rate limiting (see middleware/rateLimiters.ts for the model).
|
|
app.use('/api/', globalApiLimiter);
|
|
app.use('/api/', pollingLimiter);
|
|
|
|
// 8. Parse JSON on local requests; preserve the raw stream for remote proxy.
|
|
app.use(conditionalJsonParser);
|
|
|
|
// 9. Resolve req.nodeId and short-circuit requests to deleted nodes.
|
|
app.use(nodeContextMiddleware);
|
|
|
|
return app;
|
|
}
|