20 KiB
phase, plan, subsystem, tags, requires, provides, affects, actuals, tech-stack, key-files, key-decisions, patterns-established, requirements-completed, coverage, duration, completed, status, plan_head_before, plan_head_after
| phase | plan | subsystem | tags | requires | provides | affects | actuals | tech-stack | key-files | key-decisions | patterns-established | requirements-completed | coverage | duration | completed | status | plan_head_before | plan_head_after | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 13-p-ytarium-api-wishlist-notifications-csv-credentials-public | 05 | api |
|
|
|
|
|
|
|
|
|
|
|
34min | 2026-10-03 | complete | c540102eea33d68733ee4a5366be28c035954255 | 3c8b7cd25ac996921a745fa7e98e3a81414c7072 |
Phase 13 Plan 05: Public share routes Summary
Anyone with a shared link can now open, search and browse a collection or a wishlist in Go, with PHP's bodies and exact headers. The six anonymous routes resolve tokens through ResolvePublic. They search with the public text fields only, return facets narrowed to the shared collection, and serialize albums with the twelve-key public allow-list. Link guessing is throttled by the pubfail:<ip> counter shared by all six routes and by the per-route D-14 buckets. The corpus is at 157 ported routes, and the anonymous journey and the lockout flow replay green.
Performance
- Duration: 34 min
- Started: 2026-10-03T07:55:27Z
- Completed: 2026-10-03T08:29:30Z
- Tasks: 3 of 3
- Files modified: 53 in fonoteka.go: 15 source and test files, 36 route fixtures and 2 flows
Accomplishments
-
Resolve (Task 1, tracer).
GET public/{token}(and, in Task 2,public-wishlist/{token}) runs in this order:- the pubfail check, before any database access;
ResolvePublicfor the route's kind;{"data":{"state":"active","collection":{"name","album_count"}}}, or the one 404 body{"error":"Not found"}.
A failed resolution counts one failure.
-
Headers.
PublicShareHeadersnow sendsCache-Control: no-store, private, the value Symfony puts on the wire. It keepsX-Robots-Tagand the JSON 429 rewrite with Retry-After and X-RateLimit-*. -
PubfailCounter. A fixed window per key starts at the first failure and lasts 60 s.
TooManypeeks at the count,Hitadds one failure.Beginreserves a slot for the request in flight, so concurrent failures cannot pass the limit.- Each app gets its own counter.
-
Search (Task 2).
AlbumSearchParams.Publicselects PHP's public path:- the shared collection is the only tenant rule;
- there is no ratings join or rating filter;
- rating and price are not sorts;
- the text fields are
TextFieldsPublic(query_byname,artist_display,style_names,genre_name,track_titles, weights10,10,5,5,3, LIKE on name, artist_display, track_titles and artists.name); - engine ids are re-gated in SQL to that collection.
The authenticated set is now
TextFieldsAuthenticated, with the same values as before. -
Index. The index validates the query with PHP's rules in PHP's order. A rating gets the closure message, and the 422 envelope is
{"error":"Validation failed","errors":...}. Only the rule keys reach the search. The body is the page,meta, the facets (artists without Various Artists, genres, decades, formats; zero rows dropped,[]when empty) and the header. -
Show. Show returns
{"data":PublicAlbumDTO}for an album of the shared collection. A missing, foreign, soft-deleted or out-of-int4-range id answers 404 and is not counted as a failed resolution. -
Public payload.
PublicAlbumDTOis a type of its own with the twelve keys in PHP order. No rating or reservation is loaded or emitted. -
Routes (D-14). The group carries
public.share-headersonly. The resolve routes carry exactlythrottle:10,1. The albums routes carry exactlythrottle:fonoteka-public-tokenthenthrottle:fonoteka-public-ip. No route regex constrains{token}. -
Parity.
- The
publicstate exists on both sides (withalbums): alice's collection and wishlist are shared, bob's share is disabled, and bob has reserved the wishlist album. - 6 routes are re-recorded with 32 cases, so 157 routes are ported and 14 are pending.
- The flows
public-anonymous(12 steps, one authenticated) andpublic-pubfail(10 × 404, then 429) replay green. - The README documents the recipes.
- The
Task Commits
fonoteka.go (master, not pushed):
- Task 1: resolve shared collection links anonymously with PHP's headers and lockout -
990e311(feat) - Task 2: browse, search and open shared collections and wishlists anonymously -
6278e2b(feat) - Task 3: replay the anonymous share journey and the link-guessing lockout -
3c8b7cd(test)
Decisions Made
See key-decisions. One may need the user's attention. The pubfail counter is in memory and per process. PHP shared it through the file cache across the workers of one host, so a deployment with several Go instances multiplies the guessing budget by the instance count. A shared store (Postgres or Redis) would be a later, deliberate change.
Deviations from Plan
Auto-fixed Issues
1. [Rule 1 - Bug] The pubfail counter leaked across apps
- Found during: Task 2 (full corpus replay: the 6th public route answered 429 on its first case)
- Issue: Task 1 stored the counter on the
Pluginvalue. That value is registered once per process (party.Register(&Plugin{})), so every test target, and any second app in one process, shared one counter. - Fix:
pubfailCounter(app)publishes one counter per app in the service registry at Register, and Routes reads it from there.TestPubfailCounterPerApppins this. - Commit: 6278e2b
2. [Rule 2 - Correctness] The check and the count are serialized
- Issue: The plan's edge requires that concurrent failures never let more than 10 resolutions through. Separate TooMany and Hit calls, as in PHP, cannot guarantee that.
- Fix:
Begin(key) (done func(failed bool), ok bool)reserves a slot. The handlers use it, andTooMany/Hitremain as specified. The concurrent subtest ofTestPubfailCounterruns 200 goroutines and lets exactly 10 through. - Commit: 990e311
3. [Rule 3 - Blocking] Route inventories named the old state
- The Phase 8 coverage subtests asserted that the public family was absent. They now assert the real surface through
assertPublicShareSurface. TestRequirePasswordChangeExemptSetrequiredinv.must-change-passwordon every/_fonoteka/api/v1route. The public share routes are now exempt, guard-free and counted.- The parity contract's ported-route allow-list gained the six routes.
- Commits: 990e311, 6278e2b
4. [Structure] Handler signatures and one extra file
- The handlers are
PublicResolve/PublicAlbumsIndex/PublicAlbumsShow(app, pubfail, kind): the counter is passed in rather than looked up per request. plugin.go(not in the plan's file list) holdspubfailCounter.- Commits: 990e311, 6278e2b
5. [Recording] A share:stale var for the rotation step
- Issue: The flow may carry only one authenticated step, the regenerate, so the old token could not be read back after the regenerate's capture replaced
share:collection. - Fix: Both seeds write
share:stalewith the seeded token. Steps before the regenerate are normalized to{{share:collection}}after recording, because the recorder may pick either name for the equal values. - Commit: 3c8b7cd
6. [Coverage] X-Robots-Tag is asserted in Go tests, not by the replay
- tide compares Content-Type and Cache-Control but not
X-Robots-Tag.assertPublicHeaderschecks it on every public response in the Go tests, 200, 404 and 429 included.
7. [Tests] Tests beyond the plan's list
TestPublicAlbumsIndexcovers search fields, facets, empty states, validation, show misses, kinds and the shared counter.TestPublicAlbumsEnginechecks the public query_by and the re-gate of poisoned engine ids.TestPubfailCounterPerApppins one counter per app.
Total deviations: 7: 1 bug, 1 correctness hardening, 1 blocking test-inventory update, 1 structural change, 1 recording aid, 1 coverage note and extra tests. Impact: every recorded body, status and compared header matches PHP. The counter is stricter than PHP only under concurrency.
Issues Encountered
TestPhase09SecurityRoutes(Phase 12.2 cabana routes, logged in deferred-items.md) still fails in the fonoteka plugin package. Every other suite is green:- the root module;
- parity (
TestParityCorpus157/157,TestFonotekaNuxtFlowswith both public flows,TestParityContract); - the plugin's subpackages.
- Tests ran with
FORCE_COLORunset, as in earlier plans. - The isolated PHP server (p1305) was started for recording and stopped before returning. No upload was made through PHP, so nothing was written to the PHP checkout's storage.
Known Stubs
None.
Threat Flags
None beyond the plan's register:
- T-13-01: PubfailCounter, TestPubfailCounter, TestPublicBucketsPerRoute and the public-pubfail flow.
- T-13-02: PublicAlbumDTO, the 422 on rating, TextFieldsPublic and the collection re-gate; TestPublicAlbumFieldSet and TestPublicAlbumsEngine.
- T-13-03: the disabled, regenerated and old-token cases in TestPublicResolve and public-anonymous.
- T-13-33: the wrong-kind cases on both kinds, in the corpus and in TestPublicAlbumsIndex/kinds.
User Setup Required
None.
Next Phase Readiness
- 13-06 (unit tests) should add the six public routes to the Phase 13 route-table test.
TestPublicBucketsPerRouteandwantPublicRoutesalready pin them. The public handlers take no request body, so they need no fuzz entry. - Phase 14 leaves these routes untouched. If production runs several instances, a shared pubfail store is a follow-up decision.
Self-Check: PASSED
- Created files exist: public_share.go, serialize_public_album.go, public_share_controller.go, public_share_smoke_test.go, public-anonymous.yaml and public-pubfail.yaml, all checked with
test -f. - Commits exist in fonoteka.go: 990e311, 6278e2b, 3c8b7cd.
- Plan verification:
go vet ./...is clean.go test ./...is green for the root and parity; the plugin package fails only the pre-existing TestPhase09SecurityRoutes.- The plan's tests pass under
-race. check_corpus --require-recorded --check-secretsis green, and no recorded share token occurs in any fixture.
- Acceptance greps:
"no-store, private"appears once in the middleware.- No
fonoteka-public-tokenremains on the group line. pubfail:is in public_share.go.expectedPortedRoutesis 157.- The public query_by string appears once in album_search.go.
- There are 2 resolve route lines, each with
throttle:10,1. created_atis in serialize_public_album.go.- public-pubfail has 1
Too many requestsand 10Not found. - public-anonymous has 8
share:collectionand 1Authorization. - The README mentions pubfail 5 times.