Skip to content

tunnel: simplify allowlist grammar to bare host:port (SUBTEXT-350)#63

Open
joelgwebber wants to merge 1 commit into
mainfrom
joel/subtext-350-tunnel-allowlist
Open

tunnel: simplify allowlist grammar to bare host:port (SUBTEXT-350)#63
joelgwebber wants to merge 1 commit into
mainfrom
joel/subtext-350-tunnel-allowlist

Conversation

@joelgwebber
Copy link
Copy Markdown
Contributor

@joelgwebber joelgwebber commented May 13, 2026

SUBTEXT-350: Simplify tunnel allowlist grammar to bare host:port

Jira: 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.ts

  • 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.testfullstory.test
  • Parser strips legacy scheme:// prefix and *. wildcard prefix silently
    (both are no-ops under the new rules)
  • patternMatches() is scheme-agnostic; DNS patterns match host or
    *.host; IP patterns require exact match
  • New export canonicalizedFrom() detects when an input was broadened

tunnel/src/main.ts

  • tunnel-connect handler collects canonicalization warnings before
    registering patterns and 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

skills/tunnel/SKILL.md (and live/onboard)

  • All examples updated to bare host:port form
  • Trunk semantics and canonicalized field documented

tunnel/build/ — compiled output included

Why

The old scheme://*.suffix:port syntax required callers to know the
scheme 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 tunneled
sessions.

Testing

  • All unit tests updated and passing (pnpm test in tunnel/)
  • End-to-end integration test via local lidar + subtext-tunnel-local MCP:
    • tunnel-connect with www.fullstory.test:8043state: ready,
      canonicalized: [{input: "www.fullstory.test:8043", canonical: "fullstory.test:8043"}]
    • Navigation to https://app.fullstory.test:8043/ui succeeded through
      the tunnel (scheme-agnostic match + subdomain coverage confirmed)
    • Legacy https://*.fullstory.test:8043 input accepted, canonicalized,
      navigation succeeded

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
@joelgwebber joelgwebber requested a review from jurassix May 13, 2026 17:40
Copy link
Copy Markdown
Collaborator

@jurassix jurassix left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

all feedback optional and all basically replace fs with example but up to you s/fullstory/example

Comment thread skills/tunnel/SKILL.md

```json
"canonicalized": [
{"input": "www.fullstory.test:8043", "canonical": "fullstory.test:8043"}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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"}

Comment thread skills/tunnel/SKILL.md
- 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.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread skills/tunnel/SKILL.md
- 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`.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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`.

Comment thread skills/tunnel/SKILL.md
- **App with auth/SSO redirects between subdomains** (the common case). List the trunk:
```
allowedOrigins: ["https://*.example.test:8043"]
allowedOrigins: ["fullstory.test:8043"]
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
allowedOrigins: ["fullstory.test:8043"]
allowedOrigins: ["example.test:8043"]

Comment thread skills/tunnel/SKILL.md
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.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread skills/tunnel/SKILL.md
allowedOrigins: [
"https://*.example.test:8043",
"http://127.0.0.1:8766",
"fullstory.test:8043",
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"fullstory.test:8043",
"example.test:8043",

Comment thread skills/tunnel/SKILL.md
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`).
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 thread tunnel/build/src/main.js
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)."),
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 thread tunnel/src/main.ts
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 " +
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 " +

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants