# Contract requests from WP-A (learner) ## 1. Feature factories and the auth service (`apps/server/src/main.ts`, `@stc/contracts`) **Problem.** Phase 0 passes `Feature[]` to `startServer`, but `ServerContext` is built inside `startServer`, so a feature that needs `ctx` (every Phase 1 package exports `xFeature(ctx)`) cannot be listed. There is also no way for a feature to replace the anonymous `AuthService`. **Workaround in `feat/learner` (applied, small):** `startServer` accepts `FeatureEntry = Feature | ((ctx) => Feature & { auth?: AuthService })`. Factories run after `ctx` exists (with `ctx.jobs` set right after they return, so factories must not use `jobs` synchronously), and a feature carrying `auth` replaces `ctx.auth`. The `main` entry lists `learnerFeature`. **Proposal.** Move both into the contract: ```ts export type FeatureFactory = (ctx: ServerContext) => Feature; export interface Feature { /* … */ auth?: AuthService; migrationsDirs?: string[] } ``` and let `startServer` take `Array`. ## 2. Migrations per package `migrate()` only reads `packages/db/migrations`. The learner package exports `migrationsDirs` and, until Phase 2 wires it in, runs its own migrations lazily inside `learnerFeature(ctx)` through `runMigrations(db, dirs)`, which uses the same `schema_migrations` table and file-name keys. When `migrate()` learns about `migrationsDirs`, the lazy call becomes a no-op; it can then be removed. ## 3. Package entry for the browser **Problem.** The package index exports both the server (`learnerFeature`, which pulls in Better Auth and `@better-auth/sso`) and the browser half (`learnerWeb`, `LessonView`). A browser bundle of the index fails: `@better-auth/sso` imports `X509Certificate` from `node:crypto`, which has no browser polyfill. The root `tsconfig.json` maps `@stc/*` to `packages/*/src`, so package.json `exports` subpaths are not used either. **Workaround in `feat/learner` (applied).** `LessonView` and `learnerWeb` are exported from the index (for server-side and type use) and from a browser-only entry, `@stc/feature-learner/web`. That subpath resolves through the existing mapping to `packages/feature-learner/web/src/index.ts`, a one-line re-export of `src/web`. `apps/web/src/features.ts` imports `learnerWeb` from it, and the studio should import `LessonView` from it too. **Proposal.** Add `"@stc/*/web": ["packages/*/src/web"]` ahead of the general mapping in `tsconfig.json` (Bun honours tsconfig paths), and let every feature package expose `src/web/index.ts`. The shim directory can then be deleted. This also settles the importer's relative import in `features.ts`. ## 4. LessonView for the studio preview `import { LessonView } from "@stc/feature-learner/web"`. Props: `lesson: LessonPayload` (the JSON from `GET /api/courses/:id/lessons/:lessonId`), plus optional `look`, `track`, `onTrackChange`, `preview`. With `preview` set, it makes no notes, attempt or progress requests; it still loads code listings and decks through the normal media routes. It injects its own stylesheet (scoped under `.lr`) and works with or without a router. ## 5. Content-Security-Policy on the SPA (review C1, ruling R16) **Applied in `feat/learner`.** The SPA's CSP is a `` tag in `apps/web/index.html`, ahead of every script. `Bun.serve` serves the HTML import directly, as in Phase 0; there is no proxy and no change to proxy environment variables. The policy: ``` default-src 'self'; script-src 'self' 'sha256-'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https:; media-src 'self' blob:; font-src 'self' data:; connect-src 'self'; worker-src 'self' blob:; frame-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self' ``` - **No inline script.** - The one hash is the inline helper Bun's dev server injects (the `/_bun/unref` beacon). Production serving does not inject it. - If a Bun upgrade changes the helper, the only effect is a dev-only CSP console error; `test/csp.test.ts` (dev mode) will then fail and name it. - **Dev HMR works:** the HMR websocket `ws:///_bun/hmr` is same-origin, and Chrome matches it with `connect-src 'self'` (checked: handshake 101, no violations). - **The runner frame** (`/api/learner/runner-frame`) keeps its own header-based CSP, including `sandbox allow-scripts` and `frame-ancestors 'self'`. **Proposal for core (Phase 2).** - A meta tag cannot carry `frame-ancestors`, `sandbox` or `report-uri`. Core should additionally send `Content-Security-Policy: frame-ancestors 'self'` (or `X-Frame-Options: SAMEORIGIN`), plus `X-Content-Type-Options: nosniff`, on HTML responses. That is possible if Bun ever allows headers on HTML imports, or by serving a production static build (`bun build apps/web/index.html`) through a normal route that sets headers. - Ideally core then sends the whole policy as a header and the meta tag stays as defence in depth. - Every feature's web code must keep to this policy: no inline scripts or event handlers, and no `eval`. The learner and studio preview, through `LessonView`, comply. ## 6. `canView` and the global `author` role (review I1) **Ruling applied.** Learner routes now call core `canView` with per-course authorship only (`course_authors`), exactly like core's `/api/media`. A user with only the global `author` role gets a 404 for a restricted course they are not listed on, on every route. **Proposal for Phase 2.** Extend core `canView` so the global `author` role can view every course (the studio and AI need the same rule), for example `canView(user, course, acl, isCourseAuthor)` returning true when `user.roles.includes("author")`. The learner then needs no change: it already delegates to `canView`.