|
27 | 27 | * a slot exclusively can still self-register via |
28 | 28 | * {@link HttpDispatcher.registerDomainHandler}. |
29 | 29 | * |
30 | | - * Matching semantics are deliberately faithful to the legacy if-chain, |
31 | | - * INCLUDING its rough edges (`match: 'prefix'` on `/i18n` also matches |
32 | | - * `/i18nxx`, exactly as `startsWith` did) — fixing those edges is explicitly |
33 | | - * not this seam's job; behavior preservation is. |
| 30 | + * Matching semantics were deliberately faithful to the legacy if-chain, |
| 31 | + * INCLUDING its rough edges, for as long as the migration needed behaviour |
| 32 | + * preservation to be the only promise this seam made. That period is over and |
| 33 | + * the edges are fixed (#16263): a domain claim now stops at a SEGMENT |
| 34 | + * BOUNDARY by default, so `/i18n` no longer claims `/i18nxx`. The legacy |
| 35 | + * `startsWith` shape is still reachable, but only where a route ASKS for it in |
| 36 | + * writing (`match: 'prefix'`) — see {@link DomainRoute.match}. |
34 | 37 | */ |
35 | 38 |
|
36 | 39 | import type { HttpProtocolContext, HttpDispatcherResult } from './http-dispatcher.js'; |
@@ -60,10 +63,43 @@ export interface DomainRoute { |
60 | 63 | /** Path prefix the domain claims, e.g. `'/i18n'`. */ |
61 | 64 | prefix: string; |
62 | 65 | /** |
63 | | - * `'prefix'` — legacy `startsWith(prefix)` semantics (default). |
| 66 | + * How much of the path space this route claims. |
| 67 | + * |
| 68 | + * `'segment'` — **the default**: the path equals the prefix, or is |
| 69 | + * followed by `'/'`. Claims `/i18n` and everything under `/i18n/`, and |
| 70 | + * does NOT claim `/i18nxx`. |
64 | 71 | * `'exact'` — the path must equal the prefix exactly. |
65 | | - * `'segment'` — exact, or followed by `'/'` (the legacy |
66 | | - * `=== p || startsWith(p + '/')` branch shape; does NOT claim `/i18nxx`). |
| 72 | + * `'prefix'` — bare `startsWith(prefix)`, NO segment boundary: the legacy |
| 73 | + * if-chain's shape, which also claims `/i18nxx`. |
| 74 | + * |
| 75 | + * ## Why `'segment'` is the default and `'prefix'` must be asked for |
| 76 | + * |
| 77 | + * The reasoning is #16026's, applied to the whole table rather than to one |
| 78 | + * prefix. A bare `startsWith` claim reaches SIBLING NAMESPACES: `/authx`, |
| 79 | + * `/authentication/foo`, `/datax`, `/metaxyz`, `/uifoo` are not paths of |
| 80 | + * the domain that was claiming them by any reading, and each is a |
| 81 | + * plausible namespace someone mounts later — a route registered there is |
| 82 | + * SHADOWED by a domain that never wanted it. `'segment'` claims the prefix |
| 83 | + * exactly and everything under `prefix + '/'`, which is the whole of what |
| 84 | + * a domain owns, so narrowing to it removes only claims a domain does not |
| 85 | + * own and keeps every sub-path fallthrough intact (#4088's |
| 86 | + * `/auth/me/permissions` is the case that pins that half). |
| 87 | + * |
| 88 | + * `'segment'` was already the codebase's own spelling for a |
| 89 | + * boundary-correct claim — `/auth`, `/keys`, `/mcp`, `/mcp/skill`, |
| 90 | + * `/security` and `/share-links` each declared it — so this makes the |
| 91 | + * table's majority spelling its default rather than introducing a |
| 92 | + * convention. |
| 93 | + * |
| 94 | + * ⚠️ `'prefix'` is NOT deprecated, and one shape genuinely needs it: a |
| 95 | + * prefix ending in `'?'` (`'/keys?'`, `'/mcp?'`), which reproduces the |
| 96 | + * legacy branch's query-string form for adapters that pass the query |
| 97 | + * through in `path`. There is no `/` after that `'?'`, so a segment match |
| 98 | + * cannot express it. Those routes declare `match: 'prefix'` in writing. |
| 99 | + * |
| 100 | + * ⛔ Do not reach for `'prefix'` to widen a domain's claim over its |
| 101 | + * lexical neighbours. The default changed because that claim was never |
| 102 | + * anything but a migration artefact. |
67 | 103 | */ |
68 | 104 | match?: 'prefix' | 'exact' | 'segment'; |
69 | 105 | /** Restrict to these UPPERCASE HTTP methods. Omit = all methods. */ |
@@ -362,10 +398,12 @@ export class DomainHandlerRegistry { |
362 | 398 | switch (route.match) { |
363 | 399 | case 'exact': |
364 | 400 | return path === route.prefix; |
365 | | - case 'segment': |
366 | | - return path === route.prefix || path.startsWith(route.prefix + '/'); |
367 | | - default: |
| 401 | + case 'prefix': |
| 402 | + // Bare `startsWith`, no segment boundary — the legacy |
| 403 | + // if-chain's shape, now reachable only by asking for it. |
368 | 404 | return path.startsWith(route.prefix); |
| 405 | + default: |
| 406 | + return path === route.prefix || path.startsWith(route.prefix + '/'); |
369 | 407 | } |
370 | 408 | } |
371 | 409 |
|
|
0 commit comments