- 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]>
44 lines
1.5 KiB
TypeScript
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}`);
|
|
}
|