Skip to content

fix(cli): scan a URL without a CDP browser instead of crashing - #77

Merged
askalf merged 1 commit into
mainfrom
fix/scan-url-without-cdp
Aug 10, 2026
Merged

fix(cli): scan a URL without a CDP browser instead of crashing#77
askalf merged 1 commit into
mainfrom
fix/scan-url-without-cdp

Conversation

@askalf

@askalf askalf commented Aug 10, 2026

Copy link
Copy Markdown
Owner

The bug

picket scan <url> routed every URL to captureFromBridge, which needs a CDP endpoint. With none configured it handed puppeteer an undefined browserURL:

Error: Exactly one of browserWSEndpoint, browserURL, transport or channel
must be passed to puppeteer.connect
    at captureFromBridge (src/capture.mjs:273:35)

So npx -y @askalf/fieldpass scan https://... — the first thing a new user tries, and the only zero-install entry point we have — could never work without a browser already running. Scanning a local file worked fine, which is why it went unnoticed.

The fix

  • URL with a browser (--browser, PICKET_BROWSER_URL, or now also PICKET_CDP) → CDP path, unchanged
  • URL without a browser → fetched and parsed statically
  • Fetch failures → message + exit 65, not a stack trace
  • --json now carries capturedBy so a caller can tell which path ran

The header states the weaker path plainly:

captured: static
note: static fetch - CSS-based hiding is not resolved.
      Pass --browser <cdp-url> for computed styles.

That note is load-bearing. The static parser can't resolve computed styles, so class-driven white-on-white and offscreen hiding read as visible — the CDP↔static gap that's already documented in the module. Inline and structural hiding are still caught, so the fixture below still reaches BLOCK, just with fewer nodes marked hidden.

Verified

case result
hostile fixture over https, no CDP BLOCK, exit 2 (was: crash)
local file BLOCK, exit 2 — unchanged
https://example.com ALLOW, exit 0 — no false positive

Suite 146/146.

Why now

Needed for a zero-install demo. Worth merging on its own regardless — it's the documented usage in the CLI's own header comment (picket scan https://example.com) failing on a stack trace.

picket scan <url> sent every URL to captureFromBridge, which requires a
CDP endpoint. With none configured it handed puppeteer an undefined
browserURL and died on a bare assert stack:

  Error: Exactly one of browserWSEndpoint, browserURL, transport or
  channel must be passed to puppeteer.connect

That is the first command a new user runs, and
px @askalf/fieldpass scan
https://... could never work without a browser already running.

Now: URL + a browser (--browser, PICKET_BROWSER_URL, or PICKET_CDP) uses
the CDP path unchanged. URL with no browser is fetched and parsed
statically, and the header says so:

  captured: static
  note: static fetch - CSS-based hiding is not resolved.
        Pass --browser <cdp-url> for computed styles.

The note is load-bearing, not decoration: the static parser cannot resolve
computed styles, so class-driven white-on-white and offscreen hiding read
as visible. Inline and structural hiding are still caught. Also surfaces
capturedBy in --json so a caller can tell which path ran, and turns fetch
failures into a message + exit 65 rather than a stack trace.

Verified: hostile fixture over https -> BLOCK exit 2; local file -> BLOCK
exit 2 (unchanged); https://example.com -> ALLOW exit 0 (no false
positive). Suite 146/146.

@sprayberry-redline sprayberry-redline left a comment

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.

This is an automated review from the Sprayberry Labs fleet code reviewer.

Verdict: No blocking issues found — clean, well-scoped fix.

What I checked

  • gh pr diff 77 (the only changed file, bin/picket.mjs, +35/-4)
  • captureFromHtml / captureFromBridge in src/capture.mjs to confirm obs.capturedBy is set on both paths ('static' and 'cdp') before it's read at bin/picket.mjs for the new --json field and the captured: ${obs.capturedBy} log line
  • CI rollup: CodeQL, test, test-node20 all SUCCESS on head a6d4624

Findings

None blocking.

Minor: the PR body claims "Suite 146/146" but there is no pre-existing or new test file covering bin/picket.mjs (git log on that path shows only a dependency-bump commit before this PR, and no test/*.mjs references bin/picket). That's not a regression this PR introduces — the CLI entrypoint was already untested — but the new branch (no-CDP URL fetch, fetch failure → exit 65, --json capturedBy) is exactly the kind of logic that would benefit from a script-level test now that it's grown a failure path.

What's good

  • The failure mode is exactly what the PR describes: captureFromBridge was called unconditionally for any URL, handing puppeteer an undefined browserURL and crashing with a raw stack trace — confirmed by reading the before/after diff of the isUrl branch (bin/picket.mjs old line 32-33 vs new if (isUrl && browserURL) ... else if (isUrl) ...).
  • The static-fetch fallback correctly surfaces its own limitation instead of silently pretending to have full fidelity — the note: static fetch — CSS-based hiding is not resolved line is only printed when staticOnUrl is true, and capturedBy is threaded into --json output so callers can detect which path ran.
  • Fetch errors and non-2xx responses are caught and exit distinctly (65) rather than throwing, consistent with the rest of the CLI's exit-code contract described in the header comment.
  • Header doc-comment update matches the new behavior precisely.

@askalf
askalf merged commit 3bd907d into main Aug 10, 2026
4 checks passed
@askalf
askalf deleted the fix/scan-url-without-cdp branch August 10, 2026 21:49
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