Commit Graph
90 Commits
Author SHA1 Message Date
Lucas WintherandClaude Opus 5.5 635fd66d04 feat(ui): scroll keeper with deferred settle
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 20:33:44 +01:00
Lucas WintherandClaude Opus 5.5 b1d17956f4 feat(ui): view-transition helper and held-value hook
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 20:32:57 +01:00
Lucas WintherandClaude Opus 5.5 17b6ae3828 feat(ui): motion tokens, keyframes and reduced-motion switch
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 20:32:21 +01:00
Lucas WintherandClaude Opus 5.5 eed59d4ef2 docs: v1.1 M navigation motion plan
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 20:31:36 +01:00
Lucas WintherandClaude Opus 5.5 fd5c72afc2 docs: v1.1 plan (motion, studio import editing, perf, AI schema)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 20:21:39 +01:00
Lucas WintherandClaude Opus 5.5 58e0b20c95 merge: Phase 1 feature packages and Phase 2 integration
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 15:51:19 +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 69aaedcb43 build: docker image and compose
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]>
2026-10-30 15:17:58 +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 d8d62696da merge: learner
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 14:40:29 +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 0ff8e6a5de docs(ai): mark plan tasks done
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 04:40:02 +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 2849d3c911 docs(studio): contract requests (ctx feature factories, worktrees course dir, LessonView swap, web subpath, nav roles, DraftWriter)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 03:26:58 +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