Files
summercms/.planning/phases/12-p-ytarium-api-collections-and-albums/12-05-PLAN.md
Jakub Zych 947aabee93 docs: move golem15.user to sm-user-plugin and supersede the Phase 1 extraction deferral
- new note .planning/notes/core-plugins-own-repos.md: shared core plugins live in sm-<name>-plugin repos mounted as submodules
- 01-CONTEXT deferral points to the note; PROJECT constraint and Key Decisions row
- ROADMAP Phase 12 repos and the 12-01 entry, and Phase 12 plans 12-01, 12-02, 12-05 name sm-user-plugin and the submodule commit workflow
2026-10-02 11:02:07 +02:00

31 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, estimate, must_haves
phase plan type wave depends_on files_modified autonomous requirements estimate must_haves
12-p-ytarium-api-collections-and-albums 05 execute 5
12-04
../fonoteka.go/plugins/golem15/fonoteka/search_leak_test.go
../fonoteka.go/plugins/golem15/fonoteka/fake_engine_test.go
../fonoteka.go/plugins/golem15/fonoteka/routes_table_phase12_test.go
../fonoteka.go/plugins/golem15/fonoteka/write_endpoints_fuzz_test.go
../fonoteka.go/plugins/golem15/fonoteka/testdata/fuzz/FuzzWriteEndpoints/
../fonoteka.go/plugins/golem15/fonoteka/phase12_security_test.go
../fonoteka.go/plugins/golem15/fonoteka/classes/phase12_classes_test.go
../fonoteka.go/plugins/golem15/fonoteka/classes/album_helpers_test.go
../fonoteka.go/plugins/golem15/fonoteka/classes/search_test.go
../fonoteka.go/plugins/golem15/fonoteka/controllers/api/phase12_controllers_test.go
../fonoteka.go/plugins/golem15/user/classes/user_groups_test.go
../fonoteka.go/parity/check_corpus_test.go
modules/lagoon/validate_request_test.go
modules/lagoon/validate_rules_test.go
modules/lagoon/attach/url_test.go
modules/tide/multipart_test.go
modules/tide/normalize_upload_test.go
modules/beachcomber/searchpage_test.go
modules/beachcomber/typesense/searchpage_test.go
scripts/check-phase12.sh
.planning/phases/12-p-ytarium-api-collections-and-albums/12-SECURITY-REVIEW.md
.planning/phases/12-p-ytarium-api-collections-and-albums/12-VALIDATION.md
.planning/REQUIREMENTS.md
true
API-01
API-02
tokens raw_tokens tasks confidence
280000 280000 3 low
truths artifacts key_links prohibitions
Per D-18 and ROADMAP SC-3, with a scripted fake engine (no Typesense container) returning poisoned ids through beachcomber, GET albums/search returns none of: (1) a stale document whose album moved to a collection the caller cannot access, (2) a mis-scoped document carrying the caller's collection_id for a foreign album, (3) a soft-deleted album, (4) an album of a collection the caller was removed from as editor, (5) for a personal token pinned to one collection, hits from the user's other collections; on both auth groups.
Per D-19, the same leak test asserts meta.total and last_page never count a poisoned id (re-gated recount), found above 1000 caps the recount at 1000, and the inflated-found quirk of D-16 point 3 does not exist.
Per D-10 and D-26, a route-table test over the assembled router asserts that every `/api/v1/fonoteka` route carries exactly one `inv.scope:<read|write|ai>` equal to routes.php for that route, that switch, share, me/context, realtime/channels, household, invitations and sync are absent from the token group, that every `{id}`, `{fileId}` and `{collectionId}` path parameter has a `[0-9]+` constraint, and that the D-01 inline throttles sit on exactly their routes.
Per C-02 and ROADMAP SC-5, `FuzzWriteEndpoints` enumerates every write route of this phase from the route table (POST, PUT, DELETE, multipart included) and, for random extra keys and every server-owned key (id, owner_id, collection_id, kind, public_token, public_enabled, token_hash, user_id, market_price_source when not requested, created_at, updated_at, deleted_at, organisation_id), asserts no column outside the endpoint's allow-list changes in Postgres; its seed corpus runs in plain `go test`.
Each T-12-01..T-12-34 threat with disposition mitigate has a named test that fails when its protection is removed; `scripts/check-phase12.sh --removal` applies anchor-exact mutations to the protecting code, requires the named test to fail on an assertion, and restores the file byte for byte.
Every Go package created or changed in Phase 12 in both repos reaches at least 80% statement coverage under `go test -cover` (framework: lagoon request validation, lagoon/attach, tide, beachcomber, beachcomber/typesense; app: fonoteka classes, controllers/api, the user plugin classes and updates), with the per-package numbers recorded in 12-VALIDATION.md.
`scripts/check-phase12.sh --all` runs vet and tests in both repos, the parity corpus (99 ported, 0 failing), TestBroadcastGoldens, TestFonotekaNuxtFlows, check_corpus --require-recorded --check-secrets, TestDocsTree, the named tests by exact name (refusing skips and 'no tests to run'), the coverage floors and the evidence files, and `--self-test` proves each detector fails closed.
12-SECURITY-REVIEW.md maps every T-12 threat to its disposition, file and passing test; 12-VALIDATION.md has real task ids in its Per-Task Verification Map, no pending rows, and `nyquist_compliant: true`.
Edge (API-01 empty and API-02 boundary): table tests cover empty bodies on every write endpoint, the year, rating, cover_urls, per_page and file-size boundaries one step either side, and empty search pages.
statement verification
Edge (API-02 concurrency): fuzz and race runs over the write endpoints under `-race` show no data race in handlers or helpers; delivery order of separate broadcast jobs is not guaranteed, as with PHP's queued jobs. backstop
path provides contains
../fonoteka.go/plugins/golem15/fonoteka/search_leak_test.go TestSearchLeak with five poisoned cases and total assertions on both groups TestSearchLeak
path provides contains
../fonoteka.go/plugins/golem15/fonoteka/write_endpoints_fuzz_test.go FuzzWriteEndpoints enumerated from the route table FuzzWriteEndpoints
path provides
../fonoteka.go/plugins/golem15/fonoteka/routes_table_phase12_test.go TestRouteTablePhase12 scopes, absences, constraints, throttles
path provides
scripts/check-phase12.sh fail-closed Phase 12 gate with --self-test, --named, --removal, --coverage, --all
path provides
.planning/phases/12-p-ytarium-api-collections-and-albums/12-SECURITY-REVIEW.md threat-to-test evidence
from to via pattern
../fonoteka.go/plugins/golem15/fonoteka/search_leak_test.go ../fonoteka.go/plugins/golem15/fonoteka/classes/album_search.go fake engine registered with beachcomber.RegisterEngine and selected by search.driver RegisterEngine
from to via pattern
../fonoteka.go/plugins/golem15/fonoteka/write_endpoints_fuzz_test.go summercms.go modules/surf/router.go surf.BuildRouter(...).Routes() enumerates write routes Routes()
from to via pattern
scripts/check-phase12.sh .planning/phases/12-p-ytarium-api-collections-and-albums/12-VALIDATION.md evidence stage refuses pending or TBD rows 12-VALIDATION.md
requirement_id category statement status verification
API-02 privacy The leak test MUST NOT pass by disabling search or by asserting only status codes; it asserts the absence of each poisoned album in data and in meta.total resolved test
requirement_id category statement status verification
API-01 transparency A threat MUST NOT be marked mitigated in the security review without a named test that was run and seen to fail when the protection is removed resolved test

Phase Goal

ROADMAP Phase 12 goal (verbatim, not in user-story form): Collections and Albums endpoints are ported with byte-compatible request/response shapes, including active-context switching, editor invitations, ratings, reservations, cover handling and search. (12-01 Task 4 rewords it per D-03/D-04/D-06.)

This plan's slice: the project rule's last plan. It proves the phase's security properties (search leak, token scopes, mass assignment, the T-12 threats), brings full unit coverage to the Phase 12 code in both repos, adds the fail-closed Phase 12 gate, and signs off security and validation (API-01, API-02; ROADMAP SC-3, SC-5).

Write the D-18 leak test with D-19 total assertions, the route-table scope tests, the request-DTO fuzz over every write endpoint, failing-when-broken tests for every T-12 threat, full unit coverage for the phase's packages, `scripts/check-phase12.sh`, 12-SECURITY-REVIEW.md and the validated 12-VALIDATION.md.

Purpose: CLAUDE.md lean rule 3 (unit tests are always the last plan) and the security-review requirement for a phase touching authorization, public tokens and uploads. Decisions covered: D-10, D-18, D-19, D-26, C-02, and the evidence for every other D-NN through named tests. Output: tests in both repos, the gate script, the security review, the validated validation file, and the API-01/API-02 traceability update.

Repos: summercms.go (framework tests, gate script, planning docs) and fonoteka.go (app tests). Production code changes only where a test exposes a real bug; each such fix is its own commit naming the threat or decision. Planning docs and code in separate commits. Never add co-author tags.

<execution_context> @/.claude/gsd-core/workflows/execute-plan.md @/.claude/gsd-core/templates/summary.md </execution_context>

@.planning/PROJECT.md @.planning/STATE.md @.planning/phases/12-p-ytarium-api-collections-and-albums/12-CONTEXT.md @.planning/phases/12-p-ytarium-api-collections-and-albums/12-RESEARCH.md @.planning/phases/12-p-ytarium-api-collections-and-albums/12-VALIDATION.md @.planning/phases/12-p-ytarium-api-collections-and-albums/12-01-SUMMARY.md @.planning/phases/12-p-ytarium-api-collections-and-albums/12-02-SUMMARY.md @.planning/phases/12-p-ytarium-api-collections-and-albums/12-03-SUMMARY.md @.planning/phases/12-p-ytarium-api-collections-and-albums/12-04-SUMMARY.md @scripts/check-phase11.sh @.planning/phases/11-jobs-realtime-and-search-infrastructure/11-SECURITY-REVIEW.md - From 12-01..12-04 (see their SUMMARY files for final names): `lagoon.ValidateRequest`, tide `Parts` and upload masks, `beachcomber.SearchPage`, `beachcomber.RegisterEngine(name, EngineFactory)`, attach `URL`/`PublicURL`; app `classes.Resolve`, `AccessibleBy`, `AlbumsAccessibleBy`, `SearchAlbums`, `CreateAlbum`, `UpdateAlbum`, `SendInvitation`, `AcceptInvitation`, `RemoveEditor`, `GenerateShareToken`, `CollectionKey`, `IsAllowedImage`, `CoverImporter`, `FetchManualCover`; handlers and routes listed in the plans' Artifacts sections; seed hook `fonoteka`; `TestFonotekaNuxtFlows`. - Route table: `rt, _ := surf.BuildRouter(app, plugins); rt.Routes()` returns entries with Method, Pattern, Middleware []string, Raw (see routes_isolation_test.go TestFullRouteTableAuthGroupMutualExclusivity for the boot recipe: bootConfig, bouncer.NewRegistry, party.Activate of golem15.user and golem15.fonoteka, stubInvToken). - PHP route contract for scopes and throttles: /media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/routes.php (JWT group lines 74-330, token group 450-520). - Gate precedent: scripts/check-phase11.sh (stages, --self-test detectors, --named exact-name runs that refuse skip and no-tests, --removal anchor-exact mutation with byte-identical restore via cmp, evidence stage refusing pending rows) and 11-SECURITY-REVIEW.md (threat table with test names).

Artifacts this phase produces

(This plan's share.)

  • Tests: TestSearchLeak (subtests stale-moved, mis-scoped, soft-deleted, removed-editor, token-pin, total-cap, both groups), TestRouteTablePhase12, FuzzWriteEndpoints with a committed seed corpus, TestPhase12Threats (one subtest per T-12 id), package-level unit tests in both repos.
  • Gate: scripts/check-phase12.sh with --self-test, --go, --parity, --named, --removal, --coverage, --evidence, --all.
  • Docs: 12-SECURITY-REVIEW.md, validated 12-VALIDATION.md, REQUIREMENTS.md traceability rows API-01 and API-02 set to Complete.
Task 1: A poisoned search index cannot leak an album or a count to any caller, proven through the real search route on both auth groups Plan 12-04 is executed: `go -C ../fonoteka.go doc ./plugins/golem15/fonoteka/classes SearchAlbums` exits 0 and the parity corpus reports 99 ported routes. ../fonoteka.go/plugins/golem15/fonoteka/fake_engine_test.go, ../fonoteka.go/plugins/golem15/fonoteka/search_leak_test.go ../fonoteka.go/plugins/golem15/fonoteka/classes/album_search.go, ../fonoteka.go/plugins/golem15/fonoteka/controllers/api/album_search_controller.go, ../fonoteka.go/plugins/golem15/fonoteka/search.go (settingsGate), ../fonoteka.go/plugins/golem15/fonoteka/search_smoke_test.go and album_search_test.go (existing search harness), ../fonoteka.go/plugins/golem15/fonoteka/routes_isolation_test.go (assembled router recipe), ../fonoteka.go/plugins/golem15/fonoteka/plugin_boot_test.go (bootConfig, bootDB), summercms.go modules/beachcomber/engines.go (RegisterEngine), summercms.go modules/beachcomber/searchable.go, /media/nvme/dev/golem15/fonoteka/vendor/laravel/scout/src/Builder.php (lines 533-556), .planning/phases/12-p-ytarium-api-collections-and-albums/12-CONTEXT.md (D-16, D-18, D-19) (1) fake_engine_test.go: a test-only engine registered under a unique driver name with beachcomber.RegisterEngine in the test's init or TestMain (never in production code), implementing Engine and PageSearcher; each test scripts the ids (in order), found, and errors per call, and records every Query received (Q, QueryBy, QueryByWeights, FilterBy, SortBy, Page, PerPage) so tests can assert PHP's query_by order, weights and the collection_id filter.

(2) search_leak_test.go TestSearchLeak through the assembled router with search_use_typesense on, search.driver set to the fake, Postgres seeded with alice, bob, outsider and their collections, and a q that forces the Typesense path. Subtests, each asserting the response data ids and meta.total/last_page exactly: stale-moved (an album of alice moved to outsider's collection still returned by the engine with alice's filter: absent and not counted), mis-scoped (outsider's album id returned for alice's collection_id: absent, not counted), soft-deleted (alice's soft-deleted album id: absent, not counted), removed-editor (bob removed as editor of alice's collection, engine still returns alice's album ids for bob's request on his resolved collection: absent, not counted), token-pin (alice's token pinned to collection A, engine returns ids from alice's collection B: absent, not counted on the token group), short-page (page of 3 with one poisoned id returns 2 rows and total 2 when found equals 3), recount (found 300 over a per_page of 20: the second SearchPage calls use per_page 250 and the total is the re-gated count), cap (found 3000: no call asks beyond 1000 ids and total is at most 1000), engine-error (the fake errors: a warning is logged and the SQL path answers with only accessible rows). Run every case on both /_fonoteka/api/v1/albums/search and /api/v1/fonoteka/albums/search (read token). Assert the recorded Query has QueryBy in PHP order and weights 10,10,5,5,3,1,3,3. go -C ../fonoteka.go vet ./... && go -C ../fonoteka.go test ./plugins/golem15/fonoteka -run '^TestSearchLeak$' -count=1 -race -v <fails_when>Non-zero exit; output prints "no tests to run", "--- FAIL", "--- SKIP" or "DATA RACE", or lacks "--- PASS: TestSearchLeak/token-pin", "--- PASS: TestSearchLeak/cap" and "--- PASS: TestSearchLeak/removed-editor".</fails_when> <acceptance_criteria> - grep -c 't.Run(' ../fonoteka.go/plugins/golem15/fonoteka/search_leak_test.go prints at least 9. - grep -c 'meta' ../fonoteka.go/plugins/golem15/fonoteka/search_leak_test.go prints at least 1 and every subtest compares total, not only data. - grep -rln 'RegisterEngine' ../fonoteka.go/plugins/golem15/fonoteka --include=*.go lists only files whose names end in _test.go (the fake stays test-only). - Temporarily removing the AlbumsAccessibleBy scope from the re-gate query makes at least the stale-moved, mis-scoped and removed-editor subtests fail (checked by scripts/check-phase12.sh --removal in Task 3). </acceptance_criteria> Success criterion 3 is proven through the real route: no poisoned index entry can surface an album or a count to a JWT user or a pinned token.

Task 2: Every token route carries PHP's one scope, every write endpoint ignores server-owned keys, and every T-12 protection has a test that breaks without it ../fonoteka.go/plugins/golem15/fonoteka/routes_table_phase12_test.go, ../fonoteka.go/plugins/golem15/fonoteka/write_endpoints_fuzz_test.go, ../fonoteka.go/plugins/golem15/fonoteka/testdata/fuzz/FuzzWriteEndpoints/, ../fonoteka.go/plugins/golem15/fonoteka/phase12_security_test.go /media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/routes.php (both groups), ../fonoteka.go/plugins/golem15/fonoteka/routes.go, ../fonoteka.go/plugins/golem15/fonoteka/routes_isolation_test.go, ../fonoteka.go/plugins/golem15/fonoteka/routes_group_test.go, ../fonoteka.go/plugins/golem15/fonoteka/classes/album_write_service_fuzz_test.go and collection_write_service_fuzz_test.go (P5 service-level fuzz precedent), ../fonoteka.go/plugins/golem15/fonoteka/classes/album_write_service.go (AlbumFillFields), ../fonoteka.go/plugins/golem15/fonoteka/classes/collection_write_service.go (CollectionFillFields), the 12-02, 12-03 and 12-04 threat registers in their PLAN.md files, .planning/phases/12-p-ytarium-api-collections-and-albums/12-RESEARCH.md (Security Domain) - TestRouteTablePhase12: a token route with zero or two inv.scope entries fails; a scope that differs from routes.php fails; any of switch, collection/share, me/context, realtime/channels, household/*, invitations/*/accept, albums/sync on /api/v1/fonoteka fails; an unconstrained {id} fails; throttle:10,1 exactly on switch, share regenerate, invitation store and resend; throttle:20,1 exactly on the JWT album photo upload; throttle:60,1 exactly on sync. - FuzzWriteEndpoints: for each enumerated write route, a request body made of a valid base plus fuzzed extra keys and values never changes owner_id, collection_id, kind, public_token, public_enabled, token_hash, user_id or timestamps beyond what the endpoint sets itself, and never panics (500 is a failure). - TestPhase12Threats: one subtest per mitigated T-12 id asserting the protection (IDOR 404 parity for foreign and missing ids on every id route, token pin on stats/value/search/artists/genres, editor 404 on owner-only routes, accept 410 for reused, expired, revoked and wrong-email tokens, share token alphabet and uniqueness, SSRF refusals for private and redirecting URLs, polyglot upload refusal, raw invite token absent from river_job.args and captured logs, provisioning race, Winter pages without debug text, notification recipients). (1) routes_table_phase12_test.go `TestRouteTablePhase12`: boot the assembled router as routes_isolation_test.go does; hold an explicit table of the 66 Phase 12 routes plus genres and me with method, pattern, group, expected scope (token group) and expected throttle, transcribed from routes.php with the line number in a comment per entry; assert every expectation in the behavior list and that the table matches the router (no missing and no unexpected Phase 12 route).

(2) write_endpoints_fuzz_test.go FuzzWriteEndpoints: enumerate POST/PUT/DELETE routes from rt.Routes() under the two Phase 12 prefixes (exclude routes outside this phase by an explicit allow-list of Phase 12 patterns), give each a valid base body (JSON or multipart with a tiny PNG) and the set of columns it may change, then for fuzz input add keys and values (including every server-owned key name, nested objects, arrays, numbers as strings, very long strings) and assert, by snapshotting the affected rows before and after in Postgres, that only allowed columns changed and that the response is not 500. Commit a seed corpus under testdata/fuzz/FuzzWriteEndpoints/ so plain go test runs every route with the server-owned keys. Reset the DB state per iteration with a savepoint or a fresh seed.

(3) phase12_security_test.go TestPhase12Threats with subtests named by threat id (T-12-01 ... T-12-34 for every mitigated threat in plans 12-01 to 12-04) implementing the behavior list. Use the fake Centrifugo/memory drivers, the postcard memory driver, and a slog handler capturing logs. Any genuine bug found is fixed in production code in its own commit naming the threat. go -C ../fonoteka.go vet ./... && go -C ../fonoteka.go test ./plugins/golem15/fonoteka -run '^(TestRouteTablePhase12|FuzzWriteEndpoints|TestPhase12Threats)$' -count=1 -race -v && go -C ../fonoteka.go test ./plugins/golem15/fonoteka -run '^$' -fuzz '^FuzzWriteEndpoints$' -fuzztime 60s <fails_when>Any command exits non-zero; the verbose run prints "no tests to run", "--- FAIL", "--- SKIP" or "DATA RACE", or lacks "--- PASS: TestRouteTablePhase12", "--- PASS: FuzzWriteEndpoints" and "--- PASS: TestPhase12Threats"; the fuzz run prints "Failing input written to".</fails_when> <acceptance_criteria> - grep -c 'routes.php' ../fonoteka.go/plugins/golem15/fonoteka/routes_table_phase12_test.go prints at least 1 and the expectation table has an entry per Phase 12 route. - ls ../fonoteka.go/plugins/golem15/fonoteka/testdata/fuzz/FuzzWriteEndpoints/ lists at least one seed file per enumerated write route. - grep -c 't.Run("T-12-' ../fonoteka.go/plugins/golem15/fonoteka/phase12_security_test.go prints at least 15. - The fuzz target fails when CollectionFillFields temporarily gains owner_id (checked by --removal in Task 3). </acceptance_criteria> The token surface, the mass-assignment boundary of every write endpoint and every Phase 12 threat are pinned by tests that fail when the protection is removed.

Task 3: Phase 12 code is fully unit tested in both repos and a fail-closed gate, the security review and the validation file sign it off modules/lagoon/validate_request_test.go, modules/lagoon/validate_rules_test.go, modules/lagoon/attach/url_test.go, modules/tide/multipart_test.go, modules/tide/normalize_upload_test.go, modules/beachcomber/searchpage_test.go, modules/beachcomber/typesense/searchpage_test.go, ../fonoteka.go/plugins/golem15/fonoteka/classes/phase12_classes_test.go, ../fonoteka.go/plugins/golem15/fonoteka/classes/album_helpers_test.go, ../fonoteka.go/plugins/golem15/fonoteka/classes/search_test.go, ../fonoteka.go/plugins/golem15/fonoteka/controllers/api/phase12_controllers_test.go, ../fonoteka.go/plugins/golem15/user/classes/user_groups_test.go, ../fonoteka.go/parity/check_corpus_test.go, scripts/check-phase12.sh, .planning/phases/12-p-ytarium-api-collections-and-albums/12-SECURITY-REVIEW.md, .planning/phases/12-p-ytarium-api-collections-and-albums/12-VALIDATION.md, .planning/REQUIREMENTS.md scripts/check-phase11.sh (whole script: stages, detectors, --named, --removal, evidence), .planning/phases/11-jobs-realtime-and-search-infrastructure/11-SECURITY-REVIEW.md, .planning/phases/11-jobs-realtime-and-search-infrastructure/11-VALIDATION.md (validated map format), .planning/phases/12-p-ytarium-api-collections-and-albums/12-VALIDATION.md, the four Phase 12 SUMMARY files (final test names and files), modules/phrasebook/lang/pl/validation.yaml, /media/nvme/dev/golem15/fonoteka/vendor/laravel/framework/src/Illuminate/Validation/Concerns/ValidatesAttributes.php (1) Framework unit coverage (summercms.go, neutral names): validate_rules_test.go table over every supported rule with pass, fail, absent, null-with-nullable, blank-string and type-variant cases, every size message variant, wildcard expansion with nested arrays and missing parents, bail and implicit stop, custom rules, catalog fallback pl to en, the min/max/between fix; attach URL and webp edge cases; tide multipart (empty parts, value-only parts, unicode filenames, missing file, sha mismatch, Body plus Parts) and upload-mask negatives (wrong prefix, partition, size, mode); beachcomber fallback, weights length mismatch, per_page above 250, found decoding errors.

(2) App unit coverage (fonoteka.go): classes (resolver branches including a pin of zero, two ids, a non-integer and an inaccessible id; provisioner; gates matrix; share service; fingerprint against a php -r computed value; invitation normalization boundaries; notification payloads; album helpers: tracklist parser lines and 201-line refusal, added-date formats and MIN_DATE, duplicate matcher Polish folding, completeness tags, barcode normalization; cover importer and manual fetcher with injected fetchers; search SQL escaping and filter strings; sync cursor encode/decode and malformed cursors; stats, value formatting and missing counters), controllers/api helpers (decodeInput for JSON, form, multipart and empty bodies; phpInt; laravelBoolean; writeWinterHTTPError for 404, 409, 410 with app.url variants; writeValidationFailed), user plugin UserGroupCodes and HasGroupCode, check_corpus's invitation-token rule.

(3) scripts/check-phase12.sh modelled on check-phase11.sh: stages --go (vet and test both repos), --parity (TestParityCorpus with 99 ported and 0 failing parsed from the coverage line, TestBroadcastGoldens all four passing, TestFonotekaNuxtFlows both subtests, check_corpus --require-recorded --check-secrets), --named (every test named in 12-VALIDATION.md run by exact name with -run '^Name$', refusing skip, fail and "no tests to run"), --removal (anchor-exact mutations restored byte for byte with cmp: drop AlbumsAccessibleBy from the search re-gate, drop the token narrowing in AccessibleBy, drop the owner check in share, add owner_id to CollectionFillFields, drop the sha256 lookup in accept, swap PublicOnly for no policy in the manual fetcher, make the invitation args carry the plaintext token; each must make its named test fail on an assertion), --coverage (go test -coverprofile per listed package; refuse any package below 80% with its number), --evidence (12-SECURITY-REVIEW.md lists every T-12 id with a passing test, 12-VALIDATION.md has no pending or TBD row and nyquist_compliant true), --all, and --self-test proving each detector fails closed on planted inputs. It never names the application in framework-facing output beyond the paths it must call.

(4) Planning docs (separate commit): 12-SECURITY-REVIEW.md (reviewer note: self-performed by the executor if no reviewer agent can be spawned, per the 08-10 precedent; table of T-12-01..T-12-34 and T-12-SC with category, severity, disposition, protecting file and the test name that was run and seen failing under --removal); 12-VALIDATION.md: replace the seeded Per-Task Verification Map rows with the final rows (task ids 12-01-T1..12-05-T3, the exact commands each plan ran, file-exists ticks, statuses), tick Wave 0 items, set status validated, nyquist_compliant true, wave_0_complete true; REQUIREMENTS.md traceability rows API-01 and API-02 set to Complete and their checkboxes ticked. The user plugin test file lives in the sm-user-plugin submodule; commit it there and push its master, then commit the bumped pointer in fonoteka.go with the other fonoteka.go test changes. scripts/check-phase12.sh --self-test && scripts/check-phase12.sh --all <fails_when>Non-zero exit; output contains "refuse:" or "FAIL", a package coverage line below 80%, a named test reported skipped or with "no tests to run", a --removal mutation whose named test still passes, or the evidence stage reporting a pending row or a T-12 id without a test.</fails_when> <acceptance_criteria> - test -x scripts/check-phase12.sh succeeds and scripts/check-phase12.sh --self-test exits 0. - grep -c 'nyquist_compliant: true' .planning/phases/12-p-ytarium-api-collections-and-albums/12-VALIDATION.md prints 1 and grep -c '| TBD |' .planning/phases/12-p-ytarium-api-collections-and-albums/12-VALIDATION.md prints 0. - Every T-12 id defined in 12-01..12-04 threat registers appears in 12-SECURITY-REVIEW.md (grep -o 'T-12-[0-9]*' ... | sort -u matches the union from the four plans). - grep -n 'API-01 | Phase 12' .planning/REQUIREMENTS.md and grep -n 'API-02 | Phase 12' .planning/REQUIREMENTS.md show Complete. - The coverage stage prints one line per listed package with a value of at least 80%. </acceptance_criteria> Phase 12 is closed by evidence: full unit coverage in both repos, a gate that fails closed on any regression, a security review tying each threat to a test, and a validated validation file.

<threat_model>

Trust Boundaries

Boundary Description
Test harness → production code Tests and the gate must not weaken or bypass the protections they check
Gate script → tracked source --removal edits tracked files temporarily and must restore them exactly
Fuzz corpus → git Seed inputs are committed and must hold no secrets

STRIDE Threat Register

This plan verifies every threat registered by plans 12-01 to 12-04 (T-12-01 to T-12-24 and T-12-28 to T-12-34): TestSearchLeak covers T-12-02 and T-12-28, TestRouteTablePhase12 covers T-12-03, T-12-04, T-12-31 and T-12-34, FuzzWriteEndpoints covers T-12-11 and T-12-30, and TestPhase12Threats has one subtest per remaining id. The rows below are the threats this plan itself introduces.

Threat ID Category Component Severity Disposition Mitigation Plan
T-12-25 Tampering gate --removal leaving mutated source medium mitigate Anchor-exact mutation, cmp byte-identical restore, trap on exit, refuse to run on a dirty tree (Task 3).
T-12-26 Repudiation security review claims without evidence medium mitigate Evidence stage refuses a T-12 id without a named, executed, removal-proven test (Task 3).
T-12-27 Information Disclosure fuzz seed corpus low mitigate Seeds use synthetic values only; check_corpus-style secret scan over testdata/fuzz in the gate (Task 3).
T-12-SC Tampering package installs low accept No new dependency in this plan.
</threat_model>
- `scripts/check-phase12.sh --all` exits 0 and `--self-test` exits 0. - Both repos: `go vet ./... && go test ./... -count=1` green; `go test ./cmd/summer -run TestDocsTree -count=1` green. - 12-VALIDATION.md validated; 12-SECURITY-REVIEW.md complete; API-01 and API-02 Complete in REQUIREMENTS.md.

<success_criteria>

  • ROADMAP SC-3 (search leak including total) and SC-5 (request-DTO fuzz over every write endpoint) are proven by named tests.
  • Every Phase 12 package in both repos is at or above 80% coverage; the gate fails closed.
  • Security review and validation sign-off are complete with real task ids. </success_criteria>
Create `.planning/phases/12-p-ytarium-api-collections-and-albums/12-05-SUMMARY.md` when done.