- Media exclusions move from the editable root .gitignore to the repo's
info/exclude, and dirtyFiles never counts files listed in media.json, so an
author's or remote's .gitignore cannot block publish or Sync. Publish and
Sync refuse commits that track a file over referenced media.
- Importer jobs (code-repo, pdf-deck, transcribe) and the cover route run
under the course lock now shared with the studio (core withCourseLock) and
re-check the course inside it; after conversion they fail with "this course
is now edited in the studio". Convert answers 409 while an import job for
the course is queued or running, and the job re-checks.
- Crash-safe conversion: the import folder is hidden (.converting-<id>) before
the published worktree exists; cross-filesystem moves copy into a staging
dir, fsync, verify size and sha256, then rename; the source is retired as
.converted-<id>.<n>. Recovery runs at server start and before every convert;
a re-run of a finished conversion reports success. The loader skips
dot-folders.
- Minor: no raw NUL bytes in source, link fallback only on EXDEV/EPERM/EMLINK,
referenced media are not read whole by the editor, missing media are logged,
mediaDir JSDoc, docs on per-author copies across filesystems.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- 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]>
Dockerfile on oven/bun:1.3 with ffmpeg, git, unzip and python3-pymupdf, a
production install, non-root user, /data volume and a health check;
docker-compose.yml (project stc) with an optional postgres:17 profile and
read-only course and import mounts; docs/operations.md with configuration,
mounts, security headers and the measured idle memory (101.4 MiB for app plus
Postgres after 5 minutes idle).
Co-Authored-By: Claude Opus 5.5 <[email protected]>