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 <noreply@anthropic.com>
This commit is contained in:
1 parent
69aaedcb43
commit
8045c80d93
24 files changed
+725
-85
No files matched your search
@@ -17,7 +17,7 @@ async function setup(db: any, env: Record<string, string | undefined> = {}) {
|
||||
const courses = await createCourseRepository([join(dataDir, "courses")]);
|
||||
const ctx = {
|
||||
config: resolveConfig({
|
||||
databaseUrl: "", dataDir, courseDirs: [join(dataDir, "courses")], secret: "t", publicUrl: "",
|
||||
databaseUrl: "", dataDir, courseDirs: [join(dataDir, "courses")], secret: "t", publicUrl: "http://x",
|
||||
python: process.env.STC_PYTHON || "python3",
|
||||
importRoots: "STC_IMPORT_ROOTS" in env ? (env.STC_IMPORT_ROOTS ? [env.STC_IMPORT_ROOTS] : undefined) : [tmpdir()],
|
||||
transcribeUrl: env.STC_TRANSCRIBE_URL ?? null,
|
||||
@@ -31,7 +31,7 @@ async function setup(db: any, env: Record<string, string | undefined> = {}) {
|
||||
const app = createApp(ctx, [feature]);
|
||||
const call = (path: string, init: RequestInit & { as?: string; json?: unknown } = {}) =>
|
||||
app.fetch(new Request(`http://x${path}`, {
|
||||
method: init.method, headers: { ...(init.as ? { "x-user": init.as } : {}), ...(init.json !== undefined ? { "content-type": "application/json" } : {}) },
|
||||
method: init.method, headers: { origin: "http://x", ...(init.as ? { "x-user": init.as } : {}), ...(init.json !== undefined ? { "content-type": "application/json" } : {}) },
|
||||
body: init.json !== undefined ? JSON.stringify(init.json) : undefined,
|
||||
}));
|
||||
return { ctx, queue, call, dataDir };
|
||||
|
||||
Reference in new issue
Block a user