tunnel: simplify allowlist grammar to bare host:port (SUBTEXT-350)#63
Open
joelgwebber wants to merge 1 commit into
Open
tunnel: simplify allowlist grammar to bare host:port (SUBTEXT-350)#63joelgwebber wants to merge 1 commit into
joelgwebber wants to merge 1 commit into
Conversation
Mirror the server-side grammar change in the TypeScript allowlist client.
Key changes:
- OriginPattern type simplified to {host, port, isIP, raw}; Scheme,
Wildcard, and Suffix fields removed
- canonicalizeHost() collapses multi-label DNS hosts to last two labels
(www.fullstory.test -> fullstory.test)
- Parser tolerates legacy scheme:// prefix and *. prefix (stripped
silently; both are no-ops under the new rules)
- patternMatches() is scheme-agnostic; DNS patterns match host or *.host,
IP patterns require exact match
- canonicalizedFrom() detects when an input was broadened to its trunk
- tunnel-connect MCP handler spreads canonicalized:[{input,canonical}]
into the response for any rewritten entries
- Tool description updated: documents bare host:port grammar, trunk
semantics, and the canonicalized response field
- SKILL.md updated with new grammar, examples, and canonicalized docs
- Compiled build/ output included
jurassix
approved these changes
May 15, 2026
Collaborator
jurassix
left a comment
There was a problem hiding this comment.
all feedback optional and all basically replace fs with example but up to you s/fullstory/example
|
|
||
| ```json | ||
| "canonicalized": [ | ||
| {"input": "www.fullstory.test:8043", "canonical": "fullstory.test:8043"} |
Collaborator
There was a problem hiding this comment.
feels safer to not use real ones?
Suggested change
| {"input": "www.fullstory.test:8043", "canonical": "fullstory.test:8043"} | |
| {"input": "www.example.test:8043", "canonical": "example.test:8043"} |
| - Hosts must be loopback-resolving (`localhost`, `127.x`, `::1`, `*.test`, `*.localhost`). | ||
| - Schemes can mix freely — one tunnel can serve `http://...` and `https://...` entries. | ||
| - Each entry is a bare `host:port` — for example `fullstory.test:8043` or `localhost:3000`. | ||
| - For DNS hosts, the entry matches the bare host **and any subdomain on the same port**. List the trunk you want to allow, not individual subdomains: `fullstory.test:8043` covers `app.fullstory.test:8043`, `oauthtest.fullstory.test:8043`, and so on. |
Collaborator
There was a problem hiding this comment.
Suggested change
| - For DNS hosts, the entry matches the bare host **and any subdomain on the same port**. List the trunk you want to allow, not individual subdomains: `fullstory.test:8043` covers `app.fullstory.test:8043`, `oauthtest.fullstory.test:8043`, and so on. | |
| - For DNS hosts, the entry matches the bare host **and any subdomain on the same port**. List the trunk you want to allow, not individual subdomains: `example.test:8043` covers `app.example.test:8043`, `oauthtest.example.test:8043`, and so on. |
| - No bare `*`. No port ranges. No paths. | ||
| - Hosts must be loopback-resolving (`localhost`, `127.x`, `::1`, `*.test`, `*.localhost`). | ||
| - Schemes can mix freely — one tunnel can serve `http://...` and `https://...` entries. | ||
| - Each entry is a bare `host:port` — for example `fullstory.test:8043` or `localhost:3000`. |
Collaborator
There was a problem hiding this comment.
Suggested change
| - Each entry is a bare `host:port` — for example `fullstory.test:8043` or `localhost:3000`. | |
| - Each entry is a bare `host:port` — for example `example.test:8043` or `localhost:3000`. |
| - **App with auth/SSO redirects between subdomains** (the common case). List the trunk: | ||
| ``` | ||
| allowedOrigins: ["https://*.example.test:8043"] | ||
| allowedOrigins: ["fullstory.test:8043"] |
Collaborator
There was a problem hiding this comment.
Suggested change
| allowedOrigins: ["fullstory.test:8043"] | |
| allowedOrigins: ["example.test:8043"] |
| allowedOrigins: ["fullstory.test:8043"] | ||
| ``` | ||
| Without the wildcard, the first redirect into the SSO subdomain (`oauthtest.example.test`, `auth.example.test`, etc.) returns a 502 and chromium lands on `chrome-error://chromewebdata/`. | ||
| This covers `app.fullstory.test:8043`, `oauthtest.fullstory.test:8043`, every other subdomain. Don't narrow to `app.fullstory.test:8043` — the first OAuth bounce will fail. |
Collaborator
There was a problem hiding this comment.
Suggested change
| This covers `app.fullstory.test:8043`, `oauthtest.fullstory.test:8043`, every other subdomain. Don't narrow to `app.fullstory.test:8043` — the first OAuth bounce will fail. | |
| This covers `app.example.test:8043`, `oauthtest.example.test:8043`, every other subdomain. Don't narrow to `app.example.test:8043` — the first OAuth bounce will fail. |
| allowedOrigins: [ | ||
| "https://*.example.test:8043", | ||
| "http://127.0.0.1:8766", | ||
| "fullstory.test:8043", |
Collaborator
There was a problem hiding this comment.
Suggested change
| "fullstory.test:8043", | |
| "example.test:8043", |
| 1. `tunnel-disconnect` the current tunnel. | ||
| 2. `live-tunnel` again — the `connection_id` is preserved across reconnect, so chromium continuity is fine. | ||
| 3. `tunnel-connect` with a wildcard that covers the redirect target (e.g. `https://*.example.test:8043` instead of `https://app.example.test:8043`). | ||
| 3. `tunnel-connect` with a trunk that covers the redirect target (e.g. `fullstory.test:8043` instead of `app.fullstory.test:8043`). |
Collaborator
There was a problem hiding this comment.
Suggested change
| 3. `tunnel-connect` with a trunk that covers the redirect target (e.g. `fullstory.test:8043` instead of `app.fullstory.test:8043`). | |
| 3. `tunnel-connect` with a trunk that covers the redirect target (e.g. `example.test:8043` instead of `app.example.test:8043`). |
Comment on lines
+54
to
+60
| "individual hostnames: `fullstory.test:8043` covers " + | ||
| "`app.fullstory.test:8043`, `oauthtest.fullstory.test:8043`, etc. " + | ||
| "Hosts are restricted to the loopback class (localhost, 127.x, " + | ||
| "::1, *.test, *.localhost). IP literals match exactly with no " + | ||
| "subdomain expansion. The response includes a `canonicalized` " + | ||
| "field listing any entries that were rewritten (e.g. legacy " + | ||
| "scheme prefix stripped, or a sub-trunk collapsed to its parent)."), |
Collaborator
There was a problem hiding this comment.
Suggested change
| "individual hostnames: `fullstory.test:8043` covers " + | |
| "`app.fullstory.test:8043`, `oauthtest.fullstory.test:8043`, etc. " + | |
| "Hosts are restricted to the loopback class (localhost, 127.x, " + | |
| "::1, *.test, *.localhost). IP literals match exactly with no " + | |
| "subdomain expansion. The response includes a `canonicalized` " + | |
| "field listing any entries that were rewritten (e.g. legacy " + | |
| "scheme prefix stripped, or a sub-trunk collapsed to its parent)."), | |
| "individual hostnames: `example.test:8043` covers " + | |
| "`app.example.test:8043`, `oauthtest.example.test:8043`, etc. " + | |
| "Hosts are restricted to the loopback class (localhost, 127.x, " + | |
| "::1, *.test, *.localhost). IP literals match exactly with no " + | |
| "subdomain expansion. The response includes a `canonicalized` " + | |
| "field listing any entries that were rewritten (e.g. legacy " + | |
| "scheme prefix stripped, or a sub-trunk collapsed to its parent)."), |
Comment on lines
+84
to
+89
| "individual hostnames: `fullstory.test:8043` covers " + | ||
| "`app.fullstory.test:8043`, `oauthtest.fullstory.test:8043`, etc. " + | ||
| "Hosts are restricted to the loopback class (localhost, 127.x, " + | ||
| "::1, *.test, *.localhost). IP literals match exactly with no " + | ||
| "subdomain expansion. The response includes a `canonicalized` " + | ||
| "field listing any entries that were rewritten (e.g. legacy " + |
Collaborator
There was a problem hiding this comment.
Suggested change
| "individual hostnames: `fullstory.test:8043` covers " + | |
| "`app.fullstory.test:8043`, `oauthtest.fullstory.test:8043`, etc. " + | |
| "Hosts are restricted to the loopback class (localhost, 127.x, " + | |
| "::1, *.test, *.localhost). IP literals match exactly with no " + | |
| "subdomain expansion. The response includes a `canonicalized` " + | |
| "field listing any entries that were rewritten (e.g. legacy " + | |
| "individual hostnames: `example.test:8043` covers " + | |
| "`app.example.test:8043`, `oauthtest.example.test:8043`, etc. " + | |
| "Hosts are restricted to the loopback class (localhost, 127.x, " + | |
| "::1, *.test, *.localhost). IP literals match exactly with no " + | |
| "subdomain expansion. The response includes a `canonicalized` " + | |
| "field listing any entries that were rewritten (e.g. legacy " + |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
SUBTEXT-350: Simplify tunnel allowlist grammar to bare
host:portJira: https://fullstory.atlassian.net/browse/SUBTEXT-350
Companion PR (fullstory/mn): https://github.com/cowpaths/mn/pull/104388
What changed
Mirror the server-side grammar change in the TypeScript tunnel client.
tunnel/src/allowlist.tsOriginPatterntype simplified to{host, port, isIP, raw};scheme,wildcard, andsuffixfields removedcanonicalizeHost()collapses multi-label DNS hosts to last two labels:www.fullstory.test→fullstory.testscheme://prefix and*.wildcard prefix silently(both are no-ops under the new rules)
patternMatches()is scheme-agnostic; DNS patterns matchhostor*.host; IP patterns require exact matchcanonicalizedFrom()detects when an input was broadenedtunnel/src/main.tstunnel-connecthandler collects canonicalization warnings beforeregistering patterns and spreads
canonicalized: [{input, canonical}]into the response for anyrewritten entries
host:portgrammar, trunksemantics, and the
canonicalizedresponse fieldskills/tunnel/SKILL.md(and live/onboard)host:portformcanonicalizedfield documentedtunnel/build/— compiled output includedWhy
The old
scheme://*.suffix:portsyntax required callers to know thescheme and write an explicit
*.prefix. The new grammar is shorter,scheme-agnostic, and makes subdomain coverage the default — reducing the
most common source of
chrome-error://chromewebdata/in tunneledsessions.
Testing
pnpm testintunnel/)tunnel-connectwithwww.fullstory.test:8043→state: ready,canonicalized: [{input: "www.fullstory.test:8043", canonical: "fullstory.test:8043"}]https://app.fullstory.test:8043/uisucceeded throughthe tunnel (scheme-agnostic match + subdomain coverage confirmed)
https://*.fullstory.test:8043input accepted, canonicalized,navigation succeeded