docs(13-05): complete public share routes plan
This commit is contained in:
@@ -0,0 +1,313 @@
|
|||||||
|
---
|
||||||
|
phase: 13-p-ytarium-api-wishlist-notifications-csv-credentials-public
|
||||||
|
plan: 05
|
||||||
|
subsystem: api
|
||||||
|
tags: [public-share, anonymous, rate-limit, pubfail, search, facets, parity, tide]
|
||||||
|
|
||||||
|
requires:
|
||||||
|
- phase: 13-03
|
||||||
|
provides: "ResolvePublic(ctx, db, token, kind) in share_service.go, share:wishlist capture"
|
||||||
|
- phase: 13-04
|
||||||
|
provides: "album_search.go SQL path used by the CSV export (left unchanged)"
|
||||||
|
- phase: 12
|
||||||
|
provides: "album search with engine re-gate, album embeds serializer, share service enable/disable/regenerate, album parity state"
|
||||||
|
provides:
|
||||||
|
- "Six anonymous public routes: GET public/{token}, public/{token}/albums, public/{token}/albums/{id} and the public-wishlist twins"
|
||||||
|
- "classes.PubfailCounter (TooMany, Hit, Begin) with PubfailKey, one per app in the service registry"
|
||||||
|
- "classes.PublicHeader, PublicFacets, PublicAlbums, FindPublicAlbum; SerializePublicAlbum(s) with the twelve-key PublicAlbumDTO"
|
||||||
|
- "Public search mode in album_search.go: AlbumSearchParams.Public, TextFieldsPublic / TextFieldsAuthenticated"
|
||||||
|
- "D-14 route layout: group public.share-headers only, resolve routes throttle:10,1, albums routes fonoteka-public-token then fonoteka-public-ip"
|
||||||
|
- "PublicShareHeaders sends Cache-Control: no-store, private (PHP's wire value)"
|
||||||
|
- "Parity: public seed state on both sides (share:collection, share:wishlist, share:disabled, share:stale), 6 routes re-recorded (157 ported, 14 pending), public-anonymous and public-pubfail flows"
|
||||||
|
affects: [13-06, 14]
|
||||||
|
|
||||||
|
actuals:
|
||||||
|
tokens: 32755
|
||||||
|
tasks: 3
|
||||||
|
commits: 3
|
||||||
|
|
||||||
|
tech-stack:
|
||||||
|
added: []
|
||||||
|
patterns:
|
||||||
|
- "Per-app state of a plugin lives in the app's service registry (app.Publish / app.Lookup), never on the process-wide registered plugin value"
|
||||||
|
- "A limiter whose check and count are separate (PHP tooManyAttempts + hit) is ported with a slot reservation (Begin/done) so concurrent requests cannot overshoot"
|
||||||
|
- "A flow that rotates a captured token seeds a second var (share:stale) with the old value; steps before the rotation are normalized to the primary var after recording"
|
||||||
|
|
||||||
|
key-files:
|
||||||
|
created:
|
||||||
|
- fonoteka.go/plugins/golem15/fonoteka/classes/public_share.go
|
||||||
|
- fonoteka.go/plugins/golem15/fonoteka/classes/serialize_public_album.go
|
||||||
|
- fonoteka.go/plugins/golem15/fonoteka/controllers/api/public_share_controller.go
|
||||||
|
- fonoteka.go/plugins/golem15/fonoteka/public_share_smoke_test.go
|
||||||
|
- fonoteka.go/parity/fixtures/nuxt/public-anonymous.yaml
|
||||||
|
- fonoteka.go/parity/fixtures/nuxt/public-pubfail.yaml
|
||||||
|
- fonoteka.go/parity/fixtures/routes/ (31 new public route fixtures)
|
||||||
|
modified:
|
||||||
|
- fonoteka.go/plugins/golem15/fonoteka/classes/album_search.go
|
||||||
|
- fonoteka.go/plugins/golem15/fonoteka/middleware/public_share_headers.go
|
||||||
|
- fonoteka.go/plugins/golem15/fonoteka/middleware/public_share_headers_test.go
|
||||||
|
- fonoteka.go/plugins/golem15/fonoteka/middleware/token_scope_coverage_test.go
|
||||||
|
- fonoteka.go/plugins/golem15/fonoteka/routes.go
|
||||||
|
- fonoteka.go/plugins/golem15/fonoteka/plugin.go
|
||||||
|
- fonoteka.go/plugins/golem15/fonoteka/routes_bucket_test.go
|
||||||
|
- fonoteka.go/plugins/golem15/fonoteka/routes_isolation_test.go
|
||||||
|
- fonoteka.go/plugins/golem15/fonoteka/phase08_coverage_test.go
|
||||||
|
- fonoteka.go/parity/manifest.yaml
|
||||||
|
- fonoteka.go/parity/fixtures/routes/ (5 re-recorded public route fixtures)
|
||||||
|
- fonoteka.go/parity/fonoteka_seed_test.go
|
||||||
|
- fonoteka.go/parity/fonoteka_reset.php
|
||||||
|
- fonoteka.go/parity/fonoteka_flows_test.go
|
||||||
|
- fonoteka.go/parity/parity_test.go
|
||||||
|
- fonoteka.go/parity/parity_contract_test.go
|
||||||
|
- fonoteka.go/parity/README.md
|
||||||
|
|
||||||
|
key-decisions:
|
||||||
|
- "The pubfail counter is in memory and one per app (published in the app's service registry at Register). In production that is one counter per serve process; PHP's file cache shared it across the PHP workers of one host. With several Go instances behind a balancer, a guesser gets 10 failures per instance per minute."
|
||||||
|
- "PubfailCounter.Begin reserves a failure slot for the request in flight, so the check and the count are serialized: concurrent failures from one IP never let more than 10 resolutions through in a window. PHP checks and hits separately and can overshoot under concurrency; sequentially the two behave identically."
|
||||||
|
- "The pubfail check runs before any database access, so a throttled request never reaches the database (PHP checks before resolvePublic too)."
|
||||||
|
- "Facets order artists and genres by name COLLATE \"C\" with the id breaking ties, as the recording SQLite (BINARY) orders them; production MariaDB with a Polish _ci collation may order differently. Decades are folded in Go from a year histogram; formats follow Album::FORMATS."
|
||||||
|
- "The public search is SearchAlbums with AlbumSearchParams.Public: the shared collection is the only tenant rule (no user, token or ratings join), rating and price are not sorts, query_by and LIKE use TEXT_FIELDS_PUBLIC, and engine ids are re-gated in SQL to that collection. Authenticated callers and the CSV export are unchanged."
|
||||||
|
|
||||||
|
patterns-established:
|
||||||
|
- "Plugin-owned mutable state that must not leak across apps is published per app (pubfailCounter(app))"
|
||||||
|
- "Public route families are pinned by one table (wantPublicRoutes) shared by the Phase 8 coverage subtests and TestPublicBucketsPerRoute"
|
||||||
|
|
||||||
|
requirements-completed: [API-03, API-07]
|
||||||
|
|
||||||
|
coverage:
|
||||||
|
- id: D1
|
||||||
|
description: "GET public/{token} and public-wishlist/{token}: PHP's header body, one 404 body for malformed, unknown, disabled, regenerated, wrong-kind and case-variant tokens, the shape check before any lookup, the exact public headers on 200, 404 and 429"
|
||||||
|
requirement: API-07
|
||||||
|
verification:
|
||||||
|
- kind: integration
|
||||||
|
ref: "fonoteka.go/plugins/golem15/fonoteka/public_share_smoke_test.go#TestPublicResolve"
|
||||||
|
status: pass
|
||||||
|
- kind: integration
|
||||||
|
ref: "fonoteka.go/parity/parity_test.go#TestParityCorpus (public/{token} 5 cases, public-wishlist/{token} 3 cases)"
|
||||||
|
status: pass
|
||||||
|
human_judgment: false
|
||||||
|
- id: D2
|
||||||
|
description: "Anti-enumeration: pubfail:<ip> counter shared by all six routes of both kinds, 10th failure 404, 11th request 429 without a lookup, 60 s window from the first failure, album misses behind a valid token never count, concurrent failures capped at 10, one counter per app"
|
||||||
|
requirement: API-07
|
||||||
|
verification:
|
||||||
|
- kind: unit
|
||||||
|
ref: "fonoteka.go/plugins/golem15/fonoteka/public_share_smoke_test.go#TestPubfailCounter"
|
||||||
|
status: pass
|
||||||
|
- kind: integration
|
||||||
|
ref: "fonoteka.go/plugins/golem15/fonoteka/public_share_smoke_test.go#TestPublicAlbumsIndex/pubfail-shared-across-routes"
|
||||||
|
status: pass
|
||||||
|
- kind: integration
|
||||||
|
ref: "fonoteka.go/plugins/golem15/fonoteka/public_share_smoke_test.go#TestPubfailCounterPerApp"
|
||||||
|
status: pass
|
||||||
|
- kind: e2e
|
||||||
|
ref: "fonoteka.go/parity/fonoteka_flows_test.go#TestFonotekaNuxtFlows/public-pubfail"
|
||||||
|
status: pass
|
||||||
|
human_judgment: false
|
||||||
|
- id: D3
|
||||||
|
description: "D-14 layout: group public.share-headers only, resolve routes exactly throttle:10,1, albums routes exactly fonoteka-public-token then fonoteka-public-ip, no auth middleware, bucket keys pubtok:<token> and the client IP; Cache-Control no-store, private"
|
||||||
|
requirement: API-07
|
||||||
|
verification:
|
||||||
|
- kind: unit
|
||||||
|
ref: "fonoteka.go/plugins/golem15/fonoteka/routes_bucket_test.go#TestPublicBucketsPerRoute"
|
||||||
|
status: pass
|
||||||
|
- kind: unit
|
||||||
|
ref: "fonoteka.go/plugins/golem15/fonoteka/middleware/public_share_headers_test.go"
|
||||||
|
status: pass
|
||||||
|
human_judgment: false
|
||||||
|
- id: D4
|
||||||
|
description: "public/{token}/albums and the wishlist twin: Validator::make semantics (rating refused, 422 envelope in PHP rule order), public text fields (notes not searchable, artists.name searchable), collection-only scope and engine re-gate, facets narrowed to the collection with Various Artists and zero rows dropped, empty result and empty collection bodies"
|
||||||
|
requirement: API-07
|
||||||
|
verification:
|
||||||
|
- kind: integration
|
||||||
|
ref: "fonoteka.go/plugins/golem15/fonoteka/public_share_smoke_test.go#TestPublicAlbumsIndex"
|
||||||
|
status: pass
|
||||||
|
- kind: integration
|
||||||
|
ref: "fonoteka.go/plugins/golem15/fonoteka/public_share_smoke_test.go#TestPublicAlbumsEngine"
|
||||||
|
status: pass
|
||||||
|
- kind: integration
|
||||||
|
ref: "fonoteka.go/plugins/golem15/fonoteka/search_leak_test.go#TestSearchLeak (authenticated search unchanged)"
|
||||||
|
status: pass
|
||||||
|
- kind: integration
|
||||||
|
ref: "fonoteka.go/parity/parity_test.go#TestParityCorpus (public albums 11 cases, public-wishlist albums 4 cases)"
|
||||||
|
status: pass
|
||||||
|
human_judgment: false
|
||||||
|
- id: D5
|
||||||
|
description: "albums/{id} on both kinds and the public album payload: exactly the twelve SerializesPublicAlbum keys in PHP order for an album with a rating, a market price, notes, a shelf, a condition, a barcode and a reservation; none of those values in any public body"
|
||||||
|
requirement: API-03
|
||||||
|
verification:
|
||||||
|
- kind: integration
|
||||||
|
ref: "fonoteka.go/plugins/golem15/fonoteka/public_share_smoke_test.go#TestPublicAlbumFieldSet"
|
||||||
|
status: pass
|
||||||
|
- kind: integration
|
||||||
|
ref: "fonoteka.go/parity/parity_test.go#TestParityCorpus (public albums/{id} 6 cases, public-wishlist albums/{id} 3 cases)"
|
||||||
|
status: pass
|
||||||
|
human_judgment: false
|
||||||
|
- id: D6
|
||||||
|
description: "Recorded anonymous journey (Nuxt /k/ and /w/ calls, a dead link, owner regenerate: old token 404, new token 200) and the link-guessing lockout replay against Go; corpus 157 ported and passing; secrets check green"
|
||||||
|
requirement: API-07
|
||||||
|
verification:
|
||||||
|
- kind: e2e
|
||||||
|
ref: "fonoteka.go/parity/fonoteka_flows_test.go#TestFonotekaNuxtFlows/public-anonymous"
|
||||||
|
status: pass
|
||||||
|
- kind: integration
|
||||||
|
ref: "fonoteka.go/parity/parity_test.go#TestParityCorpus/coverage (157 ported, 157 passing, 14 pending)"
|
||||||
|
status: pass
|
||||||
|
- kind: other
|
||||||
|
ref: "go run ./parity/check_corpus.go --manifest parity/manifest.yaml --routes .../routes.php --require-recorded --check-secrets"
|
||||||
|
status: pass
|
||||||
|
human_judgment: false
|
||||||
|
|
||||||
|
duration: 34min
|
||||||
|
completed: 2026-10-03
|
||||||
|
status: complete
|
||||||
|
plan_head_before: c540102eea33d68733ee4a5366be28c035954255
|
||||||
|
plan_head_after: 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:
|
||||||
|
1. the pubfail check, before any database access;
|
||||||
|
2. `ResolvePublic` for the route's kind;
|
||||||
|
3. `{"data":{"state":"active","collection":{"name","album_count"}}}`, or the one 404 body `{"error":"Not found"}`.
|
||||||
|
|
||||||
|
A failed resolution counts one failure.
|
||||||
|
- **Headers.** `PublicShareHeaders` now sends `Cache-Control: no-store, private`, the value Symfony puts on the wire. It keeps `X-Robots-Tag` and 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.
|
||||||
|
- `TooMany` peeks at the count, `Hit` adds one failure.
|
||||||
|
- `Begin` reserves a slot for the request in flight, so concurrent failures cannot pass the limit.
|
||||||
|
- Each app gets its own counter.
|
||||||
|
- **Search (Task 2).** `AlbumSearchParams.Public` selects 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_by `name,artist_display,style_names,genre_name,track_titles`, weights `10,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.** `PublicAlbumDTO` is 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-headers` only. The resolve routes carry exactly `throttle:10,1`. The albums routes carry exactly `throttle:fonoteka-public-token` then `throttle:fonoteka-public-ip`. No route regex constrains `{token}`.
|
||||||
|
- **Parity.**
|
||||||
|
- The `public` state exists on both sides (with `albums`): 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) and `public-pubfail` (10 × 404, then 429) replay green.
|
||||||
|
- The README documents the recipes.
|
||||||
|
|
||||||
|
## Task Commits
|
||||||
|
|
||||||
|
fonoteka.go (master, not pushed):
|
||||||
|
1. **Task 1: resolve shared collection links anonymously with PHP's headers and lockout** - `990e311` (feat)
|
||||||
|
2. **Task 2: browse, search and open shared collections and wishlists anonymously** - `6278e2b` (feat)
|
||||||
|
3. **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 `Plugin` value. 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. `TestPubfailCounterPerApp` pins 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, and `TooMany`/`Hit` remain as specified. The concurrent subtest of `TestPubfailCounter` runs 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`.
|
||||||
|
- `TestRequirePasswordChangeExemptSet` required `inv.must-change-password` on every `/_fonoteka/api/v1` route. 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) holds `pubfailCounter`.
|
||||||
|
- **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:stale` with 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`. `assertPublicHeaders` checks it on every public response in the Go tests, 200, 404 and 429 included.
|
||||||
|
|
||||||
|
**7. [Tests] Tests beyond the plan's list**
|
||||||
|
- `TestPublicAlbumsIndex` covers search fields, facets, empty states, validation, show misses, kinds and the shared counter.
|
||||||
|
- `TestPublicAlbumsEngine` checks the public query_by and the re-gate of poisoned engine ids.
|
||||||
|
- `TestPubfailCounterPerApp` pins 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 (`TestParityCorpus` 157/157, `TestFonotekaNuxtFlows` with both public flows, `TestParityContract`);
|
||||||
|
- the plugin's subpackages.
|
||||||
|
- Tests ran with `FORCE_COLOR` unset, 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. `TestPublicBucketsPerRoute` and `wantPublicRoutes` already 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-secrets` is green, and no recorded share token occurs in any fixture.
|
||||||
|
- Acceptance greps:
|
||||||
|
- `"no-store, private"` appears once in the middleware.
|
||||||
|
- No `fonoteka-public-token` remains on the group line.
|
||||||
|
- `pubfail:` is in public_share.go.
|
||||||
|
- `expectedPortedRoutes` is 157.
|
||||||
|
- The public query_by string appears once in album_search.go.
|
||||||
|
- There are 2 resolve route lines, each with `throttle:10,1`.
|
||||||
|
- `created_at` is in serialize_public_album.go.
|
||||||
|
- public-pubfail has 1 `Too many requests` and 10 `Not found`.
|
||||||
|
- public-anonymous has 8 `share:collection` and 1 `Authorization`.
|
||||||
|
- The README mentions pubfail 5 times.
|
||||||
Reference in New Issue
Block a user