Skip to content

feat: silent pairing for local web-dev servers - #3955

Closed
t3dotgg wants to merge 3 commits into
mainfrom
t3code/disable-pairing-codes
Closed

feat: silent pairing for local web-dev servers#3955
t3dotgg wants to merge 3 commits into
mainfrom
t3code/disable-pairing-codes

Conversation

@t3dotgg

@t3dotgg t3dotgg commented Jul 14, 2026

Copy link
Copy Markdown
Member

What

Local web-dev servers now authenticate the web UI automatically — no more scraping one-time /pair#token=… URLs out of dev-runner stdout for every fresh browser context. This was the main DX friction for working on multiple T3 Code changes in parallel, and the reason coding agents kept giving up on browser verification.

Design doc (reviewed by gpt-5.6-sol, findings incorporated): https://xaxuzu0q3o8s.postplan.dev

How

New endpoint POST /api/auth/dev-pairing-token mints an ordinary one-time, 5-minute administrative pairing credential (subject dev-auto-bootstrap, hidden from pairing-link listings) that the web app immediately exchanges through the existing browser-session flow.

Security model (why this doesn't reopen the drive-by-localhost hole the pairing codes exist to close):

  • Eligibility: endpoint answers 404 dev_pairing_not_available unless the server is web-mode + loopback-bound (loopback-browser policy) + configured with a loopback dev URL. Prod, desktop, LAN-bound dev, and hostile --dev-url values are all ineligible. The handler is always mounted — conditional non-registration would fall into the SPA/302 wildcard route instead of 404ing.
  • Per-request: browser-attached Origin must exactly equal the dev origin (survives the Vite /api proxy, which rewrites Host but not Origin — Host/CORS-only guards are defeated by the proxy path), plus a loopback Host header check against DNS rebinding on direct requests. JSON payload forces a CORS preflight, so unauthorized web origins can't even trigger credential issuance.
  • Admin scopes match what startup pairing grants today, so Settings → Connections keeps working in dev.

Startup: eligible dev servers open the plain dev URL instead of minting a startup /pair#token credential the silent path would leave unconsumed in terminal scrollback. All other startup shapes unchanged.

Web: the auth-gate bootstrap tries the endpoint when unauthenticated in dev — never in hosted/desktop contexts, never when an explicit #token is present (manual/remote pairing keeps precedence) — and falls back to the pairing screen silently on any failure. import.meta.env.DEV keeps it out of prod bundles.

Extracted netHost.ts host-header parsing (the existing isLoopbackHostname mishandles Host: localhost:13773, and importing it into auth/http.ts would create a module cycle).

Docs: AGENTS.md "Verifying Changes in the Browser" section for agents/contributors, dev-instance auth notes in scripts.md (+ stale T3CODE_STATE_DIR fix), security spec in environment-auth.md.

Verification

  • vp check, vp run typecheck green; server 1427 passed, web 1288 passed, contracts 179 passed
  • New tests: guard unit tests (Origin/Host/eligibility incl. port + IPv6 + rebinding shapes), 7 endpoint integration tests (incl. a raw node:http rebound-Host request and admin-scope/listing-exclusion assertions), 4 startup-target tests, 5 web-gate tests (incl. explicit-token precedence and reauth retry)
  • Live check against a running dev instance: mint 200 with dev origin (direct + through Vite proxy), 403 for foreign/missing Origin and rebound Host
  • E2E: fresh cookie-less Playwright profile loads the dev web port and lands authenticated with zero interaction:

fresh headless profile lands authenticated

🤖 Generated with Claude Code


Note

High Risk
Introduces an unauthenticated endpoint that issues administrative credentials; safety depends on narrow eligibility and Origin/Host guards—misconfiguration or guard bypass would be serious.

Overview
Adds silent dev pairing so local web dev servers can authenticate the UI without manual pairing codes or scraping /pair#token URLs from stdout.

Server: New unauthenticated POST /api/auth/dev-pairing-token mints a one-time administrative bootstrap credential (dev-auto-bootstrap, hidden from pairing-link listings). It returns 404 when the server is not web-mode + loopback loopback-browser policy + loopback devUrl, and 403 when Origin does not exactly match the dev origin or Host is not loopback. Startup opens the plain dev URL (no startup pairing token) when eligible.

Web: On load in import.meta.env.DEV, the auth gate calls the endpoint and exchanges the credential via the existing browser-session flow, with silent fallback to the pairing screen; skipped for explicit #token, desktop bridge, and hosted static apps.

Shared: Loopback/host parsing moves to netHost.ts (stricter 127.0.0.0/8 and Host: port handling). Docs and agent guidance updated for auto-auth on localhost dev.

Reviewed by Cursor Bugbot for commit 929d35a. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Add silent pairing for local web dev servers via new /api/auth/dev-pairing-token endpoint

  • Adds a new POST /api/auth/dev-pairing-token endpoint that issues a one-time administrative pairing credential for eligible local dev requests, returning 404 or 403 for ineligible configurations or invalid headers.
  • The web client silently authenticates against local dev servers during development using shouldAttemptDevPairing in auth.ts, skipping the pairing screen when on a loopback host in dev mode without an explicit token.
  • Startup no longer mints a pairing URL for eligible dev servers; resolveStartupBrowserTarget in serverRuntimeStartup.ts now returns a plain base URL with requiresPairing=false instead.
  • Adds host parsing and loopback validation utilities in netHost.ts; the startupAccess.ts loopback check is now stricter (exact IPv4 match instead of startsWith("127.")).
  • Behavioral Change: listPairingLinks excludes dev-auto-bootstrap subjects by default, so dev pairing credentials do not appear in pairing-links listings.

Macroscope summarized 929d35a.

Summary by CodeRabbit

  • New Features

    • Development servers can now authenticate browsers silently on eligible localhost configurations, avoiding manual pairing.
    • Startup automatically opens the development server or requests pairing when authentication is required.
    • Added secure validation for development pairing requests and one-time administrative credentials.
  • Documentation

    • Added guidance for browser verification, development-instance authentication, shared state, and localhost cookie behavior.
    • Documented the development silent-pairing endpoint and its eligibility rules.
  • Tests

    • Added coverage for pairing security, loopback hosts, startup authentication, and silent browser authentication.

Local web dev previously required scraping a one-time, 5-minute
/pair#token URL from dev-runner stdout for every fresh browser context,
which made parallel-instance work and agent browser verification
painful. The web app now authenticates automatically against eligible
dev servers via a guarded dev-only endpoint.

Server: POST /api/auth/dev-pairing-token mints a one-time administrative
credential (subject dev-auto-bootstrap, hidden from pairing-link
listings) only when the server is web-mode, loopback-bound
(loopback-browser policy), and configured with a loopback dev URL.
Requests must carry an Origin exactly matching the dev origin (survives
the Vite proxy, unlike Host) and a loopback Host header (DNS-rebinding
defense). Ineligible configs answer an explicit 404 — the handler is
always mounted so the SPA/redirect wildcard can't shadow it.

Startup: eligible dev servers now open the plain dev URL instead of
minting a startup pairing credential the silent path would leave
unconsumed in the URL and scrollback.

Web: the auth-gate bootstrap silently tries the endpoint when
unauthenticated in dev (never in hosted/desktop contexts, never when an
explicit #token is present) and falls back to the pairing screen on any
failure. import.meta.env.DEV keeps the request out of prod bundles.

Extracted host parsing into netHost.ts (auth/http.ts importing the old
helper from http.ts would have been a module cycle; the old helper also
mishandled Host headers with ports).

Docs: AGENTS.md browser-verification section, dev-instance auth notes in
scripts.md (plus stale T3CODE_STATE_DIR fix), and a security spec in
environment-auth.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 12.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title is concise and accurately summarizes the main change: silent pairing for local web-dev servers.
Description check ✅ Passed The description is detailed and covers what changed, why, behavior, docs, and verification, so it is mostly complete despite missing the exact template headings.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch t3code/disable-pairing-codes

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:L 100-499 changed lines (additions + deletions). labels Jul 14, 2026
Comment thread apps/server/src/netHost.ts
Comment thread apps/server/src/netHost.test.ts
@macroscopeapp

macroscopeapp Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

This PR introduces a new authentication endpoint that mints administrative credentials for dev servers. An unresolved review comment identifies a potential security bypass when Vite is LAN-exposed, allowing remote Origin forgery. Auth changes with open security concerns warrant human review.

You can customize Macroscope's approvability policy. Learn more.

t3dotgg and others added 2 commits July 13, 2026 18:19
A prefix test also accepted DNS names like 127.attacker.example, which
would pass both the devUrl eligibility check and the Host header guard
(review finding from macroscope).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Want fixes drafted automatically? Bugbot Autofix can create code changes for findings. A team admin can enable Autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 929d35a. Configure here.

return "host_not_loopback";
}
return null;
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Proxied Origin forge bypass

Medium Severity

The Host loopback check is defeated for requests through the Vite /api proxy because changeOrigin rewrites Host to the loopback upstream. Eligibility only constrains the server bind address, not the Vite listen address. If Vite is LAN-exposed (HOST=0.0.0.0), a remote client can forge Origin to the configured devUrl origin and mint an administrative pairing credential without local filesystem access.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 929d35a. Configure here.

@t3dotgg

t3dotgg commented Jul 30, 2026

Copy link
Copy Markdown
Member Author

Closing because the dev-auth direction has been overtaken by isolated worktree state and single-origin sharing in #4555/#4556 plus explicit fresh pairing in #4955. That path is simpler and avoids preserving a reusable silent administrative bootstrap surface.

@t3dotgg t3dotgg closed this Jul 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L 100-499 changed lines (additions + deletions). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant