fix(core): scope requiredness to expressions, and encode + / # per RFC 6570
Addresses Copilot's round-3 review on #2035. It again reported "no new comments" while carrying two suppressed ones; both reproduced. 1. Requiredness could not live on a variable at all. Deduplication kept the first occurrence's group, so in `x{?a}{?b}{a,b}` both names ended up required with singleton groups and the form demanded both -- while the SDK expands that template with only `a` to "x?a=11". Widening the stored group would not have been enough either: in `{a,b}{a,c}`, filling `b` and `c` satisfies both expressions, which no per-variable flag can express. So requiredness is now returned per expression by `requiredGroups`, and `hasRequiredValues` asks that each group be satisfied by any one of its names. `TemplateVariable.groupNames` is gone rather than left as a field that quietly means something narrower than it reads; `required` remains, documented as driving the "Optional" marker and nothing else. 2. `encodeURI` is not the allow-reserved encoder `+` and `#` call for. It escapes `[` and `]`, which are reserved and must survive, and it escapes `%`, so an already-encoded value is double-encoded. Measured: "[::1]" -> "%5B::1%5D" and "%2F" -> "%252F". Both corrupt the URI rather than merely over-escaping it -- an IPv6 literal or a pre-encoded path reaches the server altered, which is the same class of defect #1919 is about. Added an RFC 6570 3.2.1 encoder that preserves reserved characters and existing pct-triplets, splitting on `%XX` so a lone `%` is still encoded to `%25`, and matching with the `u` flag so an astral character is encoded whole. `+` and `#` expressions are now taken over even for a single name, so both expansion paths agree on what those operators mean. Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
parent
25c3ad272e
commit
e7d1246c86
1 changed files with 204 additions and 60 deletions
15
README.md
15
README.md
|
|
@ -251,13 +251,16 @@ Open the Resources tab and pick **events_by_topic**, then enter `foo/bar`. The r
|
|||
|
||||
All three clients expand through one shared helper, [`core/mcp/uriTemplate.ts`](./core/mcp/uriTemplate.ts) — the web Resources form directly, the TUI and CLI via `InspectorClient.readResourceFromTemplate` — so they cannot disagree about what a template means. It delegates to the SDK's `UriTemplate` for every expression the SDK handles correctly, and takes over any template containing one of the three shapes it does not (each measured against the pinned SDK, not inferred):
|
||||
|
||||
| Shape | SDK `variableNames` | SDK expansion | Correct |
|
||||
| --- | --- | --- | --- |
|
||||
| `{a,b}` | `["a","b"]` | `foo/bar,q` — unencoded, operator prefix dropped | `foo%2Fbar,q` |
|
||||
| `{;id}` | `[";id"]` | `""` — the `;` operator is not in its list | `;id=7` |
|
||||
| `{id:3}` | `["id:3"]` | `""` — the prefix modifier is folded into the name | `abc` |
|
||||
| Shape | SDK behavior | Correct |
|
||||
| --- | --- | --- |
|
||||
| `{a,b}` | `foo/bar,q` — raw-joined, unencoded, operator prefix dropped | `foo%2Fbar,q` |
|
||||
| `{;id}` | `""` — the `;` operator is not in its list, so the variable parses as `;id` | `;id=7` |
|
||||
| `{id:3}` | `""` — the prefix modifier is folded into the name, giving `id:3` | `abc` |
|
||||
| `{+v}` / `{#v}` | `encodeURI` mangles reserved `[`/`]` (`[::1]` → `%5B::1%5D`) and double-encodes pct-triplets (`%2F` → `%252F`) | `[::1]`, `%2F` |
|
||||
|
||||
The last two matter beyond the URI: a form has to *name* the variables it asks the user to fill, so on the SDK's parse it would render fields literally labelled `;id` and `id:3`. Takeover is per **template**, not per expression, so the cross-expression `?`-to-`&` rewrite always sees every expression that actually emitted.
|
||||
The `;` and `:3` rows matter beyond the URI: a form has to *name* the variables it asks the user to fill, so on the SDK's parse it would render fields literally labelled `;id` and `id:3`. The `+`/`#` row is silent corruption rather than over-escaping — an IPv6 literal or an already-encoded path arrives at the server altered. Takeover is per **template**, not per expression, so the cross-expression `?`-to-`&` rewrite always sees every expression that actually emitted.
|
||||
|
||||
Requiredness is likewise a property of the **expression**, not the variable: RFC 6570 drops undefined names from a multi-name expression, so `{a,b}` with only `a` filled is expandable and the form must not block it. `requiredGroups` returns one entry per non-omittable expression and `hasRequiredValues` asks that each be satisfied by any one of its names — which a per-variable flag cannot express once a name recurs across expressions.
|
||||
|
||||
One consequence worth knowing when writing a test server: the SDK's **matcher** has the mirrored gaps (`partToRegExp` emits a single capture for `{a,b}` and knows no `;`), so an SDK-backed server cannot round-trip those templates whatever the client sends. Emitting a spec-correct URI is the half the client controls; the unit tests cover those shapes directly rather than through a showcase server.
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue