edge: say what the two-spelling match does not cover
The final re-review could not break the invariant inside the space the matcher models — every combination of a backend percent-decoding or not and removing dot segments or not — but found the class that sits outside it: a backend that folds case, strips servlet-style path parameters, accepts backslash separators, strips trailing dots or double-decodes could resolve a path neither spelling matches into a flipped account's subtree. Rails does none of these, so Tito is not exposed, which is why this is recorded rather than defended against. The point is that the guarantee now states its own scope: a future consumer with different normalisation needs the reasoning redone, not inherited. An unqualified invariant in the one comment written to stop someone reversing this decision is worse than the residue it hides.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2 files changed,
+27
−0
docs/superpowers/specs/2026-09-02-path-based-routing-design.md+14 −0internal/edge/edge.go+13 −0
CI passed — run details
diff --git a/docs/superpowers/specs/2026-09-02-path-based-routing-design.md b/docs/superpowers/specs/2026-09-02-path-based-routing-design.md| index 3c038b4f3..ea44193e0 100644 |
| --- a/docs/superpowers/specs/2026-09-02-path-based-routing-design.md |
| +++ b/docs/superpowers/specs/2026-09-02-path-based-routing-design.md |
| @@ -317,6 +317,20 @@ invariant above: |
| - A prefix literally named `.` or `..` cannot exist — `validPathPrefix` |
| refuses both — so the raw spelling of `/../bob` can never match anything. |
| +**The scope of the guarantee**, stated so it is not over-read. The two |
| +spellings model exactly two normalisations — percent-decoding and dot-segment |
| +removal — and the invariant holds across every combination of a backend doing |
| +or not doing those two. It is *not* a claim that no other normalisation |
| +exists. A backend that folds case (`/PAULCA/foo`), strips servlet-style path |
| +parameters (`/paulca;jsessionid=x/foo`), accepts backslash separators, strips |
| +trailing dots, or double-decodes could resolve a path neither spelling matches |
| +into a flipped account's subtree. |
| + |
| +Rails does none of these — Rack keeps `;` in `PATH_INFO`, and routing is |
| +case-sensitive — so Tito is not exposed, which is why this is recorded rather |
| +than defended against. A future consumer with different normalisation |
| +behaviour needs this reasoning redone, not inherited. |
| + |
| **Three call sites, and they do not all want a path** (this is the subtle part): |
| - `edge.go:930` — the ServeHTTP dispatch. Takes the path. |
diff --git a/internal/edge/edge.go b/internal/edge/edge.go| index d1ac4885f..5d3ee5b39 100644 |
| --- a/internal/edge/edge.go |
| +++ b/internal/edge/edge.go |
| @@ -1023,6 +1023,19 @@ func (e *Edge) lookupUpstreamPath(host, urlPath string) (upstream, bool) { |
| // Do not collapse this back to a single form. Whichever one you keep, |
| // you reopen the bypass the other one closes. |
| // |
| + // The scope of the guarantee, stated so it is not over-read: the two |
| + // forms model percent-decoding and dot-segment removal, and the |
| + // invariant holds across every combination of a backend doing or not |
| + // doing those two. It is NOT a claim that no normalisation exists. A |
| + // backend that folds case ("/PAULCA/foo"), strips servlet-style path |
| + // parameters ("/paulca;jsessionid=x/foo"), accepts backslash |
| + // separators, strips trailing dots, or double-decodes could resolve a |
| + // path neither form matches into a flipped account's subtree. Rails |
| + // does none of these — Rack keeps ";" in PATH_INFO and routing is |
| + // case-sensitive — so Tito is not exposed, but a future consumer with |
| + // different normalisation would need this reasoning redone rather than |
| + // inherited. |
| + // |
| // No guard is needed for the empty segment ("/", "//", ""): validPathPrefix |
| // refuses an empty prefix, so no row can be keyed on one and both lookups |
| // below simply miss, falling through to the host's own route. |