Design reviewed-boundary chooser POST continuation amendment
1 file changed,
+49
−0
implementation/docs-chooser-continuation-design.md+49 −0
diff --git a/implementation/docs-chooser-continuation-design.md b/implementation/docs-chooser-continuation-design.md| new file mode 100644 |
| index 0000000..0999149 |
| --- /dev/null |
| +++ b/implementation/docs-chooser-continuation-design.md |
| @@ -0,0 +1,49 @@ |
| +# Docs chooser POST continuation amendment |
| + |
| +Status: proposed, awaiting configured Claude Fable review. Design only; based on reviewed chooser `07043915daebae10512a710915263093ed793b5b`, Home discussion 26. Branch discussion 27 records this amendment. |
| + |
| +## Problem and evidence |
| + |
| +A successful team selection currently responds to a same-origin POST with a 303 to the selected Docs origin. With `form-action 'self'`, Chromium and WebKit block that external redirect. The POST has already reached Home, but the browser stays on the chooser. Adding every team origin to the form policy would grow that header with the fleet. |
| + |
| +The isolated experiment in `/tmp/oficina-home-meta-refresh-experiment` compared this control with a successful POST returning 200 HTML containing an immediate meta-refresh and an ordinary Continue link. Both engines, with JavaScript on and off, reached the exact external destination by GET for the latter. The escaped query separator survived. History grew by one; Back fetched the chooser with GET, with no POST replay or dialog. All four controls were blocked and all four continuation cases succeeded. These were synthetic same-site HTTPS origins with a fixture CSRF check and static team allowlist, not the production chooser or membership database. |
| + |
| +## Narrow response change |
| + |
| +Keep Entry, Select, session identity, method/path/query checks, bounded single-field form parsing, same-origin CSRF middleware and the current-membership re-query. Only a successful explicit selection changes its response from 303 to 200 continuation HTML. A team removed between listing and submission still receives the existing generic 404. A failed membership or destination read still receives the existing generic unavailable response. The continuation creates no session, cookie, remembered team, grant or durable navigation record. |
| + |
| +The generic GET entry with one team continues to return 303. Zero-team and multi-team GET pages remain unchanged. This amendment does not add routes, apply schema, change provisioning, or mount anything on the credential role. It does not revise signed-out/expired-session behavior; actual-router acceptance must exercise those paths too and report any existing CSP redirect problem instead of relaxing policy. |
| + |
| +Build the destination from the chosen current store result alone: the validated stored origin, fixed `handoff.StartPath`, and `url.Values` encoding fixed `return_to=/`. The request supplies only its bounded team sqid. It never supplies a hostname, resource path, return URL, display label or destination query. A stale rendered origin cannot override a newly read origin. |
| + |
| +## Destination and output boundary |
| + |
| +The production store remains responsible for its compiled app registry and authoritative provisioning destination policy. `directorydata.Choices` already validates a canonical HTTPS origin and invokes `ValidateRoute` for each live row; any invalid row refuses the whole result. The response builder must additionally reject a noncanonical origin before interpolating a result, including ports, userinfo, path, query, fragment, mixed-case host, controls, whitespace and non-DNS host syntax. Reuse a narrow exported origin validator from directorydata if needed; do not introduce a weaker parser or a second provisioning policy. Never infer permission from a shared suffix, same-site cookies, or arbitrary configured URL input. |
| + |
| +Only an origin admitted by that authoritative policy may be used. Same-site and cross-site destinations have the same validation requirement; being cross-site does not relax it. Synthetic cross-site browser tests will use a separately allowlisted fixture origin. This amendment authorizes no new production origin and no public-suffix wildcard. |
| + |
| +Use the existing Home renderer and layout with a default-empty head block. The continuation page alone supplies a meta refresh with literal delay zero and a destination string passed through Go `html/template` attribute escaping. The plain anchor receives the identical destination string through its normal URL-aware template context. Do not use `template.HTML`, `template.HTMLAttr`, `template.URL`, string-built markup, request URLs, or JavaScript. Assert rendered meta and anchor destinations are equal after HTML parsing, contain no template rejection marker, and match the computed canonical URL. The shared layout change must leave every other page without a refresh directive. |
| + |
| +The page uses the existing Estilo assets and Rastrillo components. Its title, heading and anchor use fixed generic copy, `Continue to Docs`; no team name, address or stored origin is printed as prose. The visible link is a real navigable anchor even when refresh is disabled. The user waived human copy review; ordinary writing and code review still apply before implementation. |
| + |
| +Respond with `Cache-Control: no-store`, `Content-Type: text/html; charset=utf-8` and `Referrer-Policy: no-referrer` before the buffered renderer writes the 200. The anchor additionally uses `referrerpolicy="no-referrer"`. Preserve the existing central Content Security Policy without adding any origins, wildcard or handler override. There is no Location or HTTP Refresh header. The content response is an explicit navigation handoff, never evidence of authorization at Docs. |
| + |
| +## Integration is separately gated |
| + |
| +The reviewed chooser remains unmounted. This amendment makes no runtime schema/store or configuration decision. A later integration design must explicitly settle: directory-role-only application of `directorydata.Schema`; the store's existing dedicated sealer and lifecycle; a compiled application registry with the exact Docs slug; the canonical team-origin and consumer-ID relationship enforced by provisioning and reads; directory session and exact-host middleware order; and absent/invalid configuration behavior. It must name the actual canonical destination formula or authoritative registry before code. It must not create duplicated per-team pilot environment configuration merely to mount this feature. Credential-role schema and routing stay unchanged. |
| + |
| +The consumer still verifies its own issuer, audience and account mapping and enforces item grants. Navigation does not claim team admission. Production activation requires separate review, actual integrated tests and canary browser evidence; this design approval alone authorizes none of those changes. |
| + |
| +## Acceptance evidence required before a code review |
| + |
| +Tests must preserve the existing zero/one/many, stale-membership, privacy and bounded-input coverage. Add a response test showing successful POST is 200, contains one immediate refresh and one exact matching anchor, uses the three headers above, and creates no cookie or Location header. Add hostile/noncanonical-origin tests at the response boundary, changed-origin re-query, removal between GET and POST, and a mutation that restores 303 and fails the intended browser check. Verify no other shared-renderer page gains a refresh directive. |
| + |
| +Actual `App.Handler` tests must use the real directory store, sessions and CSRF middleware under exact Host handling; test absent/foreign/sibling Origin, missing/invalid session, duplicate or unexpected form fields, wrong Host, credential role refusal and unavailable store. While production routes remain unmounted, an isolated test-only router hook may wire the real chooser under that same middleware; it must be visibly an overlay fixture and must not claim production routing is mounted. A later integration must repeat these against the actual mounted router. |
| + |
| +Run Chromium and WebKit with JavaScript on/off and narrow/wide layouts, using real browser HTTPS Host/Origin and the actual Home renderer and central CSP. Verify successful POST then external GET, exact destination, no CSP/page errors, no horizontal overflow, no referrer at the destination, Back returning chooser without POST replay, and removal refusal. Test separately allowlisted same-site and cross-site fixture destinations; no live private keys, email or synthetic live assertion. |
| + |
| +Prove fallback operation by intercepting the already rendered continuation response in the browser fixture and removing only the refresh element, recording that intervention. Then click the actual unmodified anchor and verify the same GET destination and no referrer. This demonstrates anchor operability when automatic refresh is absent, not a claim about every browser accessibility preference. Preserve normal immediate-refresh receipts separately. |
| + |
| +## Usability limits |
| + |
| +The immediate intermediate page may be too brief to read or focus. The plain link is for browsers or preferences that suppress automatic refresh; it is not a timed choice. The synthetic experiment observed useful Back behavior but did not cover screen readers, refresh-disabled browser preferences, network failure, reload or every browser version. Those limits must remain in the review and release record. If actual integrated tests find a navigation loop or POST replay, stop and amend the design rather than suppressing the observation. |