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

12 KiB
Raw Blame History

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