Serialize public sites through an explicit field allowlist
Summary
Replaces the public Site serializer's denylist (delete snapshot.brandWorld; delete snapshot.production) with an explicit allowlist, as required by #2133 Scope ("Use an explicit allowlist for public Site serialization before storing new private context references") and the acceptance item "Public/draft/preview access tests prove private context and raw traces never leak via published JSON". New lib/sites/serializePublicSiteSnapshot.ts copies only name, artistName (derived from release context before that context is dropped), releaseUrl, assets[].{url,name,type} and design (field-by-field, including experience.{html,css,javascript}). Saved brief/request IDs, evidence documents, gaps, creative direction, reviews, brand-world specification, asset generation provenance/rationale and any key added later (for example YouTube/social-era documents from #2131/#2132) are excluded by default instead of removed by name. The RED test showed the previous code leaking assets[].generation.rationale and an unknown futurePrivateField. processPublicSite keeps signup, fanConnectUrl and getSitePlaybackAudio(site) unchanged; playback still reads the stored row, never the public copy. The chat renderer already consumes only the allowlisted fields, so no client change is needed.
Refs recoupable/app#2133 (child of epic recoupable/app#2116).
Merge sequencing
api only (no docs, database or app change). Independent of the OPEN Sites PRs: api#946 (private player preview) touches getSitePlaybackAudio.ts, processSiteOperation.ts and appends a README subsection after "Build progress"; this PR touches neither file and anchors its README paragraph in "Saved Context Engine handoff". api#962 (repair recovery) and api#935 (concept quality) touch workflow/director files that are untouched here. Once api#946 lands, its getSitePreview can reuse serializePublicSiteSnapshot for draft previews if a preview snapshot is ever returned (today it returns only a token and playback URL).
Verification
- Ran:
pnpm exec vitest run lib/sites/__tests__/publicSiteAllowlist.test.ts lib/sites/__tests__/publicSite.test.ts→ 2 files, 9 passed (4 new + 5 existing), 0 failed - Ran:
pnpm exec vitest run lib/sites/__tests__→ 44 files passed, 3 skipped (live-only); 207 tests passed, 4 skipped - Ran:
pnpm exec vitest run lib/context→ 67 files passed, 8 skipped (live/local); 326 tests passed, 8 skipped - Ran:
pnpm exec eslinton the three changed TS files → 0 problems;pnpm exec prettier --checkon the four changed files → clean - Ran:
grep -c 'delete snapshot' lib/sites/processPublicSite.ts→ 0 - Ran:
pnpm exec tsc --noEmit→ 199 pre-existing errors in 65 files on origin/main (mostlylib/tasks,lib/trigger,lib/chatstest fixtures); 0 errors in the four changed files; the two pre-existinglib/sitestest errors (buildProgress.test.ts,publishAction.test.ts) do not import the changed modules - Red → green:
publicSiteAllowlist.test.tsfirst failed withCannot find module '../serializePublicSiteSnapshot'; after adding the serializer alone, theprocessPublicSitecase still failed with the stored row leakingassets[].generation.{provider,model,requestId,rationale,durationMs}andfuturePrivateField: "PRIVATE_FUTURE_FIELD"; it went green afterprocessPublicSiteswitched to the allowlist - Not verified: hosted deployment, live providers, production database, the real published-site page in a browser. State is implemented + fixture-tested only.
Out of scope / known gaps
- The reversible rollout switch for the engine-backed
collectReleaseContextpath (#2133 Scope item 4) is not built here; it needscollectReleaseContext.ts/siteProductionWorkflow.ts, which api#962 is editing concurrently. Follow-up slice. - The director input-manifest assertion (selected branding text in, raw artwork out) is owned by sibling #2130 this run.
- Draft preview serialization is governed by api#946 (OPEN); this PR does not change preview routes.
- Blocking deps #2131/#2132 remain open; their new document topics are excluded from public JSON automatically by this allowlist.
- No OpenAPI schema exists for the public Sites response; a docs note is deferred to the #2134 release docs.
🤖 Generated with Claude Code
Summary by cubic
Follows the #2133 scope requirement by switching the public Site serializer from a denylist to an explicit allowlist so private fields can no longer reach published JSON by default.
processPublicSite previously spread the published snapshot and deleted brandWorld and production by name; any other key stored on a published row — such as asset generation rationale or a future Context Engine reference — leaked into the public payload. New serializePublicSiteSnapshot copies only name, artistName (derived from release context before that context is dropped), releaseUrl, assets[].{url,name,type}, and design. Playback audio still reads the stored row, so signed recordings are unaffected, and the chat renderer already consumes only these fields. Legacy rows without production fall back to the stored artist name. Tests cover a snapshot carrying every private field plus an unknown one and prove none of it surfaces.
Out of scope
- The reversible rollout switch for the engine-backed context path is a follow-up slice.
- Draft preview serialization is unchanged and governed by the open #946 PR; this PR is api-only and touches no shared files.