--- phase: 06-http-routing-auth-groups-and-rate-limiting plan: 10 type: execute wave: 6 depends_on: ["06-06"] files_modified: - fonoteka.go/plugins/golem15/fonoteka/middleware/token_scope.go - fonoteka.go/plugins/golem15/fonoteka/middleware/token_scope_test.go autonomous: true gap_closure: true requirements: [HTTP-05, HTTP-06] must_haves: truths: - "InvScope without a resolved user returns byte-exact 401 body {\"error\":\"Invalid token\"} with no trailing newline" - "InvScope with a token missing the requested scope returns byte-exact 403 body {\"error\":\"Missing required scope: \"} with no trailing newline" - "Tests compare raw recorder bytes directly and cannot hide whitespace with TrimSpace" - "A correctly scoped token still reaches the next handler unchanged" artifacts: - path: fonoteka.go/plugins/golem15/fonoteka/middleware/token_scope.go provides: "InvScope 401/403 responses delegated to framework wire.WriteJSON" - path: fonoteka.go/plugins/golem15/fonoteka/middleware/token_scope_test.go provides: "exact raw-byte assertions for both TokenScope denial branches" key_links: - from: fonoteka.go/plugins/golem15/fonoteka/middleware/token_scope.go to: summercms.go/wire/response.go via: "both denial branches call wire.WriteJSON, the shared PHP-compatible no-newline writer" pattern: "wire\.WriteJSON" --- Restore PHP byte parity for personal-token scope denial by using the shared no-newline JSON writer and making the 401/403 tests compare exact raw bytes. Purpose: the guard registry and response-convention contracts are only satisfied when TokenScope responses are byte-identical; `json.Encoder.Encode` adds a byte PHP does not send. Output: `InvScope` delegated to `wire.WriteJSON` and exact-byte denial tests that cannot mask the regression. @/home/jin/.codex/get-shit-done/workflows/execute-plan.md @/home/jin/.codex/get-shit-done/templates/summary.md @.planning/PROJECT.md @.planning/phases/06-http-routing-auth-groups-and-rate-limiting/06-CONTEXT.md @.planning/phases/06-http-routing-auth-groups-and-rate-limiting/06-VERIFICATION.md @.planning/phases/06-http-routing-auth-groups-and-rate-limiting/06-REVIEW.md @.planning/phases/06-http-routing-auth-groups-and-rate-limiting/06-01-SUMMARY.md @.planning/phases/06-http-routing-auth-groups-and-rate-limiting/06-03-SUMMARY.md Task 1: Emit and assert exact no-newline InvScope denial bodies fonoteka.go/plugins/golem15/fonoteka/middleware/token_scope.go, fonoteka.go/plugins/golem15/fonoteka/middleware/token_scope_test.go - No bouncer.User context -> 401, Content-Type application/json, raw body bytes exactly `{"error":"Invalid token"}`. - Resolved user with an ApiToken lacking `write` -> 403, Content-Type application/json, raw body bytes exactly `{"error":"Missing required scope: write"}`. - Both bodies have final byte `}` and contain no `\n` or `\r`; tests perform no trimming or whitespace normalization. - Resolved user plus a token containing the requested scope -> next handler runs and returns its original 204 response. fonoteka.go/plugins/golem15/fonoteka/middleware/token_scope.go (current local json.Encoder writer) fonoteka.go/plugins/golem15/fonoteka/middleware/token_scope_test.go (assertErrorBody currently hides the newline with strings.TrimSpace) fonoteka.go/plugins/golem15/fonoteka/middleware/token_scope_coverage_test.go (wrong-credential fail-closed path that reuses the assertion helper) summercms.go/wire/response.go (`wire.WriteJSON` exact behavior: SetEscapeHTML(false), trailing newline removed) /media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/middleware/TokenScope.php (source-of-truth 401/403 payloads) .planning/phases/06-http-routing-auth-groups-and-rate-limiting/06-CONTEXT.md (D-08 exact TokenScope bodies) .planning/phases/06-http-routing-auth-groups-and-rate-limiting/06-VERIFICATION.md (fourth authoritative gap) .planning/phases/06-http-routing-auth-groups-and-rate-limiting/06-REVIEW.md (WR-11) In `token_scope.go`, remove the local `writeJSON` helper and its `encoding/json` import. Import `git.golem15.com/golem15/summercms/wire` and call `wire.WriteJSON` in both denial branches with the existing statuses and exact `map[string]string` payloads. Do not change user/credential lookup, fail-closed wrong-credential behavior, scope text, status codes, or middleware ordering (D-08). In `token_scope_test.go`, remove `strings.TrimSpace` and compare `rec.Body.Bytes()` or `rec.Body.String()` directly to the expected literal. Keep JSON decoding only as a secondary shape/type assertion after the exact-byte comparison; it must not replace or normalize the wire assertion. Add explicit checks that neither denial body ends in newline/carriage-return. Ensure the named no-user 401 and missing-scope 403 subtests each make their own exact expected-body assertion. Keep the read-scope success case and the wrong-credential 403 coverage green. cd /media/nvme/dev/golem15/summercms.io/summercms/fonoteka.go && go test ./plugins/golem15/fonoteka/middleware -run 'TestInvScope' -count=1 -race -short && go vet ./plugins/golem15/fonoteka/middleware && go test ./plugins/golem15/fonoteka/... -count=1 -short - `token_scope.go` imports and calls `wire.WriteJSON`; it contains no `json.NewEncoder` or local response writer. - The 401 raw body equals exactly `{"error":"Invalid token"}` and the 403 raw body equals exactly `{"error":"Missing required scope: write"}`. - `token_scope_test.go` contains no `TrimSpace`, `Trim`, or normalized-body comparison on either denial path. - Both denial tests retain exact status and Content-Type assertions, while the valid-scope next-handler and wrong-credential fail-closed tests pass. InvScope's 401 and 403 are byte-identical to PHP TokenScope, with exact tests that fail on any trailing newline. ## Trust Boundaries | Boundary | Description | |----------|-------------| | personal token context -> HTTP denial | Authentication/scope state crosses into a public wire response whose status and bytes are part of the PHP compatibility contract | ## STRIDE Threat Register | Threat ID | Category | Component | Disposition | Mitigation Plan | |-----------|----------|-----------|-------------|-----------------| | T-06-27 | Tampering | `InvScope` 401/403 serialization | mitigate | Use the established `wire.WriteJSON` implementation and raw-byte assertions for both branches; forbid trimming in the regression tests | | T-06-SC | Tampering | package supply chain | accept | No install or manifest change; `wire` is an existing framework package already used by the phase | Run exact InvScope tests under `-race`, then the full fonoteka plugin suite. Grep the production and test files to prove `json.NewEncoder` and TrimSpace are absent from the TokenScope path. - Personal-token 401 and 403 bodies contain no trailing newline and match PHP bytes exactly. - Tests compare raw bytes before optional JSON shape checks. - Scope authorization and fail-closed credential behavior are unchanged. - The fonoteka middleware and plugin suites pass. Create `.planning/phases/06-http-routing-auth-groups-and-rate-limiting/06-10-SUMMARY.md` when done.