Commit Graph
77 Commits
Author SHA1 Message Date
Lucas WintherandClaude Opus 5.5 771d755da4 fix(studio): review round 1 for imported-course editing
- 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]>
2026-10-30 20:53:50 +01:00
Lucas WintherandClaude Opus 5.5 20246b442f feat(studio): "Make editable in the studio" panel for imported courses
Owner sees what conversion does, starts it, follows progress and lands in the
workbench; other authors are told only the owner can. Existing tokens only.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 20:33:55 +01:00
Lucas WintherandClaude Opus 5.5 afa2338ad3 feat(studio): owner-only "Make editable in the studio" route and conversion job
POST /api/studio/courses/:id/convert (owner only, CSRF-guarded by core) queues
studio.convert-import; GET /api/studio/jobs/:id reports progress. Read-only
responses say whether the caller may convert. Resolves TODO(studio) (ruling R9).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 20:32:59 +01:00
Lucas WintherandClaude Opus 5.5 df6d235070 feat(studio): convert an imported course into a studio repo, media kept outside git
Small files are committed unchanged (visibility and source.kind stay); large media
are referenced from .stcourses/media.json, ignored, and hard-linked into every
worktree from <dataDir>/media/<id>, where the import folder is moved. Studio writes
to referenced media are refused and never write through a shared inode.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 20:31:19 +01:00
Lucas WintherandClaude Opus 5.5 60179407f4 feat(studio): media references for imported courses (path, size, sha256, .gitignore)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 20:28:39 +01:00
Lucas WintherandClaude Opus 5.5 db5a604647 fix(ui): cover tiles keep the monogram for landscape images; quiet pill header nav
Landscape images (imported video frames, slide pages) would lose most of the
picture in a 3:4 crop, so the tile keeps its monogram; portrait and
near-square images (w/h <= 1.05) fade in over it, cropped from the centre.
The header nav follows the mockup's segmented-control style: muted text, no
underline, the current area raised in accent.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 15:47:01 +01:00
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
Lucas WintherandClaude Opus 5.5 d71a95dbc1 fix(web): styled role-filtered nav, hidden on phones; clearer studio preview label
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 15:07:45 +01:00
Lucas WintherandClaude Opus 5.5 777ae2cc37 feat: resolve Phase 1 contract requests and compose every feature
Contracts and core:
- Feature gains auth and stop; FeatureFactory; core composeServer builds the
  context, runs the factories, then the job queue from the union of their jobs
  (R8/R14). startServer uses it; main.ts composes learner, import, studio, AI.
- AppConfig/loadConfig own the layout and settings: courseDirs include
  <dataDir>/courses and <dataDir>/worktrees, studioReposDir, cacheDir,
  importRoots, uploadMaxMb, aiAllowedBaseUrls, transcribeUrl, python.
  resolveConfig derives the same for tests; cachePaths shares the importer's
  cache layout.
- canView: the global author role views every course (R14); the learner,
  studio and AI packages all call it.
- migrate(db, packages): core first, then packages in name order.
- Media: nosniff on every response, a sandbox CSP on svg/html/js; a Range on
  an empty file is 416.
- courseUrl/lessonUrl and the lesson route patterns live in @stc/contracts.

Packages:
- import, studio and AI read AppConfig instead of process.env; the importer
  refuses ids used by studio/repos.
- learner migrations run through core migrate; library covers fall back to
  the importer's cached candidate behind a canView-checked route.
- studio preview renders through the learner LessonView (anchors honoured,
  draft URLs); AI drafts go to the studio draft branch for studio courses (as
  the requesting user) and to <dataDir>/drafts otherwise, and the generator
  page says which.

Web: tsconfig paths for @stc/<feature>/web browser entries; features.ts
imports them by package name; nav.roles hides links by role.

Tests: compose, config, canView, media headers, migrations order, covers,
preview, nav, SPA bundle without server code, and an end-to-end flow on
SQLite and Postgres.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 15:02:46 +01:00
Lucas WintherandClaude Opus 5.5 427eda6e93 merge: ai
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 14:44:06 +01:00
Lucas WintherandClaude Opus 5.5 2854c26a7d merge: studio
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 14:42:51 +01:00
Lucas WintherandClaude Opus 5.5 627ebe0029 merge: import
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 14:41:36 +01:00
Lucas WintherandClaude Opus 5.5 0da35ec6f8 fix(learner): remove the SPA proxy; CSP as a meta tag in index.html (ruling R16)
The proxy, its internal server, Unix socket and NO_PROXY changes are gone; main.ts serves the HTML
import directly again. apps/web/index.html carries the SPA policy as <meta http-equiv=CSP> ahead of every
script (script-src 'self' plus the hash of Bun's dev-server helper, no unsafe-inline/eval; no
frame-ancestors, which meta cannot carry). Dev HMR connects under it. The runner frame keeps its header CSP.

Tests: the served index.html (prod and dev, in a subprocess) has the meta policy and every inline script
is hash-covered; the XSS browser test fails with the sanitizer disabled and passes with it, and the meta
CSP alone blocks unsanitized handlers (with a no-CSP control); no source assigns proxy env vars, and the
feature leaves them untouched at runtime. Contract request 5 asks core for frame-ancestors/X-Frame-Options.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 14:38:04 +01:00
Lucas WintherandClaude Opus 5.5 213f2247b4 fix(learner): SPA proxy forwards no credentials and never goes through HTTP_PROXY
spaWithCsp proxies GET and HEAD only (405 otherwise, no body buffering) and forwards only accept,
if-none-match, if-modified-since and range. The internal server listens on a Unix socket in a private
0700 temp dir. Bun 1.3.14 ignores proxy: false for this hop, and over a socket with a proxy set it sends
an absolute-form target that breaks asset routing, so localhost, 127.0.0.1 and ::1 are appended to
NO_PROXY/no_proxy (existing entries kept). accept-encoding is no longer forced; fetch decompresses and
the encoding headers are dropped.

Test: in a subprocess with HTTP(S)_PROXY and ALL_PROXY set to a dead proxy, the SPA and an asset load
with the CSP, POST gets 405, and the upstream sees no Cookie, Authorization or X-Forwarded-For.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 14:27:56 +01:00
Lucas WintherandClaude Opus 5.5 c7b9e1f98e fix(learner): review round 1: sanitize-html, sandboxed runner frame, one visibility rule, no Tab capture
C1: lesson HTML goes through a sanitize-html allowlist on the fully rendered output (no on* attributes, no
style, no svg/math/iframe/object/embed/form; URLs limited to http(s), mailto, relative and anchors). Raw tags
outside the allowlist are escaped as text. The SPA document is served with a CSP (script-src 'self', no inline
script) through spaWithCsp in main.ts; contract request §5 asks core to own it.

C2: exercises run in <iframe sandbox="allow-scripts"> (opaque origin, no cookies) loaded from
/api/learner/runner-frame, whose CSP allows only its own script (by hash) and blob: workers, and connect-src
'none'. postMessage only, both sides check event.source; the timeout removes the frame. The worker stubs now
also cover Worker, SharedWorker and caches.

I1: learner visibility calls core canView with per-course authorship only, like /api/media (contract request §6).
I2: Tab and Shift+Tab move focus; Enter keeps the previous line's indentation.

Tests: XSS unit cases, headless-Chrome checks for the exploit lesson and for fetch, import, Cache.add, nested
Worker, img, EventSource, WebSocket, XHR and sendBeacon from learner and test code (plus a negative control),
visibility on both dialects, SPA and runner-frame CSP headers.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 05:20:43 +01:00
Lucas WintherandClaude Opus 5.5 061ae7a2d9 feat(learner): search results show lesson matches cleanly; contract notes for the browser entry and LessonView; plan marked done
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 04:54:53 +01:00
Lucas WintherandClaude Opus 5.5 89c51f013d feat(learner): web shell, library, lesson views (LessonView), decks, code files, exercises, notes, account and admin pages
Tasks 10-12 of the WP-A plan in one commit (the route modules import each other):
- header extras portalled into the Phase 0 header: course switcher, search, Reading/Work, account
- course frame with outline sidebar (mobile overlay, Escape closes), progress, gating locks
- library: featured course, continue row, New and collection rows, course list, filters, search
- LessonView: text (track switcher, folds, ordered hint tiers), video (resume, done near the end,
  timestamp notes, transcript), companion decks rendered with pdf.js (thumbs, arrows, fullscreen,
  per-slide notes), code files (tree, tabs, copy, zip, command), exercises (Web Worker runner or
  local command with optional pasted evidence), floating action bar
- notes list, account (email/password, providers, SSO by email) and owner admin pages
- pdf.js worker served from /api/learner/pdf.worker.mjs
- browser entry @stc/feature-learner/web (the package index also carries the server half)

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 04:53:36 +01:00
Lucas WintherandClaude Opus 5.5 f3ce2e34bf fix(ai): lessonUrl builder in one place; authoring follows core canView
- Learner links go through lessonUrl/courseUrl (src/web/urls.ts).
- Generation, the studio course list and file listing require an authoring
  right and core canView, so a global author needs a grant for a restricted
  course. Contract requests 8 (global author in canView) and 9
  (STC_AI_ALLOWED_BASE_URLS in AppConfig).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 04:49:48 +01:00
Lucas WintherandClaude Opus 5.5 ead7327bb1 fix(ai): solution and hidden-file guard runs on the canonical real path
read_file and read_lesson URL-decode, posix-normalise, reject .. and absolute
paths, resolve through CourseRepository.resolvePath (symlinks followed), then
guard the real course-relative path and compare real paths with every
manifest browser.solution. Hidden files are refused too.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 04:48:56 +01:00
Lucas WintherandClaude Opus 5.5 cfc41c6a63 fix(ai): stdio MCP servers get PATH, HOME, LANG and their configured env only
STC_SECRET, database URLs and API keys are no longer inherited by MCP child
processes. Test: a fake server reports its environment.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 04:47:47 +01:00
Lucas WintherandClaude Opus 5.5 a13cc66ebb fix(ai): BYOK endpoints limited to official presets and admin-allowed origins (SSRF)
- A user's own key goes only to https://api.anthropic.com, https://api.openai.com/v1
  or an origin listed in STC_AI_ALLOWED_BASE_URLS / the server config's
  allowedBaseUrls; anything else is a 400, and a stored key whose origin was
  later removed stops being used.
- User-scope provider calls never follow redirects, and their errors reach the
  user only as the HTTP status plus a fixed message; the full text is logged.
- The admin's server-scope provider may still use any base URL.
- Admin UI: allowed origins field; MCP stdio env editable, HTTP headers kept.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 04:47:25 +01:00
Lucas WintherandClaude Opus 5.5 2d162a4282 fix(studio): review round 1 (symlink-safe writes, remote allowlist, upload cap/types/no-overwrite, owner rule at publish/sync/resolve, studio/repos layout and read-only imports, side-effect-free list)
C1: writes lstat every path component and refuse symlinks (dangling included),
check the parent's real path after mkdir, and open with O_NOFOLLOW.
I1: only https://, ssh:// and git@host: remotes; protocol.file/ext/fd=never on
every fetch and push; global/system git config ignored.
I2: STC_UPLOAD_MAX_MB cap (default 100) by Content-Length and while streaming;
extension allowlist (415); stcourse.yaml refused; 409 unless overwrite=true;
sandboxing CSP on draft raw files.
I3: owner-only visibility/source.kind rule checked on the merged result at
publish and sync, and at resolve; publish also validates the merged tree.
I4: bare repos in <dataDir>/studio/repos/<id>.git; imported courses read-only.
I5: list status comes from the bare repo; drafts are created lazily.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 04:43:53 +01:00
Lucas WintherandClaude Opus 5.5 1f111b80db docs(ai): contract requests; dev fake provider returns a draft file
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 04:39:58 +01:00
Lucas WintherandClaude Opus 5.5 a47e6e0971 feat(ai): my_progress tool so the assistant sees the learner's progress
Spec 9.2 has the assistant retrieve from the learner's notes and progress;
progress was missing. Reads progressRepo(db).get for the signed-in user.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 04:37:25 +01:00
Lucas WintherandClaude Opus 5.5 6fb0019cd9 feat(ai): read the importer's deck and transcript cache; studio DraftWriter fits DraftSink as is
Retrieval now reads <dataDir>/cache/decks/<courseId>/<deckId>/text.json and
<dataDir>/cache/transcripts/<courseId>/<lessonId>.json (skipped when the lesson
has a transcript file). DraftSink returns { location?, branch?, commit? }, so
the studio's createDraftWriter(ctx) needs no adapter; generateDraft computes
paths itself and reports the draft branch.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 04:36:48 +01:00
Lucas WintherandClaude Opus 5.5 08d3e3f642 feat(ai): assistant panel, AI settings and studio generator UI
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 03:30:22 +01:00
Lucas WintherandClaude Opus 5.5 99c0e4929f feat(learner): lesson markdown pipeline with track variants, folds, hint tiers and safe HTML
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 03:29:14 +01:00
Lucas WintherandClaude Opus 5.5 667f3e814b fix(studio): forbid ext:: remotes; type the per-dialect test table
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 03:27:30 +01:00
Lucas WintherandClaude Opus 5.5 9ff5862382 feat(learner): JS/TS exercise runner in a Web Worker with timeout and network blocked
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 03:26:32 +01:00
Lucas WintherandClaude Opus 5.5 7adf9765b1 fix(import): allowed import roots, hostile zip symlink removal, tests
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 03:26:22 +01:00
Lucas WintherandClaude Opus 5.5 0992df7257 feat(studio): studio web UI (course list, manifest, lessons with live preview, decks, attachments, exercises, conflicts, sync) and dev entry
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 03:26:18 +01:00
Lucas WintherandClaude Opus 5.5 752e44696f fix(learner): zip buffer types
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 03:25:30 +01:00
Lucas WintherandClaude Opus 5.5 34826762a8 feat(learner): section code listing and zip download
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 03:25:18 +01:00
Lucas WintherandClaude Opus 5.5 a57222ecc3 feat(ai): HTTP routes, chat streaming and generation job
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 03:25:10 +01:00
Lucas WintherandClaude Opus 5.5 478ebeb255 fix(learner): narrow lesson target result for the type checker
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 03:24:28 +01:00
Lucas WintherandClaude Opus 5.5 96c147e3a2 feat(learner): progress, notes and exercise attempt API
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 03:24:16 +01:00
Lucas WintherandClaude Opus 5.5 5f85ce1d4b feat(learner): library, search, course outline with gating, lesson payload API
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 03:23:30 +01:00
Lucas WintherandClaude Opus 5.5 3cec42bba5 feat(ai): house-style generator writing drafts through a DraftSink
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 03:22:38 +01:00
Lucas WintherandClaude Opus 5.5 c4df51f7ca feat(import): UI polish, contract requests, report
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 03:22:22 +01:00
Lucas WintherandClaude Opus 5.5 a8a8e32bcb feat(learner): owner admin API for roles, groups, course access and library curation
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 03:20:49 +01:00
Lucas WintherandClaude Opus 5.5 90a477691b feat(import): admin UI at /admin/import, dev server, extraction cleanup
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 03:20:40 +01:00
Lucas WintherandClaude Opus 5.5 49d1df0f13 feat(ai): assistant prompt, course tools and tool loop
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 03:20:31 +01:00
Lucas WintherandClaude Opus 5.5 668ae379bf feat(learner): Better Auth sessions, owner bootstrap, claim groups; server takes feature factories
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 03:20:06 +01:00
Lucas WintherandClaude Opus 5.5 24cd6329cb feat(ai): MCP client over stdio and streamable HTTP
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 03:19:13 +01:00
Lucas WintherandClaude Opus 5.5 9f96e293e1 feat(import): wire feature into server and web shell; browser-safe web entry
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 03:18:26 +01:00
Lucas WintherandClaude Opus 5.5 497ce91216 feat(studio): /api/studio routes, author access, acceptance tests
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 03:18:25 +01:00
Lucas WintherandClaude Opus 5.5 50e1e49b6d feat(ai): BM25 course retrieval over lessons, transcripts, code, deck text and notes
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 03:18:21 +01:00
Lucas WintherandClaude Opus 5.5 9aa475329a feat(learner): Better Auth adapter over Bun.SQL for SQLite and Postgres
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 03:17:44 +01:00
Lucas WintherandClaude Opus 5.5 596099a13f feat(import): owner-only API routes and importFeature
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 03:17:31 +01:00
Lucas WintherandClaude Opus 5.5 7c3f613feb feat(import): optional transcription job
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 03:17:07 +01:00