@@ -55,10 +55,10 @@ Requirements for v1 (the Płytarium port). Each maps to roadmap phases. "User" b
- [x]**HTTP-03**: Three mutually exclusive auth groups share the same handlers with different route subsets: JWT under /_fonoteka/api/v1, personal scoped token under /api/v1/fonoteka, and public groups (onboarding, public/{token}, public-wishlist/{token}, invitation inspection)
- [x]**HTTP-04**: A rate limiter supports named buckets keyed by a resolver (token id, IP, route param), stacking two limiters on one route, and ports Płytarium's five named buckets and inline throttles 1:1
- [x]**HTTP-05**: An auth guard registry lets plugins add guards (JWT, personal token, OAuth bearer) that all resolve to the same current-user accessor
- []**HTTP-06**: Response conventions are preserved: empty arrays serialize as [], timestamps as +00:00, tri-state booleans keep null, conditional keys are omitted not nulled, and no blanket envelope or error middleware wraps OAuth routes
- [x]**HTTP-06**: Response conventions are preserved: empty arrays serialize as [], timestamps as +00:00, tri-state booleans keep null, conditional keys are omitted not nulled, and no blanket envelope or error middleware wraps OAuth routes
- [ ]**HTTP-07**: A guarded outbound fetch helper enforces host allow-lists, byte caps and timeouts for user-supplied URLs (manual cover URL, Discogs cover)
- []**HTTP-08**: OpenAPI is generated from swaggo/swag annotations on handlers and openapi-typescript produces the admin SPA's types
- []**HTTP-09**: CORS and JSON body size limits match the PHP deployment
- [x]**HTTP-08**: OpenAPI is generated from swaggo/swag annotations on handlers and openapi-typescript produces the admin SPA's types
- [x]**HTTP-09**: CORS and JSON body size limits match the PHP deployment
### Authentication and users (AUTH)
@@ -188,10 +188,10 @@ Which phases cover which requirements. Updated during roadmap creation.
@@ -162,6 +163,10 @@ Recent decisions affecting current work:
- [Phase 06]: fonoteka-* buckets live on the app plugin via surf.BucketProvider, not hardcoded in surf — summercms.go must stay Płytarium-agnostic
- [Phase 06]: Empty PHP group builders plus test-only boot-probe routes prove middleware strings resolve without 501 shells — D-15: no 501 shells; wrap() only sees routes
- [Phase 06]: php_parity.sh pins APP_DEBUG=false; three existing HTML exception fixtures need re-recording — T-06-09 production-shaped error bodies
- [Phase 06]: HasHouseMiddleware is the only plugin-facing house-tag path; Assemble/BuildRouter is the sole RegisterHouseMiddleware caller (D-16)
- [Phase 06]: Production body limits are 134217728/134217728 (128MiB), operator-confirmed 2026-09-19 from nginx client_max_body_size=128M and php.ini post_max_size=128M/upload_max_filesize=128M (D-18, T-06-13)
- [Phase 06]: CORS path globs compile as Laravel nested * because Go path.Match would miss /api/v1/fonoteka/genres (Pitfall 10)
- [Phase 06]: swag v1 Swagger 2 is converted by a local swagger2openapi helper to OpenAPI 3 for openapi-typescript 7; Phase 10 wires types into the admin SPA
### Pending Todos
@@ -183,6 +188,6 @@ Items acknowledged and carried forward from previous milestone close:
- "HasHouseMiddleware is the only way a plugin declares house-tagged names; Assemble/BuildRouter is the sole RegisterHouseMiddleware caller (D-16)"
- "Assemble split into BuildRouter + compile so route:list can read Routes() without serving"
- "CORS path globs compile as Laravel * (nested segments), because Go path.Match would miss /api/v1/fonoteka/genres"
- "Production body limits are 134217728/134217728 (128MiB binary), operator-confirmed 2026-09-19 from nginx 128M + php.ini 128M/128M (D-18, T-06-13)"
- "swag v1 emits Swagger 2; a local swagger2openapi converter produces OpenAPI 3 for openapi-typescript 7.13.0"
patterns-established:
- "Raw groups refuse house-tagged middleware at wrap()/Assemble time and recover with a bare 500"
- "House vs ordinary middleware is a plugin capability split (Middlewares vs HouseMiddlewares), not a Register* call site"
- "Non-raw routes wrap http.MaxBytesReader from http.body_limits.default_bytes; body.limit:N overrides innermost; raw routes stay uncapped at this layer"
- "genre_controller writeJSON/writeOpaque500 are one-line delegations to wire"
# Phase 6 Plan 03: Raw groups, wire, CORS, and body-limit Summary
**Registration-time raw-group house-middleware refusal, route table + route:list, wire JSON helpers, path-scoped CORS, and operator-confirmed 128MiB body limits (134217728) with a committed OpenAPI document from ListGenres**
## Performance
- **Duration:** 20 min active execution (human-verify pause between Task 3 code and close-out not counted)
- **Started:** 2026-09-19T17:55:32Z
- **Completed:** 2026-09-19T18:57:39Z
- **Tasks:** 3 (Task 3 includes the production body-limit checkpoint)
- **Files modified:** 24
## Accomplishments
-`GroupRaw` plus sticky raw inheritance: a raw group cannot spawn a non-raw child. House-envelope names are declared only through `pact.HasHouseMiddleware`; `BuildRouter` registers them and `wrap()` fails boot with `surf: raw group cannot use house-envelope middleware %q (plugin %q)`. Raw panics write a bare 500; non-raw keep the house JSON body.
-`Router.Routes()` returns a defensive `[]RouteInfo` (method, pattern, plugin, middleware, raw). `surf.RouteListCommand` renders it; generated `main` registers `route:list` next to `serve`. Mutual-exclusivity of jwt.auth vs inv_token/inv.scope is asserted over the assembled table (closes T-06-02 / T-06-10).
-`wire.WriteJSON` matches genre_controller byte-for-byte (`SetEscapeHTML(false)`, trailing-newline trim). `wire.Time` marshals Carbon `+00:00` never `Z`; `TriBool` nulls when invalid; `Slice` never returns nil. `ListGenres` delegates and carries swag annotations; `docs/openapi.json` is committed and validated by openapi-typescript 7.13.0.
- Path-scoped CORS copies `config/cors.php` (`api/*`, `_user/api/*`, `_journal/api/*`, `_feedback/api/*`, `oauth/mcp/*`). JWT `/_fonoteka/api/v1/genres` emits no `Access-Control-Allow-Origin`; personal-token `/api/v1/fonoteka/genres` emits `*`.
- Body limits: non-raw routes wrap `http.MaxBytesReader` from `http.body_limits.default_bytes`; `body.limit:N` overrides innermost; raw routes are uncapped at this layer. Production values are operator-confirmed 2026-09-19: nginx `client_max_body_size=128M`, php.ini `post_max_size=128M` and `upload_max_filesize=128M` → **134217728 / 134217728** (128 × 1024 × 1024). D-18 / T-06-13 closed. No `INTERIM` remains in production yaml.
-`golem15.fonoteka` implements `HouseMiddlewares()` with `inv.must-change-password`. The oauth group is `GroupRaw("/", Use(), empty)` with zero handlers; Phase 8 attaches `throttle:fonoteka-oauth-token` / `throttle:fonoteka-oauth-register` per-route.
## Task Commits
Each task was committed atomically:
1.**Task 1: Raw group enforcement, house-middleware capability, route table, route:list** - `fa7e6d1` (feat, summercms.go)
- House-tagged middleware is a plugin capability (`HasHouseMiddleware`), never a `Register*` call from plugin code (D-16, T-06-11).
-`Assemble` is `BuildRouter` then `compile()`, so `route:list` inspects the table without serving.
- CORS `*` globs match nested path segments the way Laravel `api/*` does; Go `path.Match` would leave `/api/v1/fonoteka/genres` without CORS and fail Pitfall 10.
- Body-size numbers are not a guess: operator read production nginx + php.ini on 2026-09-19; both keys are 134217728 (D-18, T-06-13).
- swag v1.16.6 stays (STACK.md rejects swag v2 RC); a local converter bridges Swagger 2 → OpenAPI 3 for openapi-typescript 7.13.0. Phase 10 owns wiring types into the admin SPA.
## Deviations from Plan
### Auto-fixed Issues
**1. [Rule 3 - Blocking] Laravel glob instead of Go path.Match for CORS paths**
- **Found during:** Task 3 (path-scoped CORS)
- **Issue:** The plan specified `path.Match("api/*", trimmedPath)`. Go's `path.Match` does not let `*` cross `/`, so `api/*` would not match `api/v1/fonoteka/genres` and the personal-token group would emit no CORS headers — failing Pitfall 10 and HTTP-09.
- **Fix:** `compileLaravelGlob` treats `*` as `.*` (Laravel `fnmatch` / `api/*` semantics) against the leading-slash-stripped path.
- **Fix:** `scripts/swagger2openapi.go` converts the swag JSON to OpenAPI 3; the script writes `docs/openapi.json` and deletes the intermediate swagger.json. No new dependency.
**Impact on plan:** Both required for HTTP-08/HTTP-09 to be true. No scope creep; no extra libraries.
## Issues Encountered
None beyond the two auto-fixes. The human-verify checkpoint is planned flow, not an issue: operator reported all three production values as 128 MB on 2026-09-19.
## Checkpoint Resolution (D-18 / T-06-13)
Operator-confirmed 2026-09-19 from the production host:
INTERIM comments replaced with the confirmation comment. Body-limit tests assert against config-loaded / fixture YAML values and were not changed.
Re-run:
-`go test ./surf -count=1 -run BodyLimit` (summercms.go) — pass
-`go test ./plugins/golem15/fonoteka/... -count=1 -short` (fonoteka.go) — pass
## Auth Gates
Task 3 `checkpoint:human-verify` for production body-size numbers. Operator confirmed; numbers applied; plan closed. Not a deviation.
## Known Stubs
None that block this plan. The oauth `GroupRaw` has zero handlers by design (Phase 8 owns the two POST routes). Empty jwt_locale / onboarding / public_invitation / public_share builders were already declared in 06-02.
## User Setup Required
None remaining. The production-host body-size read is done (2026-09-19).
## Next Phase Readiness
- Phase 8 can mount `/oauth/mcp/token` and register on the existing raw group without house envelope/error middleware.
- Phase 10 can consume `docs/openapi.json` (already valid TypeScript via openapi-typescript).
- 06-05 owns full coverage, the remaining route-table assertions, and `06-SECURITY-REVIEW.md`.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.