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:
Lucas Winther
2026-10-30 15:42:17 +01:00
co-authored by Claude Opus 5.5
parent 69aaedcb43
commit 8045c80d93
24 changed files with 725 additions and 85 deletions
+21
View File
@@ -22,6 +22,27 @@ services:
STC_UPLOAD_MAX_MB: ${STC_UPLOAD_MAX_MB:-100}
STC_AI_ALLOWED_BASE_URLS: ${STC_AI_ALLOWED_BASE_URLS:-}
STC_TRANSCRIBE_URL: ${STC_TRANSCRIBE_URL:-}
STC_TRANSCRIBE_KEY: ${STC_TRANSCRIBE_KEY:-}
STC_TRANSCRIBE_MODEL: ${STC_TRANSCRIBE_MODEL:-}
STC_ALLOWED_ORIGINS: ${STC_ALLOWED_ORIGINS:-}
# Sign-in providers (unset ones stay off). Social: STC_AUTH_<GITHUB|GOOGLE|MICROSOFT|GITLAB|DISCORD>_ID/_SECRET.
STC_AUTH_GITHUB_ID: ${STC_AUTH_GITHUB_ID:-}
STC_AUTH_GITHUB_SECRET: ${STC_AUTH_GITHUB_SECRET:-}
STC_AUTH_GOOGLE_ID: ${STC_AUTH_GOOGLE_ID:-}
STC_AUTH_GOOGLE_SECRET: ${STC_AUTH_GOOGLE_SECRET:-}
STC_AUTH_MICROSOFT_ID: ${STC_AUTH_MICROSOFT_ID:-}
STC_AUTH_MICROSOFT_SECRET: ${STC_AUTH_MICROSOFT_SECRET:-}
STC_AUTH_GITLAB_ID: ${STC_AUTH_GITLAB_ID:-}
STC_AUTH_GITLAB_SECRET: ${STC_AUTH_GITLAB_SECRET:-}
STC_AUTH_DISCORD_ID: ${STC_AUTH_DISCORD_ID:-}
STC_AUTH_DISCORD_SECRET: ${STC_AUTH_DISCORD_SECRET:-}
STC_AUTH_OIDC_ISSUER: ${STC_AUTH_OIDC_ISSUER:-}
STC_AUTH_OIDC_ID: ${STC_AUTH_OIDC_ID:-}
STC_AUTH_OIDC_SECRET: ${STC_AUTH_OIDC_SECRET:-}
STC_AUTH_OIDC_NAME: ${STC_AUTH_OIDC_NAME:-}
STC_AUTH_OIDC_SCOPES: ${STC_AUTH_OIDC_SCOPES:-}
STC_AUTH_OIDC_DISCOVERY_URL: ${STC_AUTH_OIDC_DISCOVERY_URL:-}
STC_AUTH_GROUPS_CLAIM: ${STC_AUTH_GROUPS_CLAIM:-}
depends_on:
postgres:
condition: service_healthy