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]>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
69aaedcb43
commit
8045c80d93
@@ -1,6 +1,6 @@
|
||||
import type { FeatureFactory, ServerContext } from "@stc/contracts";
|
||||
import type { PackageMigrations } from "@stc/db";
|
||||
import { aiFeature, createFsDraftSink, migrationsDirs as aiMigrations, type DraftSink } from "@stc/feature-ai";
|
||||
import { aiFeature, createFsDraftSink, migrationsDirs as aiMigrations, type AiFeatureOptions, type DraftSink } from "@stc/feature-ai";
|
||||
import { importFeature, migrationsDirs as importMigrations } from "@stc/feature-import";
|
||||
import { learnerFeature, migrationsDirs as learnerMigrations, userIdentity } from "@stc/feature-learner";
|
||||
import { createDraftWriter, migrationsDirs as studioMigrations, studioFeature, type StudioFeature } from "@stc/feature-studio";
|
||||
@@ -30,15 +30,24 @@ export function aiDraftSink(ctx: ServerContext, studio: Pick<StudioFeature, "sto
|
||||
export interface FeatureSetOptions {
|
||||
/** Studio published-worktree polling (default on). */
|
||||
studioWatch?: boolean;
|
||||
/** Test seam: replaces AI provider construction (e.g. a canned provider). Production leaves it unset. */
|
||||
aiProviderFactory?: AiFeatureOptions["providerFactory"];
|
||||
}
|
||||
|
||||
/** The product: learner (auth, library, reader), importer, studio and AI, in that order. */
|
||||
/**
|
||||
* The product: learner (auth, library, reader), importer, studio and AI, in that order.
|
||||
* Order matters: the AI factory needs the studio feature (its draft sink writes through the studio store), so the
|
||||
* studio factory must run first. composeServer runs factories in list order; the AI factory checks this.
|
||||
*/
|
||||
export function productFeatures(opts: FeatureSetOptions = {}): FeatureFactory[] {
|
||||
let studio: StudioFeature | null = null;
|
||||
return [
|
||||
learnerFeature,
|
||||
(ctx) => importFeature(ctx),
|
||||
(ctx) => (studio = studioFeature(ctx, { watch: opts.studioWatch ?? true })),
|
||||
(ctx) => aiFeature(ctx, { draftSink: aiDraftSink(ctx, studio!) }),
|
||||
(ctx) => {
|
||||
if (!studio) throw new Error("productFeatures: the studio factory must run before the AI factory");
|
||||
return aiFeature(ctx, { draftSink: aiDraftSink(ctx, studio), ...(opts.aiProviderFactory ? { providerFactory: opts.aiProviderFactory } : {}) });
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ export async function startServer(config: AppConfig, features: (Feature | Featur
|
||||
return {
|
||||
server,
|
||||
ctx: composed.ctx,
|
||||
features: composed.features,
|
||||
async stop() {
|
||||
await server.stop(true);
|
||||
await composed.stop();
|
||||
|
||||
Reference in New Issue
Block a user