feat(server): let a request name the tab that relays it - #207
Conversation
`request()` derives the relay tab from the request's own host. That is right for app hosts — routing photos.x.com through a www.x.com tab would be wrong — but it assumes every host CAN have a tab, and API hosts cannot. api.creditkarma.com serves no HTML app: a tab opened there has no content script, so a bridged request to it is unroutable no matter what the user opens. Meanwhile the signed-in www.creditkarma.com tab can issue that cross-origin fetch perfectly well — CK's own web app does exactly that for every GraphQL call. With a profile declared for the apex and a signed-in www tab open, there was no way to satisfy: fpx post-json https://api.creditkarma.com/graphql @body.json -p ck # bridge error: no tab matching https://api.creditkarma.com/ `viaTab` (CLI: `--via-tab <url>`) names the relay explicitly, leaving the default untouched so the photos.x.com reasoning still holds. It is guarded with `assertUrlInDomains`, the same way the request URL is. This widens which tab performs the fetch, never which origins can be reached — the declared-domain set the user approved at pair time stays the boundary. Matching is prefix-based, so `https://www.example.com/` accepts any page on the host while a deeper path pins one. Verified live: the reproduction above now returns a real GraphQL response body from api.creditkarma.com, relayed through the www tab. Closes #203 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012o2nXwu7tov6j7ciBEpigo
|
Claude finished @chrischall's task in 1m 59s —— View job Reviewing PR #207
SummaryClean, well-scoped addition. The Note: I was unable to run Nits
Observation (out of scope, author already flagged)The PR body itself notes Verdict: pass |
|
✅ Auto-review verdict: pass — Well-scoped, correctly wired, and well-tested addition of a viaTab/--via-tab override for relay-tab selection; only a minor CLI-vs-server exit-code inconsistency on malformed input. |
Correcting the evidence in this PRThe verification I gave was weaker than it looked, and I should have caught that before opening it. Two gaps:
Both have now been tested properly. The conclusion is unchanged, but the reasoning is different and sharper than what the PR body says. Control, back to back, same bridge stateSame command, same seconds apart, one flag different: unroutable → real origin response. Why the API-host tab genuinely cannot workI opened a tab on {"url":"chrome-error://chromewebdata/",
"title":"api.creditkarma.com",
"bodyStart":"This api.creditkarma.com page can't be found\nNo webpage was found for the web address: https://api.creditkarma.com/"}
So no amount of refreshing fixes it, and the "refresh the page to inject the content script" advice in that error is unactionable for this class of host. What that changesNothing about the code — the guard, the default, and the tests all stand. But the PR body's framing ("serves no HTML app") is imprecise; the operative fact is 404 → Worth folding that mechanism into the 🤖 Generated with Claude Code |
…210) Closes #209 — the nit auto-review raised on #207. ## The inconsistency `runFetch` validates the request URL against the profile before connecting, so a typo is exit 1 with guidance. `--via-tab` skipped that, so the same class of mistake travelled to the server guard and came back as exit 2 ("bridge error") — after making the user wait on a connection to be told their flag was malformed. Both now fail identically, before `listen()`: ``` $ fpx get https://api.x.com/v1 -p x --via-tab 'not a url' fpx: not a valid URL: "not a url" # exit 1 $ fpx get https://api.x.com/v1 -p x --via-tab https://evil.example/ evil.example is not on this profile's declared domains (…) # exit 1 ``` Verified by exit code, and the tests assert `listen()` was never called — a typo shouldn't cost a bridge round-trip. The server-side guard stays. It's the real boundary for library callers; this just stops the CLI from routing a usage error through it. ## Also: corrects the `viaTab` doc comment #207 said API hosts "serve no HTML app". That's imprecise, and it doesn't explain why the extension's own advice for the failure — *"Refresh the page in your browser to inject the content script"* — can't work. The actual mechanism, confirmed by opening such a tab and inspecting it: ```json {"url":"chrome-error://chromewebdata/", "bodyStart":"This api.creditkarma.com page can't be found…"} ``` The host 404s at `/`, so Chrome renders its **own** document at `chrome-error://chromewebdata/`, and Chrome never injects content scripts into `chrome-error://` pages regardless of the `<all_urls>` match. `chrome.tabs.query` still reports the tab's URL as the requested https one — which is exactly why the failure reads "1 URL match, none responded". No amount of reloading fixes it. Full reproduction and the missing control are in the correction comment on #207. Short version: my original evidence for that PR was weaker than it looked — one success with no matching failure run, and a motivation I'd asserted rather than tested. The conclusion held, but I should have run the control before opening it. ## Tests 1157 pass (up from 1154). Three new CLI tests: malformed relay tab, off-domain relay tab, and the accepted case — the first two also asserting no connection attempt. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_012o2nXwu7tov6j7ciBEpigo Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
🤖 I have created a release *beep* *boop* --- ## [2.0.0](v1.11.0...v2.0.0) (2026-08-06) ### ⚠ BREAKING CHANGES * **protocol:** bind the ephemeral key into the ready signature ([#222](#222)) ### Features * **protocol:** add write_cookies, the one verb that can repair a rotated session ([#211](#211)) ([b2557c2](b2557c2)) * **protocol:** bind the ephemeral key into the ready signature ([#222](#222)) ([c13aeed](c13aeed)) * **server:** let a request name the tab that relays it ([#207](#207)) ([c5d3f4d](c5d3f4d)) * **server:** pin the extension's identity, and verify it on the peer path ([#213](#213)) ([0eeced7](0eeced7)) ### Bug Fixes * **cli:** let a real filesystem error be itself, not "no extension pin" ([#221](#221)) ([c87a864](c87a864)), closes [#220](#220) * **cli:** validate --via-tab before connecting, like the request URL ([#210](#210)) ([959fcc5](959fcc5)) * **extension:** reattach the write_cookies doc block, and name the writable cookies as writable ([#215](#215)) ([2730c4a](2730c4a)) * **extension:** use the guarded caps local for the cookie heading ([#217](#217)) ([f95c832](f95c832)) * **server:** release only our own extension claim, and stop guessing scoped names ([#219](#219)) ([3d90a64](3d90a64)), closes [#218](#218) * **server:** type no-tab rejections so they stop reading as version mismatches ([#205](#205)) ([dc30bd9](dc30bd9)) ### Refactor * **server:** drop the concatBytes imports the signature change orphaned ([#224](#224)) ([4985ba7](4985ba7)), closes [#223](#223) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Closes #203.
The gap
request()derives the relay tab from the request's own host (ws-server.ts), and the fetch handler matches it strict-prefix (background.ts). That's correct for app hosts — the existing comment'sphotos.x.com/www.x.comreasoning is sound — but it assumes every host can have a tab.API hosts can't.
api.creditkarma.comserves no HTML app; a tab opened there has no content script (1 URL match, none responded). So with a profile declared for the apex and a signed-inwwwtab open, this was unsatisfiable:Meanwhile that
wwwtab can issue the cross-origin fetch perfectly well — CK's own web app does it for every GraphQL call.The change
viaTabonRequestOpts/BodylessRequestOpts,--via-tab <url>onget/post-json/request. The default is untouched, so thephotos.x.comcase is unaffected — you only get the override by asking for it.Verified live, same command that failed above:
A real GraphQL response from
api.creditkarma.com, relayed through thewwwtab. (The error body is just the missing bearer — the point is that it reached the origin at all.)Security
viaTabis guarded withassertUrlInDomains, exactly like the request URL. It widens which tab performs the fetch, never which origins are reachable; the declared-domain set approved at pair time stays the boundary. Malformed values throw naming the option rather than failing deep in the bridge. Both are tested.Matching stays prefix-based, so
https://www.example.com/accepts any page on that host and a deeper path pins one specific page — tested.Observation worth a separate issue
While tracing this I noticed
handleFetchRequestgatesreq.init.urlagainst declared domains but notreq.init.tabUrl, and content scripts inject on<all_urls>. So a consumer calling the lower-levelfetch()directly can already relay through a tab on an undeclared domain. The blast radius looks small — the request URL is still gated, so it's the page's origin being borrowed rather than its data being read — but it's an unenforced boundary, and this PR deliberately does not rely on it (hence the server-side guard). I left it alone rather than widen this PR's scope; say the word and I'll file it.Tests
1154 pass (up from 1148).
packages/server/tests/via-tab.test.tscovers the default, the override for absolute and relative paths, the declared-domain guard, subdomain/path forms, and malformed input. CLI tests cover flag parsing across all three verbs and the threading intoserver.request. Help text andCLAUDE.mdupdated.🤖 Generated with Claude Code
https://claude.ai/code/session_012o2nXwu7tov6j7ciBEpigo