Files
summercms/.planning/phases/06-http-routing-auth-groups-and-rate-limiting/06-03-SUMMARY.md
Jakub Zych f61776d0a6 docs(06-03): complete raw groups, wire, CORS, and body-limit plan
- Operator-confirmed 128MiB body limits (134217728); D-18/T-06-13 closed
- HTTP-06, HTTP-08, HTTP-09 marked complete
2026-09-19 21:01:07 +02:00

217 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
phase: 06-http-routing-auth-groups-and-rate-limiting
plan: 03
subsystem: auth
tags: [surf, wire, cors, body-limit, raw-group, openapi, route-table, house-middleware]
requires:
- phase: 06-http-routing-auth-groups-and-rate-limiting
provides: pact.Router verbs and name:param factories, inv_token + jwt.auth groups, throttle factory and five fonoteka buckets
provides:
- Router.GroupRaw with sticky raw inheritance and registration-time house-envelope refusal
- pact.HasHouseMiddleware collected only from Assemble/BuildRouter's plugin loop
- Router.Routes() / RouteInfo and surf.RouteListCommand (route:list)
- wire.WriteJSON, WriteOpaque500, Time (+00:00), TriBool, Slice
- path-scoped CORS matching config/cors.php (JWT group: no headers; token group: *)
- per-route http.MaxBytesReader body limits with operator-confirmed 128MiB production numbers
- raw oauth group declared with zero handlers; inv.must-change-password house-tagged
- committed OpenAPI document from ListGenres swag annotations, validated by openapi-typescript
affects: [06-05-tests, phase-08-oauth, phase-10-admin-spa]
tech-stack:
added: []
patterns:
- HasHouseMiddleware is the only plugin-facing house-tag path; plugins never call Register*
- Assemble = BuildRouter + compile so route:list inspects without serving
- Laravel-style CORS globs (api/* matches nested segments), not Go path.Match
- swag v1 Swagger 2 converted by a local swagger2openapi helper to OpenAPI 3 for openapi-typescript 7
key-files:
created:
- surf/routetable.go
- surf/routetable_test.go
- surf/routelist_command.go
- surf/cors.go
- surf/cors_test.go
- surf/bodylimit.go
- surf/bodylimit_test.go
- wire/response.go
- wire/response_test.go
- scripts/check-openapi.sh
- scripts/swagger2openapi.go
- docs/openapi.json
- plugins/golem15/fonoteka/routes_cors_test.go
modified:
- pact/capabilities.go
- surf/router.go
- surf/router_test.go
- surf/middleware_test.go
- internal/build/build.go
- internal/build/build_test.go
- plugins/golem15/fonoteka/plugin.go
- plugins/golem15/fonoteka/routes.go
- plugins/golem15/fonoteka/controllers/genre_controller.go
- config/http.yaml
- parity/genre_security_test.go
key-decisions:
- "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"
requirements-completed: [HTTP-06, HTTP-08, HTTP-09]
duration: 20 min
completed: 2026-09-19
---
# 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)
2. **Task 2: wire helpers (framework)** - `9ecf2d1` (feat, summercms.go)
3. **Task 2: OpenAPI from ListGenres and wire delegation (app)** - `db51df6` (feat, fonoteka.go)
4. **Task 3 code: path-scoped CORS and per-route body limits (framework)** - `30539b9` (feat, summercms.go)
5. **Task 3 code: raw oauth group, house-tagged password middleware, INTERIM yaml** - `cdc324a` (feat, fonoteka.go)
6. **Task 3 checkpoint: production 128MiB body limits** - `73671a8` (feat, fonoteka.go)
**Plan metadata:** (this commit)
## Files Created/Modified
- `pact/capabilities.go` — `GroupRaw` on Router; `HasHouseMiddleware`
- `surf/router.go` — raw flag, house-tagged registration, BuildRouter, per-route recoverBare/recoverJSON, body-limit wrap, path-scoped CORS
- `surf/routetable.go` — `RouteInfo` and `Routes()` defensive copy
- `surf/routelist_command.go` — `route:list` table command
- `surf/cors.go` — `CORSConfig`, Laravel glob compile, path-scoped middleware
- `surf/bodylimit.go` — `http.MaxBytesReader` default + `body.limit:N` override
- `wire/response.go` — WriteJSON, WriteOpaque500, Time, TriBool, Slice
- `internal/build/build.go` — generated main appends `surf.RouteListCommand`
- `plugins/golem15/fonoteka/plugin.go` — `HouseMiddlewares()`; `inv.must-change-password` moved out of `Middlewares()`
- `plugins/golem15/fonoteka/routes.go` — empty `GroupRaw` oauth landing spot
- `plugins/golem15/fonoteka/controllers/genre_controller.go` — wire delegation + swag comments
- `plugins/golem15/fonoteka/routes_cors_test.go` — CORS, house-capability, raw-refusal, route-table exclusivity on real plugins
- `config/http.yaml` — cors.php paths; body_limits 134217728/134217728 operator-confirmed 2026-09-19
- `scripts/check-openapi.sh` / `scripts/swagger2openapi.go` / `docs/openapi.json` — swag → OpenAPI 3 → openapi-typescript
## Decisions Made
- 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.
- **Files modified:** `surf/cors.go`, `surf/cors_test.go`
- **Verification:** `go test ./surf -run TestCORS`; fonoteka `TestCORSPathScopedOnAssembledRouter` (JWT empty ACAO, token `*`)
- **Committed in:** `30539b9` (Task 3)
**2. [Rule 3 - Blocking] Local swagger2openapi converter so openapi-typescript 7 can validate swag v1 output**
- **Found during:** Task 2 (check-openapi.sh)
- **Issue:** swag v1.16.6 emits Swagger 2.0 (`docs/swagger.json`). openapi-typescript 7.13.0 requires OpenAPI 3.x. STACK.md forbids swag v2 (RC only).
- **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.
- **Files modified:** `scripts/swagger2openapi.go`, `scripts/check-openapi.sh`
- **Verification:** `bash scripts/check-openapi.sh` exits 0; `docs/openapi.json` contains `/_fonoteka/api/v1/genres`
- **Committed in:** `db51df6` (Task 2)
---
**Total deviations:** 2 auto-fixed (2 Rule 3 blocking)
**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:
| Source | Value |
|--------|-------|
| nginx `client_max_body_size` | 128M |
| php.ini `post_max_size` | 128M |
| php.ini `upload_max_filesize` | 128M |
Converted with binary megabytes (nginx `m` / PHP `M` = 1024×1024): `128 * 1024 * 1024 = 134217728`.
`fonoteka.go/config/http.yaml`:
- `http.body_limits.default_bytes: 134217728`
- `http.body_limits.upload_bytes: 134217728`
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`.
- No blockers.
## Self-Check: PASSED
- FOUND: surf/routetable.go, surf/routelist_command.go, surf/cors.go, surf/bodylimit.go, wire/response.go, pact/capabilities.go
- FOUND: fonoteka.go/config/http.yaml default_bytes=134217728 upload_bytes=134217728, zero INTERIM
- FOUND: fa7e6d1, 9ecf2d1, 30539b9 (summercms.go); db51df6, cdc324a, 73671a8 (fonoteka.go)
- `go test ./surf -count=1 -run BodyLimit` pass; `go test ./plugins/golem15/fonoteka/... -short` pass
---
*Phase: 06-http-routing-auth-groups-and-rate-limiting*
*Completed: 2026-09-19*