test(auth): carry the shared redirect-URI case table in this repo #3

Closed
opened 2026-08-25 02:15:26 +00:00 by jlxq0 · 1 comment
Owner

Decided 2026-08-25. Keep the copies; no shared crate. The decision and
its reasoning are in
the first comment on this issue:
Julian's constraint is that the five MCP servers stay independent, so the
remaining work is a redirect-URI case table carried identically in each
repo, with no repo importing anything from another. This issue is now that
task for caldav-mcp, not an open question.

Status in this repo as of 2026-08-26: seven of the nine rows are already
pinned in src/oauth_redirect.rs. The two that are not are the alternate
IPv4 loopback spellings (127.1, 0177.0.0.1, 2130706433) and bracketed
IPv6 ([::1]). Both behave as the table expects today — measured, not
assumed — so pinning them records behaviour rather than fixing a bug.

The observation

src/oauth_redirect.rs exists in five repositories — caldav-mcp, carddav-mcp, jmap-mcp, typst-mcp, m365-mcp — as five copies rather than one dependency. The RFC 8252 §7.3 loopback-port defect (#2 and its four siblings) therefore had to be fixed five times.

The copies have already diverged, and nobody decided to

  • caldav-mcp, carddav-mcp, typst-mcp: is_allowed_redirect_uri at line 23.
  • jmap-mcp: line 27.
  • m365-mcp: line 53, plus a compiled-in DEFAULT_REDIRECT_URIS constant and additive env semantics — M365_MCP_OAUTH_REDIRECT_URIS extends the compiled list, where the other four treat their env var as the whole allowlist.

That last difference is a behavioural divergence in a security control, and it happened by drift rather than by a decision, because there was no single place for the decision to live. It also means the same operator mistake — an empty or wrong env var — fails closed in four servers and fails open-ish in the fifth.

The question, not the answer

Whether to extract this into a shared crate is a genuine trade, and this issue exists to record the decision rather than to pre-make it.

For extracting: one place for a security-relevant matcher; a defect gets fixed once; the divergence above cannot recur silently.

Against extracting: a git dependency across five repositories means five coordinated version bumps for every change, and five servers that today can be released without reference to each other stop being independent. A shared crate also makes a bad change land in five places at once, which is the same property that makes it attractive.

A middle option: keep the copies but add a CI check that fails when they disagree, so drift becomes loud without coupling the release cycles.

Remaining work in this repo

Add the two unpinned rows to src/oauth_redirect.rs, and delete the
unreachable "::1" arm in is_loopback_host while there: both call sites pass
Url::host_str(), which always brackets IPv6, so the bare form cannot be
reached and reads as coverage that is not there.

The alternate IPv4 spellings resolve through the url crate's host
normalisation rather than through anything this repo wrote, which is the
argument for pinning them: a crate bump could change that behaviour with
nothing going red.

Related: #2

> **Decided 2026-08-25. Keep the copies; no shared crate.** The decision and > its reasoning are in > [the first comment on this issue](https://forge.oddie.app/jlxq0/caldav-mcp/issues/3#issuecomment-10569): > Julian's constraint is that the five MCP servers stay independent, so the > remaining work is a redirect-URI **case table** carried identically in each > repo, with no repo importing anything from another. This issue is now that > task for `caldav-mcp`, not an open question. > > Status in this repo as of 2026-08-26: seven of the nine rows are already > pinned in `src/oauth_redirect.rs`. The two that are not are the alternate > IPv4 loopback spellings (`127.1`, `0177.0.0.1`, `2130706433`) and bracketed > IPv6 (`[::1]`). Both behave as the table expects today — measured, not > assumed — so pinning them records behaviour rather than fixing a bug. ## The observation `src/oauth_redirect.rs` exists in five repositories — `caldav-mcp`, `carddav-mcp`, `jmap-mcp`, `typst-mcp`, `m365-mcp` — as five copies rather than one dependency. The RFC 8252 §7.3 loopback-port defect (#2 and its four siblings) therefore had to be fixed five times. ## The copies have already diverged, and nobody decided to - `caldav-mcp`, `carddav-mcp`, `typst-mcp`: `is_allowed_redirect_uri` at line 23. - `jmap-mcp`: line 27. - `m365-mcp`: line 53, plus a compiled-in `DEFAULT_REDIRECT_URIS` constant and **additive** env semantics — `M365_MCP_OAUTH_REDIRECT_URIS` extends the compiled list, where the other four treat their env var as the whole allowlist. That last difference is a behavioural divergence in a security control, and it happened by drift rather than by a decision, because there was no single place for the decision to live. It also means the same operator mistake — an empty or wrong env var — fails closed in four servers and fails open-ish in the fifth. ## The question, not the answer Whether to extract this into a shared crate is a genuine trade, and this issue exists to record the decision rather than to pre-make it. **For extracting:** one place for a security-relevant matcher; a defect gets fixed once; the divergence above cannot recur silently. **Against extracting:** a git dependency across five repositories means five coordinated version bumps for every change, and five servers that today can be released without reference to each other stop being independent. A shared crate also makes a bad change land in five places at once, which is the same property that makes it attractive. A middle option: keep the copies but add a CI check that fails when they disagree, so drift becomes loud without coupling the release cycles. ## Remaining work in this repo Add the two unpinned rows to `src/oauth_redirect.rs`, and delete the unreachable `"::1"` arm in `is_loopback_host` while there: both call sites pass `Url::host_str()`, which always brackets IPv6, so the bare form cannot be reached and reads as coverage that is not there. The alternate IPv4 spellings resolve through the `url` crate's host normalisation rather than through anything this repo wrote, which is the argument for pinning them: a crate bump could change that behaviour with nothing going red. Related: https://forge.oddie.app/jlxq0/caldav-mcp/issues/2
Author
Owner

Decision: keep the copies. Julian's constraint is that the five MCP servers stay independent of each other.

"All mcps should be and stay independent from each other."

That rules out a shared crate, and it rules it out for a better reason than taste: a git dependency would mean a bad change lands in five production services at once, which is the same property that made extraction attractive. Independent release cycles are worth more than one fewer copy.

Recommendation: a shared test corpus, not a shared crate and not a byte-diff

The naive version of "keep the copies but detect drift" is a job that diffs the five oauth_redirect.rs files and complains when they differ. Do not build that. It would be permanently red and therefore permanently ignored, because the files are supposed to differ — m365-mcp legitimately compiles a DEFAULT_REDIRECT_URIS list and treats its env var as additive, and that is a deliberate design difference, not drift.

The divergence that actually hurt was behavioural, not textual, and no textual check would have caught it: four repos treat the env var as the whole allowlist and fail closed on a bad value; the fifth extends a compiled list and does not. Nobody decided that.

So pin the behaviour, in each repo, with no dependency between them:

A table of redirect-URI cases, identical in all five repos, each asserting an outcome rather than an implementation. Every repo carries its own copy — no repo imports anything from another, independence is untouched — and any repo whose matcher behaves differently from the others fails its own test suite. Divergence in intent then becomes a deliberate edit to that table, visible in review, with a reason attached.

The cases already exist; tonight's work produced them across four repos independently:

case expected
loopback entry, different port accept
loopback entry, different path reject
loopback entry, different query reject
loopback entry, different host reject
https request against http loopback entry reject
http request against https loopback entry reject — the downgrade
127.1, 0177.0.0.1, 2130706433 spellings same outcome as 127.0.0.1
non-loopback entry, different port reject
bracketed IPv6 [::1] same outcome as ::1

Four of the five repos had the http-against-https-entry case unpinned until tonight — caldav found it, jmap, typst and m365 each confirmed it independently on their own copies. Four for four is the copy-paste, and it is exactly what this table prevents recurring.

The one thing that may legitimately be byte-identical across the five is that table. A small check outside the repos — in mantis, which depends on all of them and is depended on by none — comparing just the case table across the five is cheap and does not couple anything. That part is optional; the tests are not.

What this does not fix

Nothing here stops someone writing a sixth copy, and nothing here makes the five files converge. It makes a behavioural difference between them fail loudly in the repo that has it, which is the failure mode that actually occurred. That is the whole claim.

Related: #7, which is the same "a test that cannot fail is not evidence" shape one level down.

## Decision: keep the copies. Julian's constraint is that the five MCP servers stay independent of each other. > "All mcps should be and stay independent from each other." That rules out a shared crate, and it rules it out for a better reason than taste: a git dependency would mean a bad change lands in five production services at once, which is the same property that made extraction attractive. Independent release cycles are worth more than one fewer copy. ## Recommendation: a shared test corpus, not a shared crate and not a byte-diff The naive version of "keep the copies but detect drift" is a job that diffs the five `oauth_redirect.rs` files and complains when they differ. **Do not build that.** It would be permanently red and therefore permanently ignored, because the files are *supposed* to differ — `m365-mcp` legitimately compiles a `DEFAULT_REDIRECT_URIS` list and treats its env var as additive, and that is a deliberate design difference, not drift. The divergence that actually hurt was behavioural, not textual, and no textual check would have caught it: four repos treat the env var as the whole allowlist and fail closed on a bad value; the fifth extends a compiled list and does not. Nobody decided that. So pin the behaviour, in each repo, with no dependency between them: **A table of redirect-URI cases, identical in all five repos, each asserting an outcome rather than an implementation.** Every repo carries its own copy — no repo imports anything from another, independence is untouched — and any repo whose matcher behaves differently from the others fails *its own* test suite. Divergence in intent then becomes a deliberate edit to that table, visible in review, with a reason attached. The cases already exist; tonight's work produced them across four repos independently: | case | expected | |---|---| | loopback entry, different port | accept | | loopback entry, different path | reject | | loopback entry, different query | reject | | loopback entry, different host | reject | | `https` request against `http` loopback entry | reject | | `http` request against `https` loopback entry | **reject** — the downgrade | | `127.1`, `0177.0.0.1`, `2130706433` spellings | same outcome as `127.0.0.1` | | non-loopback entry, different port | reject | | bracketed IPv6 `[::1]` | same outcome as `::1` | Four of the five repos had the `http`-against-`https`-entry case unpinned until tonight — caldav found it, jmap, typst and m365 each confirmed it independently on their own copies. Four for four is the copy-paste, and it is exactly what this table prevents recurring. The one thing that may legitimately be byte-identical across the five is that table. A small check outside the repos — in `mantis`, which depends on all of them and is depended on by none — comparing just the case table across the five is cheap and does not couple anything. That part is optional; the tests are not. ## What this does not fix Nothing here stops someone writing a sixth copy, and nothing here makes the five files converge. It makes a *behavioural* difference between them fail loudly in the repo that has it, which is the failure mode that actually occurred. That is the whole claim. Related: https://forge.oddie.app/jlxq0/caldav-mcp/issues/7, which is the same "a test that cannot fail is not evidence" shape one level down.
jlxq0 changed title from decide: five copies of oauth_redirect.rs, and they have already diverged to test(auth): carry the shared redirect-URI case table in this repo 2026-08-26 04:55:34 +00:00
jlxq0 closed this issue 2026-08-26 05:25:17 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
jlxq0/caldav-mcp#3
No description provided.