Skip to content

fix(cli): validate --via-tab before connecting, like the request URL - #210

Merged
chrischall merged 1 commit into
mainfrom
fix/via-tab-cli-validation
Aug 5, 2026
Merged

fix(cli): validate --via-tab before connecting, like the request URL#210
chrischall merged 1 commit into
mainfrom
fix/via-tab-cli-validation

Conversation

@chrischall

Copy link
Copy Markdown
Owner

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:

{"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.ai/code/session_012o2nXwu7tov6j7ciBEpigo

`runFetch` checks the request URL against the profile up front, so a typo
is exit 1 with guidance. `--via-tab` skipped that, so the same class of
mistake reached the server guard instead and came back as exit 2 — a
"bridge error" for what is plainly a usage error, after making the user
wait on a connection to be told so.

Both now fail the same way, before `listen()`:

  $ fpx get https://api.x.com/v1 -p x --via-tab 'not a url'   # exit 1
  $ fpx get https://api.x.com/v1 -p x --via-tab https://evil.example/  # exit 1

Also corrects the `viaTab` doc comment. It said API hosts "serve no HTML
app", which is imprecise and does not explain why the extension's own
advice for the failure ("refresh the page to inject the content script")
is unactionable. The real mechanism, confirmed by inspecting such a tab:
the host 404s at `/`, so Chrome renders its own document at
`chrome-error://chromewebdata/`, and Chrome never injects content scripts
into `chrome-error://` pages. `chrome.tabs.query` still reports the tab's
URL as the requested https one — hence "1 URL match, none responded" —
and no amount of reloading changes it.

Closes #209

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012o2nXwu7tov6j7ciBEpigo
@github-actions github-actions Bot added the auto-review Trigger Claude + Copilot review on this PR label Aug 5, 2026
@claude

claude Bot commented Aug 5, 2026

Copy link
Copy Markdown

Claude finished @chrischall's task in 2m 10s —— View job


Review complete

  • Read CLAUDE.md for conventions
  • Read PR diff
  • Review packages/cli/src/verbs/fetch.ts changes
  • Review packages/server/src/ws-server.ts changes
  • Review test coverage in packages/cli/tests/fetch.test.ts
  • Post findings and verdict

Posted a full summary review: #210 (comment)

Summary: The fix correctly moves --via-tab validation before the bridge connection, reusing the existing assertUrlOnProfile helper (same domain-matching semantics as the request URL). Traced the control flow confirming the UsageError propagates uncaught to main.ts's handler and maps to exit code 1, matching the PR's claim. New tests correctly assert server.listen is never called for the two rejection cases. The ws-server.ts change is a pure doc-comment correction — verified no logic changed there. No issues found. Could not execute the test suite in this sandbox (Bash approval not granted), noted as a limitation rather than a finding.

Verdict: pass

@claude

claude Bot commented Aug 5, 2026

Copy link
Copy Markdown

Review

Reviewed the diff against origin/main (git diff origin/main...HEAD) across all three changed files.

packages/cli/src/verbs/fetch.ts — Adds if (cmd.viaTab !== undefined) assertUrlOnProfile(cmd.viaTab, profile); right after the existing URL check, before the server is constructed. This reuses the pre-existing assertUrlOnProfile helper (unchanged in this PR) rather than adding new validation logic, so the domain-matching semantics for --via-tab are identical to the request URL's. Since this throws before the try { server.listen() ... } block, it propagates as an uncaught UsageError, which main.ts's handler (if (err instanceof UsageError), line 128) maps to exit code 1 with the hint — exactly the claimed behavior.

packages/cli/tests/fetch.test.ts — Three new tests cover malformed --via-tab, off-domain --via-tab, and the accepted case, and the first two assert server.listen was never called, which is the right way to prove the fix (no bridge round-trip for a usage error). I confirmed by reading runFetch that the ordering matches: validation happens before makeServer(...)/listen() are ever reached.

packages/server/src/ws-server.ts — Diff is purely a doc-comment rewrite on RequestOpts.viaTab; confirmed no logic changed (assertUrlInDomains / the viaTab handling in request() at lines ~1806-1816 is byte-identical to main). The corrected mechanism (API host 404 → chrome-error://chromewebdata/ → no content-script injection) is a documentation-only claim about browser behavior, not independently verifiable here, but it doesn't affect code correctness either way.

No correctness, convention, or test-coverage issues found. I was not able to execute npm test/vitest in this sandbox (Bash approval required and not granted), so I did not independently re-run the suite — this is a limitation of my environment, not a finding against the PR.

Verdict: pass

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

✅ Auto-review verdict: pass — PR correctly validates --via-tab against the profile's domains before connecting, reusing the existing assertUrlOnProfile helper; tests properly assert no bridge connection occurs on rejection, and the ws-server.ts change is a pure doc-comment correction with no logic changes.

@chrischall chrischall added the ready-to-merge Owner has reviewed; arm auto-merge to land when CI is green label Aug 5, 2026
@chrischall
chrischall enabled auto-merge (squash) August 5, 2026 22:21
@chrischall
chrischall merged commit 959fcc5 into main Aug 5, 2026
11 checks passed
@chrischall
chrischall deleted the fix/via-tab-cli-validation branch August 5, 2026 22:21
chrischall added a commit that referenced this pull request Aug 6, 2026
🤖 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

auto-review Trigger Claude + Copilot review on this PR ready-to-merge Owner has reviewed; arm auto-merge to land when CI is green

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Auto-review follow-ups for PR #207: feat(server): let a request name the tab that relays it

1 participant