Recording the full mcp-lifecycle fixture against real isolated PHP
(08-09-PLAN.md Task 2) uncovered three byte-level gaps between wristband's
assumed contract and actual production PHP behavior:
- Every explicit "Cache-Control: no-store" PHP sets is actually delivered
as "no-store, private" (Laravel's session-cookie default merges "private"
onto any explicit value); wristband's own default for unheadered JSON
error responses is "no-cache, private" (matching the house convention
already used elsewhere), not empty.
- PHP's redirect responses (authorize success and every error redirect)
render Symfony's default HTML redirect body with Content-Type
"text/html; charset=utf-8"; Go's bare 302 with no body never matched.
wristband/redirect_html.go ports that exact byte template, including
PHP's htmlspecialchars(ENT_QUOTES) escaping (Go's html.EscapeString uses
different quote entities).
tide/normalize.go: isIDKey now also masks "_ids" plural array fields
(e.g. collection_ids), a latent parity-corpus gap no prior fixture had
exercised with a literal, non-empty, non-placeholder array value.
- Register validates redirect_uris/grant_types/response_types/auth-method
in PHP's exact order, strips control characters and caps client_name at
255 runes, and generates client_id/secret via crypto/rand base64url
- confidential clients return the raw secret once; only its sha256 hex
persists (constant-time-comparable fixed transform)
- sweep-unconsented, the atomic cap check and the create all run inside one
wristband.Backend.WithinTx transaction (T-08-DCR-FLOOD)
- 64 KiB body bound via http.MaxBytesReader collapses to the endpoint's
native invalid_client_metadata body, matching D-21
- TestPhase8RedRegistration asserts the exact public-client DCR success
contract and fails while Server.Register is a 501 stub
- adds the Backend/Tx transaction-scoped store bundle (ClientStore,
AuthCodeStore, RefreshTokenStore, AccessTokenIssuer) and wristband's own
in-memory implementation for framework-level tests (D-07)
- adds crypto.go's fixed-transform helpers (random base64url, sha256 hex,
constant-time compare, S256) and Options/Server seams for the DCR
lifetimes, cap, sweep age and 64 KiB body bound (D-03/D-21)