Review fix round 1.
- Secret box v2 tokens: one HKDF key per purpose (byok, server-api-key,
mcp) and AES-GCM AAD bound to the row (user id + provider; server id +
field). MCP env and headers are sealed per field. open() refuses v1
tokens; the data upgrade re-seals v1 keys, v1 plaintext MCP values and
the draft secretCipher, and marks ai_secrets_v2 only when nothing
legacy is left (retried until the previous secret is back).
- Unreadable secrets are reported (admin: unreadableSecrets with server
and field; user: byok.keyUnreadable), logged at startup, and kept on
save unless replaced or cleared (clearApiKey, clearSecrets). The admin
and account pages show the warnings.
- allowedOrigins and provider resolution no longer open MCP secrets.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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 <noreply@anthropic.com>
- 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 <noreply@anthropic.com>