Files
STCourses/apps/server/src/main.ts
T
Lucas WintherandClaude Opus 5.5 8045c80d93 fix: final-review wave (CSRF guard, fail-closed owner bootstrap, streamed zips, e2e visibility suite)
- Critical 1 (CSRF): core wrap refuses every POST/PUT/PATCH/DELETE whose Origin
  (Referer as fallback) is not publicUrl's origin or STC_ALLOWED_ORIGINS, and
  any body that is not application/json (multipart only on declared upload
  routes). Better Auth routes go through the same guard; only declared
  identity-provider callbacks (SAML ACS/SLO, OAuth callbacks) are exempt.
  Better Auth trustedOrigins use the same list. Session cookie stays Lax
  (Strict breaks OAuth/OIDC/SAML returns); documented.
- Important 4 / learner minor 4: the first-owner claim and the role row are
  one transaction guarded by "no owner yet"; only a unique violation means
  learner; any other error fails the sign-up with 500 and removes the
  half-created user. Serialised per DB handle (Bun's SQLite connection).
- Important 2 / learner minor 12: code zips stream entry by entry (large
  files stored in 256 KB chunks with a data descriptor and incremental
  CRC-32), so memory stays at one chunk; the cap is now 1 GiB (ZIP32).
- Important 3 / learner minor 10: the e2e runs the production wiring
  (productFeatures order, AI drafts through the real sink) against a
  restricted course and asserts 404/401 for learner and anonymous on
  course, lesson, media, cover, code, zip, notes, progress, attempts, AI
  chat, library, search and AI status; a real Range request (206, 416);
  forged writes (foreign Origin, text/plain, no Origin) are 403 and the MCP
  test command never runs.
- Minors 7-9: compose passes every documented variable; the health check
  uses STC_PORT; productFeatures checks the studio-before-AI order.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 15:42:17 +01:00

44 lines
1.5 KiB
TypeScript

import type { AppConfig, Feature, FeatureFactory } from "@stc/contracts";
import { composeServer, loadConfig, type ComposeOptions } from "@stc/core";
import indexHtml from "../../web/index.html";
import { packageMigrations, productFeatures } from "./features";
export interface StartOptions extends ComposeOptions {}
/**
* Composes the features (core `composeServer`) and serves them with the SPA. Bun routes win over `fetch`;
* "/api/*" falls through to the composed app, so unknown API paths are 404 rather than the SPA.
*/
export async function startServer(config: AppConfig, features: (Feature | FeatureFactory)[], opts: StartOptions = {}) {
const composed = await composeServer(config, features, opts);
const { app } = composed;
let server: Bun.Server<undefined>;
try {
server = Bun.serve({
port: config.port,
routes: { ...app.routes, "/api/*": (req) => app.fetch(req), "/*": indexHtml },
fetch: (req) => app.fetch(req),
// Production (NODE_ENV=production, as in the Docker image) bundles the SPA once, minified, without HMR.
development: process.env.NODE_ENV !== "production",
});
} catch (e) {
await composed.stop();
throw e;
}
return {
server,
ctx: composed.ctx,
features: composed.features,
async stop() {
await server.stop(true);
await composed.stop();
},
};
}
if (import.meta.main) {
const config = loadConfig();
const { server } = await startServer(config, productFeatures(), { migrations: packageMigrations });
console.log(`STCourses listening on ${server.url}`);
}