10 KiB
phase, plan, subsystem, tags, requires, provides, affects, tech-stack, key-files, key-decisions, patterns-established, requirements-completed, duration, completed
| phase | plan | subsystem | tags | requires | provides | affects | tech-stack | key-files | key-decisions | patterns-established | requirements-completed | duration | completed | |||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 06-http-routing-auth-groups-and-rate-limiting | 02 | auth |
|
|
|
|
|
|
|
|
|
14 min | 2026-09-19 |
Phase 6 Plan 02: Fixed-window limiter, five buckets, and PublicShareHeaders Summary
Laravel ThrottleRequests wire parity in surf.FixedWindowLimiter (success + 429 headers, first-hit-wins fixed window), five fonoteka- buckets registered by the app plugin, PublicShareHeaders 429 rewrite, and APP_DEBUG=false on the PHP recorder*
Performance
- Duration: 14 min
- Started: 2026-09-19T17:29:07Z
- Completed: 2026-09-19T17:42:58Z
- Tasks: 3
- Files modified: 16
Accomplishments
surf.Store/MemoryStoreandsurf.FixedWindowLimiterport Laravel's tooManyAttempts-before-hit, first-hit-wins window, stackedthrottle:middleware, and header placement (X-RateLimit-Limit/Remainingon 200;Retry-After/X-RateLimit-Reseton 429 with{"message":"Too Many Attempts."})surf.ClientIPhonorsX-Forwarded-Foronly whenRemoteAddris insidehttp.trusted_proxies; empty list is RemoteAddr onlygolem15.fonotekaimplementssurf.BucketProviderwith the five PHP buckets (60/30/30/60/120) and attachesthrottle:fonoteka-api-tokento the personal-token genres group;PublicShareHeadersrewrites 429 to{"error":"Too many requests"}and setsX-Robots-Tag/Cache-Control- Remaining Phase-6-scope groups (
jwt_locale,onboarding,public_invitation,public_share) are declared as empty builders;TestAllRouteGroupsBootwraps their real middleware stacks;php_parity.shexportsAPP_DEBUG=false; ROADMAP/REQUIREMENTS say five buckets
Task Commits
Each task was committed atomically:
- Task 1: Fixed-window Store, FixedWindowLimiter, trusted-proxy ClientIP, and router wiring -
68c6ab8(feat, summercms.go) - Task 2: Register the five buckets, attach throttle:fonoteka-api-token, port PublicShareHeaders, declare remaining route groups -
0acb3f6(feat, fonoteka.go) - Task 3 code: APP_DEBUG=false and boot-smoke coverage for all groups -
fbf143e(feat, fonoteka.go) - Task 3 docs: HTTP-04 named bucket count to five -
ab20ad0(docs, summercms.go)
Plan metadata: (this commit)
Files Created/Modified
surf/limiter_store.go— Store interface and mutex-guarded MemoryStore (Hit / TooManyAttempts / AvailableIn)surf/limiter.go— Bucket, BucketProvider, FixedWindowLimiter, named and inline throttle:N,Msurf/clientip.go— ClientIP and TrustedProxiessurf/router.go— limiter field, throttle factory, BucketProvider loop, noOpLimit removed; Limiter interface keptpact/capabilities.go— comment that BucketProvider is type-asserted in Assembleplugins/golem15/fonoteka/plugin.go— Buckets() and public.share-headersplugins/golem15/fonoteka/routes.go— token-group throttle plus four empty group buildersplugins/golem15/fonoteka/middleware/public_share_headers.go— 429 rewrite and noindex/no-storeconfig/http.yaml—trusted_proxies: []parity/php_parity.sh—APP_DEBUG=false.planning/ROADMAP.md/.planning/REQUIREMENTS.md— five named buckets (D-01)
Decisions Made
- Named the concrete type
FixedWindowLimiterso it coexists with the retainedsurf.Limiterinterface; rate limiting is exclusivelythrottle:...middleware (D-03, D-05) - Trusted proxies are captured at
NewFixedWindowLimiterconstruction so named-bucket keys and inline N,M share one list - Named
fonoteka-*buckets are registered by the app plugin viasurf.BucketProvider; surf stays Płytarium-agnostic - Empty group builders in
routes.goplus test-only/boot-probe/...routes satisfy D-15 (no 501 shells) while still failing Assemble on unknown throttle/middleware names - Production-shaped error bodies:
APP_DEBUG=falsefrom this point; three already-recorded HTML exception fixtures are flagged for re-record (not 429s)
Deviations from Plan
Auto-fixed Issues
*1. [Rule 3 - Blocking] -short boot-smoke cannot register inv_token without gorm.DB
- Found during: Task 3 (
TestAllRouteGroupsBoot -short) - Issue:
inv_tokenis registered only when Boot sees a published*gorm.DB. The plan's verify command uses-short, which skips testcontainers. - Fix: If the registry has no
inv_tokenmiddleware after Activate, register a stub Guard so wrap() can resolve the personal-token stack. Real TokenGuard remains the production path. - Files modified:
plugins/golem15/fonoteka/routes_bucket_test.go - Verification:
go test ./plugins/golem15/fonoteka/... -run TestAllRouteGroupsBoot -shortpasses - Committed in:
fbf143e(Task 3)
2. [Rule 2 - Missing Critical] Empty groups never reach wrap(), so throttle names would not fail boot
- Found during: Task 3 (boot-smoke design)
- Issue:
r.Group(..., Use(...), func(g) {})with zero Get/Post registers no routes; Assemble would succeed even ifpublic.share-headersor a bucket name were missing. - Fix:
bootProbePluginmounts throwawayGET /boot-probe/.../okroutes using each group's real middleware list (not PHP paths, not 501 shells). Assemble of user + fonoteka + probe fails on unknown names. - Files modified:
plugins/golem15/fonoteka/routes_bucket_test.go - Verification: TestAllRouteGroupsBoot passes; missing-bucket Assemble already covered in surf tests
- Committed in:
fbf143e(Task 3)
Total deviations: 2 auto-fixed (1 Rule 2, 1 Rule 3) Impact on plan: Both keep the -short boot-smoke honest without adding production 501 routes. No limiter-algorithm change.
Issues Encountered
go test ./... -shortin fonoteka.go failsTestParityCorpuscoverage (ported 2 passing 0) becauseparityDBskips undertesting.Short()while the coverage subtest still requires passing==ported. Pre-existing; not caused by this plan. Plan verification (./plugins/golem15/fonoteka/... -shortand./surf/... -short) is green. ExtraX-RateLimit-*headers are outside tide's compare allow-list, so they will not fail a full (non-short) replay.
Fixture audit (T-06-09)
CACHE_DRIVER=array still means live PHP 429s cannot be recorded from this harness. Three existing non-429 fixtures contain Winter HTML exception pages (exception-name-block / stack trace) and were recorded under debug:
parity/fixtures/routes/POST___fonoteka_api_v1_invitations_{token}_accept_jwt.yamlparity/fixtures/routes/POST___fonoteka_api_v1_tokens_jwt.yamlparity/fixtures/routes/POST___fonoteka_api_v1_oauth_consent_jwt.yaml
Re-record these against the now-pinned APP_DEBUG=false isolated PHP when those routes are ported. Do not treat them as 429 contract fixtures.
User Setup Required
None - no external service configuration required.
Next Phase Readiness
Ready for 06-03 (route table, raw OAuth group, path-scoped CORS). Throttle factory, buckets, ClientIP, and public-share 429 shape are load-bearing. oauth group remains unregistered (D-09).
Self-Check: PASSED
- FOUND: surf/limiter.go, surf/limiter_store.go, surf/clientip.go, plugins/golem15/fonoteka/middleware/public_share_headers.go, plugins/golem15/fonoteka/routes_bucket_test.go
- FOUND:
68c6ab8, 0acb3f6, fbf143e,ab20ad0 - FOUND: zero
noopLimiter|noOpLimitin surf/; zero "seven named" in ROADMAP.md/REQUIREMENTS.md; APP_DEBUG=false in php_parity.sh go vet ./...green in both repos;go test ./surf/... -shortandgo test ./plugins/golem15/fonoteka/... -shortgreen
Phase: 06-http-routing-auth-groups-and-rate-limiting Completed: 2026-09-19