wip: add headless t3 connect setup for SSH boxes#3749
Conversation
Phase 1 of the headless `t3 connect` flow (.plans/t3-connect-remote-setup.html). The CLI's OAuth login hardcodes a 127.0.0.1 loopback redirect, which cannot work on a machine reached over SSH. These hosted pages give the CLI an out-of-band leg for the same Clerk PKCE flow: - /connect reads state + code_challenge from the URL fragment, waits for a Clerk session, and forwards to Clerk's authorize endpoint. - /connect/callback shows the account being connected plus a one-time code.state blob to paste back into the waiting terminal, refusing to render codes for requests this browser did not start. Both routes bounce to / unless running as the hosted static app, since the same bundle ships inside local instances. The fragment carries no secrets; the PKCE verifier never leaves the CLI box, so an observed code cannot be exchanged. packages/shared/connectAuth.ts holds the URL/blob codecs shared with the CLI side, and VITE_CLERK_CLI_OAUTH_CLIENT_ID exposes the existing CLI OAuth client id to the web build. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Phase 2 of the headless connect flow (.plans/t3-connect-remote-setup.html). CliTokenManager gains pasteCodeLogin: it generates the same PKCE verifier/challenge/state as the loopback flow, prints a hosted app.t3.codes/connect URL instead of binding 127.0.0.1:34338, and exchanges the pasted code.state blob (validating state to keep the CSRF check). The prompt is injected by the caller so the flow is testable without a TTY. `t3 connect` (bare) now runs the whole setup — relay client install, authorization, desired-link flag — instead of requiring login + link as separate steps. Paste mode is auto-selected inside SSH sessions (SSH_CONNECTION/SSH_TTY) and available anywhere via --paste; desktop keeps the loopback browser flow unchanged. T3CODE_HOSTED_APP_URL overrides the hosted origin for staging. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Phase 3 of the headless connect flow (.plans/t3-connect-remote-setup.html). After linking, `t3 connect` asks whether T3 Code should run in the background whenever the machine boots. Saying yes installs a systemd user unit running `t3 serve`, enables it immediately, and runs `loginctl enable-linger` so the service survives SSH logout and reboots. Linux-only for now; other platforms get a clear skip message. `t3 connect logout` removes the unit again. Because systemd user units run with a minimal environment and fail invisibly, the unit uses only absolute paths (node binary + entry point) and appends service output and install failures to a log file whose path is printed at setup time. When the CLI is running out of the ephemeral npx cache, the exact running version is first pinned via `npm install --prefix` into ~/.t3/runtime/versions/<v> — a real install (t3 ships native deps like node-pty), and never `npx t3@latest` in the unit, which would make boot depend on the npm registry. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review round on the headless-connect branch, focused on simplicity and reliability. Reliability fixes: - Boot service: quote/escape systemd unit values (paths with spaces or percents produced word-split ExecStart lines); drop the inert After=network-online.target (not a user-manager unit); add WorkingDirectory=%h so boot-started servers don't run from /; run `loginctl enable-linger` without a username ($USER can be stale or unset); give the pinned npm install a 10-minute timeout (default 60s killed healthy node-pty builds) and record success with a sentinel, removing the half-installed tree on failure so retries don't enable a crashlooping service; detect pnpm dlx / bunx caches as ephemeral, not just npx. - BootService.status now reports supported/current: connect no longer prompts on platforms where install can only fail, offers a repair when the installed unit is stale (old runtime path after an upgrade), and a boot-service failure warns instead of failing the whole command after connect already succeeded. uninstall reports whether it removed a unit and logout no longer pre-checks status (a status error used to silently skip removal). - Paste flow: a stored credential whose refresh fails now falls through to a fresh sign-in instead of dead-ending the command, and the paste prompt gets the same 10-minute timeout as the loopback flow. - Callback page: read the expected state without deleting it — consume-on-render was eaten by StrictMode's double render, disabling the CSRF check; add a Sign in button for a dismissed Clerk modal. Simplicity/duplication fixes: single DEFAULT_HOSTED_APP_URL and readHashParams in packages/shared; the loopback flow now builds its authorize URL with the shared builder; one checkConnectAuthCode used by both the prompt validation and the authoritative re-check; shared AuthSurfaceShell for the pairing/connect pages; route gating moved to cloud/connectCliAuth (routes no longer import from each other); detectHeadlessSession reads HostProcessEnvironment; dropped the duplicated cliVersion on BootServiceHost and unused exports. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…talls Round-2 review fixes, each verified against a live systemd user manager: - `systemctl --user enable --now` does not replace an already running process, so repairing a stale unit swapped the file but kept the old server running until reboot. Split into enable + restart. - StandardOutput=append: paths go through systemd specifier expansion but must not be quoted; a `%` in the home path broke standard-output setup and killed the service. Escape % (only) in the append paths. - If an activation step failed after the unit file was written, the next `t3 connect` saw the file and reported the service as already set up even though enable/linger never ran. Remove the unit again when activation fails so the retry actually repairs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ot-service edges
Fixes from three independent Fable review agents (general / simplify /
over-engineering) on the whole branch.
Security:
- The paste flow links whichever account completed the browser auth, and
the callback page previously failed open (no recorded state) and labeled
the code with the local session's account. An attacker who saw a victim's
printed URL could get the victim to link their machine to the attacker's
account. Two mitigations: the callback now fails closed (a missing or
mismatched state is refused, not displayed), and the CLI prints the
account it actually linked ("Connected as theo@…") by reading the OIDC
id_token, so account substitution is visible before the machine comes
online — matching the original plan's mockup.
Correctness/UX:
- Loopback `t3 connect` now falls through to a fresh login when the stored
credential can't be refreshed, instead of dead-ending — the desktop
counterpart of the paste-side fallback.
- Ctrl-C / EOF at the paste prompt propagates as QuitError (quiet cancel)
instead of being wrapped into an authorization-error dump.
Boot service:
- `current` now also requires the unit's entry point to exist, so a deleted
pinned runtime triggers a repair instead of "already set up".
- Failed activation rolls back with disable + remove + daemon-reload (was
remove only), so no dangling enable symlink or stale definition survives.
- Add StartLimitIntervalSec/Burst so a persistently broken service stops
instead of restarting every 5s forever and growing the append log.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 3 potential issues.
❌ 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 7e75fa2. Configure here.
| } | ||
| if (!isSignedIn) { | ||
| clerk.openSignIn({ forceRedirectUrl: window.location.href }); | ||
| return; |
There was a problem hiding this comment.
Sign-in drops connect hash
Medium Severity
The /connect page keeps the CLI PKCE state and challenge only in the URL fragment and reads them once on mount. When the user is not signed in, Clerk sign-in is started with forceRedirectUrl pointing at that URL; after a full-page redirect sign-in (typical for SSO), the fragment is often stripped, readConnectAuthorizeRequest returns null, and the flow stops at “incomplete connect link” before Clerk authorization or paste code can run.
Reviewed by Cursor Bugbot for commit 7e75fa2. Configure here.
| } catch { | ||
| // Session storage can be unavailable (e.g. blocked). The callback page | ||
| // then falls back to trusting the state Clerk echoed back. | ||
| } |
There was a problem hiding this comment.
Blocked storage breaks callback
Medium Severity
The callback page requires sessionStorage to hold the connect state written on /connect, and rejects the OAuth response when that value is missing. If sessionStorage is unavailable or the write fails, rememberConnectCliAuthState fails silently and a legitimate Clerk callback still shows “different request” with no paste code, so the SSH setup cannot finish.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 7e75fa2. Configure here.
| .exists(runtimeSentinelPath) | ||
| .pipe(Effect.mapError((cause) => new BootServiceInstallError({ cause }))); | ||
| if (alreadyPinned) { | ||
| return; |
There was a problem hiding this comment.
Pinned runtime repair skipped incorrectly
Medium Severity
ensurePinnedRuntime treats a present .install-complete sentinel as proof the pinned install is usable and skips npm install, but status marks the unit stale when plannedEntryPath is missing (e.g. after deleting ~/.t3/runtime to reclaim space). Accepting connect’s repair prompt then rewrites/restarts the unit without restoring the runtime, so the service keeps failing until the sentinel is removed manually.
Reviewed by Cursor Bugbot for commit 7e75fa2. Configure here.
|
|
||
| // Connect itself already succeeded; a boot-service failure must not | ||
| // fail the command, just tell the user what happened and move on. | ||
| const background = yield* offerBootService.pipe( |
There was a problem hiding this comment.
🟡 Medium cli/connect.ts:608
When the offerBootService prompt cannot be answered (closed stdin, Ctrl-C, or EOF), Prompt.run fails with a Terminal.QuitError. The caller in connectCommand only catches BootServiceUnsupportedError, BootServiceCommandError, and BootServiceInstallError, so the quit error propagates and aborts the t3 connect command — even though the environment was already linked successfully. Consider catching Terminal.QuitError alongside the boot-service errors so the command completes instead of terminating after a successful link.
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/server/src/cli/connect.ts around line 608:
When the `offerBootService` prompt cannot be answered (closed stdin, Ctrl-C, or EOF), `Prompt.run` fails with a `Terminal.QuitError`. The caller in `connectCommand` only catches `BootServiceUnsupportedError`, `BootServiceCommandError`, and `BootServiceInstallError`, so the quit error propagates and aborts the `t3 connect` command — even though the environment was already linked successfully. Consider catching `Terminal.QuitError` alongside the boot-service errors so the command completes instead of terminating after a successful link.
| // Show which account was linked so an unexpected identity (a pasted | ||
| // code that authorized a different account) is visible before the | ||
| // machine is brought online. | ||
| yield* Console.log(`\nConnected${connectedAs(linked.identity)}!`); |
There was a problem hiding this comment.
🟡 Medium cli/connect.ts:604
The t3 connect command prints Connected${connectedAs(linked.identity)}!, but linked.identity is null whenever authorizeCli reuses an existing stored credential (the common path for already-authenticated machines). In that case it prints only Connected!, so the operator cannot see which account is linked — the identity-visibility safety check this command is meant to provide silently disappears for already-authenticated machines. Consider resolving the connected account identity from the stored token in the reuse path, not only after a fresh paste-code login.
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/server/src/cli/connect.ts around line 604:
The `t3 connect` command prints `Connected${connectedAs(linked.identity)}!`, but `linked.identity` is `null` whenever `authorizeCli` reuses an existing stored credential (the common path for already-authenticated machines). In that case it prints only `Connected!`, so the operator cannot see which account is linked — the identity-visibility safety check this command is meant to provide silently disappears for already-authenticated machines. Consider resolving the connected account identity from the stored token in the reuse path, not only after a fresh paste-code login.
| const alreadyPinned = yield* fs | ||
| .exists(runtimeSentinelPath) | ||
| .pipe(Effect.mapError((cause) => new BootServiceInstallError({ cause }))); | ||
| if (alreadyPinned) { | ||
| return; | ||
| } |
There was a problem hiding this comment.
🟡 Medium cloud/bootService.ts:259
ensurePinnedRuntime returns early when the .install-complete sentinel exists, but status separately checks whether plannedEntryPath actually exists on disk. If the pinned runtime tree is partially deleted after a successful install (user cleans ~/.t3/runtime/... but leaves the sentinel), status reports the service as stale, install is called to repair it, and line 262 skips reinstallation because the sentinel is still present. The rewritten unit still points at the missing runtimeEntryPath, so systemctl restart fails and the repair can never recover without manually deleting the sentinel. Consider checking that plannedEntryPath exists alongside the sentinel, or checking the entry file directly, before skipping the install.
const alreadyPinned = yield* fs
.exists(runtimeSentinelPath)
.pipe(Effect.mapError((cause) => new BootServiceInstallError({ cause })));
if (alreadyPinned) {
- return;
+ const entryMissing = yield* fs
+ .exists(plannedEntryPath)
+ .pipe(Effect.mapError((cause) => new BootServiceInstallError({ cause })));
+ if (entryMissing) {
+ return;
+ }
}🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/server/src/cloud/bootService.ts around lines 259-264:
`ensurePinnedRuntime` returns early when the `.install-complete` sentinel exists, but `status` separately checks whether `plannedEntryPath` actually exists on disk. If the pinned runtime tree is partially deleted after a successful install (user cleans `~/.t3/runtime/...` but leaves the sentinel), `status` reports the service as stale, `install` is called to repair it, and line 262 skips reinstallation because the sentinel is still present. The rewritten unit still points at the missing `runtimeEntryPath`, so `systemctl restart` fails and the repair can never recover without manually deleting the sentinel. Consider checking that `plannedEntryPath` exists alongside the sentinel, or checking the entry file directly, before skipping the install.
| const EPHEMERAL_CACHE_SEGMENTS = [ | ||
| "/_npx/", // npx | ||
| "\\_npx\\", | ||
| "/pnpm/dlx", // pnpm dlx (~/.cache/pnpm/dlx and $PNPM_HOME/.pnpm/dlx) | ||
| "/.pnpm/dlx", | ||
| "/.bun/install/cache/", // bunx | ||
| ]; |
There was a problem hiding this comment.
🟡 Medium cloud/bootService.ts:29
The pnpm segments "/pnpm/dlx" and "/.pnpm/dlx" in EPHEMERAL_CACHE_SEGMENTS are matched via entryPath.includes(segment) without a trailing path boundary. A stable checkout under a path like /home/me/dev/pnpm/dlx-tools/t3 matches isEphemeralCacheEntry(), so install treats it as ephemeral, runs npm install unnecessarily, and writes the unit to the published t3@<version> runtime instead of the actual checkout the user invoked. That makes the boot service run different code from the linked CLI and can fail on offline machines even though the original checkout was stable. Consider anchoring these segments with a trailing / (e.g. "/pnpm/dlx/", "/.pnpm/dlx/") so dlx-tools no longer matches.
| const EPHEMERAL_CACHE_SEGMENTS = [ | |
| "/_npx/", // npx | |
| "\\_npx\\", | |
| "/pnpm/dlx", // pnpm dlx (~/.cache/pnpm/dlx and $PNPM_HOME/.pnpm/dlx) | |
| "/.pnpm/dlx", | |
| "/.bun/install/cache/", // bunx | |
| ]; | |
| +const EPHEMERAL_CACHE_SEGMENTS = [ | |
| + "/_npx/", // npx | |
| + "\\_npx\\", | |
| + "/pnpm/dlx/", // pnpm dlx (~/.cache/pnpm/dlx and $PNPM_HOME/.pnpm/dlx) | |
| + "/.pnpm/dlx/", | |
| + "/.bun/install/cache/", // bunx | |
| +]; |
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/server/src/cloud/bootService.ts around lines 29-35:
The pnpm segments `"/pnpm/dlx"` and `"/.pnpm/dlx"` in `EPHEMERAL_CACHE_SEGMENTS` are matched via `entryPath.includes(segment)` without a trailing path boundary. A stable checkout under a path like `/home/me/dev/pnpm/dlx-tools/t3` matches `isEphemeralCacheEntry()`, so `install` treats it as ephemeral, runs `npm install` unnecessarily, and writes the unit to the published `t3@<version>` runtime instead of the actual checkout the user invoked. That makes the boot service run different code from the linked CLI and can fail on offline machines even though the original checkout was stable. Consider anchoring these segments with a trailing `/` (e.g. `"/pnpm/dlx/"`, `"/.pnpm/dlx/"`) so `dlx-tools` no longer matches.
| * machines. Overridable so staging/nightly builds can point their CLIs at a | ||
| * matching hosted deployment. | ||
| */ | ||
| export const hostedAppUrlConfig = makePublicValueConfig( |
There was a problem hiding this comment.
🟡 Medium cloud/publicConfig.ts:109
hostedAppUrlConfig accepts any trimmed non-empty string for T3CODE_HOSTED_APP_URL, so a base-less value like app.t3.codes passes validation. When pasteCodeLogin passes that value into buildConnectAuthorizeRequestUrl() and connectCallbackUrl(), the new URL(..., hostedAppUrl) calls throw at runtime instead of failing at config load time. Consider validating that the fallback and runtime value parse as an absolute HTTPS URL, the same way makeRelayUrlConfig uses normalizeSecureRelayUrl.
Also found in 1 other location(s)
apps/web/src/cloud/connectCliAuth.ts:48
buildConnectCliClerkAuthorizeUrlcallsclerkFrontendApiUrlFromPublishableKey(clerkPublishableKey)at line 48 without handling its parse errors.hasConnectCliAuthConfig()/hasCloudPublicConfig()only check that the publishable key is non-empty, so a malformedVITE_CLERK_PUBLISHABLE_KEYleaves the routes enabled but makes/connectthrow during redirect construction instead of failing gracefully.
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/server/src/cloud/publicConfig.ts around line 109:
`hostedAppUrlConfig` accepts any trimmed non-empty string for `T3CODE_HOSTED_APP_URL`, so a base-less value like `app.t3.codes` passes validation. When `pasteCodeLogin` passes that value into `buildConnectAuthorizeRequestUrl()` and `connectCallbackUrl()`, the `new URL(..., hostedAppUrl)` calls throw at runtime instead of failing at config load time. Consider validating that the fallback and runtime value parse as an absolute HTTPS URL, the same way `makeRelayUrlConfig` uses `normalizeSecureRelayUrl`.
Also found in 1 other location(s):
- apps/web/src/cloud/connectCliAuth.ts:48 -- `buildConnectCliClerkAuthorizeUrl` calls `clerkFrontendApiUrlFromPublishableKey(clerkPublishableKey)` at line 48 without handling its parse errors. `hasConnectCliAuthConfig()`/`hasCloudPublicConfig()` only check that the publishable key is non-empty, so a malformed `VITE_CLERK_PUBLISHABLE_KEY` leaves the routes enabled but makes `/connect` throw during redirect construction instead of failing gracefully.
| yield* runStep("cleaning up the service", "systemctl", [ | ||
| "--user", | ||
| "disable", | ||
| BOOT_SERVICE_UNIT_FILE, | ||
| ]).pipe(Effect.ignore); |
There was a problem hiding this comment.
🟡 Medium cloud/bootService.ts:353
rollbackFailedInstall runs systemctl --user disable without --now, so when installation fails after the service has already been started (e.g. loginctl enable-linger fails at line 341), the rollback removes the unit file but leaves the t3code process running. The install command reports failure while the service stays active, violating the intended "leave nothing behind" cleanup. Consider using disable --now (or an explicit stop) in the rollback so the running process is also stopped.
yield* runStep("cleaning up the service", "systemctl", [
"--user",
- "disable",
+ "disable",
+ "--now",
BOOT_SERVICE_UNIT_FILE,
]).pipe(Effect.ignore);🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/server/src/cloud/bootService.ts around lines 353-357:
`rollbackFailedInstall` runs `systemctl --user disable` without `--now`, so when installation fails after the service has already been started (e.g. `loginctl enable-linger` fails at line 341), the rollback removes the unit file but leaves the `t3code` process running. The install command reports failure while the service stays active, violating the intended "leave nothing behind" cleanup. Consider using `disable --now` (or an explicit `stop`) in the rollback so the running process is also stopped.
| return { state, challenge }; | ||
| } | ||
|
|
||
| export function connectCallbackUrl(hostedAppUrl: string): string { |
There was a problem hiding this comment.
🟠 High src/connectAuth.ts:56
connectCallbackUrl derives the redirect URI from whatever base URL it receives, so the /connect page (using window.location.origin) and the CLI (using the configured hostedAppUrl) produce different redirect_uri values on alias origins like nightly.app.t3.codes. Since isHostedStaticApp treats alias origins as valid, these can diverge. OAuth 2.0 requires the token request's redirect_uri to match the authorization request's exactly, so codes issued for the alias callback URL are rejected when the CLI sends the canonical callback URL. Consider deriving the redirect URI from a single canonical source (e.g., DEFAULT_HOSTED_APP_URL or a resolved canonical origin) rather than letting each side compute it independently.
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @packages/shared/src/connectAuth.ts around line 56:
`connectCallbackUrl` derives the redirect URI from whatever base URL it receives, so the `/connect` page (using `window.location.origin`) and the CLI (using the configured `hostedAppUrl`) produce different `redirect_uri` values on alias origins like `nightly.app.t3.codes`. Since `isHostedStaticApp` treats alias origins as valid, these can diverge. OAuth 2.0 requires the token request's `redirect_uri` to match the authorization request's exactly, so codes issued for the alias callback URL are rejected when the CLI sends the canonical callback URL. Consider deriving the redirect URI from a single canonical source (e.g., `DEFAULT_HOSTED_APP_URL` or a resolved canonical origin) rather than letting each side compute it independently.
ApprovabilityVerdict: Needs human review 7 blocking correctness issues found. This PR introduces a significant new feature (headless SSH connect flow) with new OAuth authentication paths, systemd service management, and multiple web routes. Additionally, there are 9 unresolved review comments identifying potential bugs, including a high-severity OAuth redirect_uri mismatch and multiple medium-severity issues in core functionality. You can customize Macroscope's approvability policy. Learn more. |
t3 connect setup for SSH boxest3 connect setup for SSH boxes


What Changed
Makes
npx t3 connecta one-command setup path for machines with no local browser (SSH boxes), matching the existing loopback flow's capabilities:app.t3.codes/connectand/connect/callbackrun the same Clerk PKCE flow as the existing loopback OAuth, but out-of-band: the CLI prints a URL, the user opens it on any machine with a browser, and pastes a one-time code back into the terminal. Auto-selected inside SSH sessions;--pasteforces it anywhere. Desktop's loopback flow is untouched.t3 connectnow does relay-client install + authorize + link in one shot (previouslylogin+linkas separate steps), and prints the linked account (Connected as theo@…) so an unexpected identity is visible before the machine comes online.t3 serve) +loginctl enable-linger, so the machine stays reachable through T3 Connect after the SSH session ends. Runs a pinned runtime under~/.t3/runtime(nevernpx t3@latestin the unit, since that would make boot depend on the npm registry) when the CLI is invoked from an ephemeral cache (npx/pnpm dlx/bunx).Design writeup:
.plans/t3-connect-remote-setup.html.Six commits: three feature phases, three review-fix rounds (multi-agent review + two independent gpt-5.5/Fable passes each caught real issues — systemd unit escaping/timeouts/rollback, a CSRF fail-open in the callback page, a credential-refresh dead end).
Why
Setting up T3 Connect on a remote dev box previously required forwarding a local port for the OAuth callback (
ssh -L 34338:...) or wasn't practical at all. This gives SSH users the same one-command setup local users already have, with the account being linked always visible.UI Changes
Two new pages, both plain informational/redirect surfaces (no interaction beyond a copy button and a sign-in button), reusing the existing pairing-page visual treatment (
AuthSurfaceShell, shared withPairingRouteSurface):/connect— waits for a Clerk session, then forwards to Clerk's authorize endpoint. Static "Connecting your terminal…" message with a Sign-in button if not authenticated./connect/callback— shows the account being linked and a one-time code to paste into the terminal, with a warning against pasting into an untrusted session.No motion/animation involved.
Checklist
Test plan
%h, restart-on-repair, rollback-on-failed-activation) against a real systemd user managerSSH_CONNECTION(prints URL, prompts, validates/rejects pasted codes)https://app.t3.codes/connect/callbackregistered as an allowed redirect URI before this works against production Clerk — this is the one external dependency called out in the design doc🤖 Generated with Claude Code
Note
Medium Risk
Touches Clerk OAuth (paste flow, callback URI, PKCE) and installs a persistent systemd user service that keeps the machine reachable via T3 Connect; misconfiguration or partial install is mitigated by rollback and logging but still affects machine exposure.
Overview
Enables one-shot
t3 connecton machines without a local browser (typical SSH boxes) by swapping the loopback OAuth callback for a hosted paste-code flow, while keeping desktop loopback behavior unchanged.Hosted auth (web + shared): New
@t3tools/shared/connectAuthcentralizes PKCE URL building,code.statepaste blobs, and state checks. The hosted app adds/connectand/connect/callback(gated to hosted + Clerk CLI OAuth config), sign-in → Clerk authorize → copyable code with sessionStorage state verification. Config addsT3CODE_HOSTED_APP_URL/VITE_CLERK_CLI_OAUTH_CLIENT_ID.CLI:
CliTokenManager.pasteCodeLoginprints the hosted URL, validates pasted codes, exchanges with PKCE, and surfaces linked identity fromid_token.authorizeCliauto-uses paste in SSH (SSH_CONNECTION/SSH_TTY) or with--paste; baret3 connectnow installs the relay client, authorizes, links, and showsConnected as ….login/linkshare the same paths; refresh failures fall through to re-auth instead of dead-ending.Boot service (Linux): New
bootServicewrites a systemd user unit fort3 serve, pinst3@versionunder~/.t3/runtimewhen invoked from npx-style caches, enables linger, logs failures, and rolls back partial installs;connectprompts to install,logoutuninstalls.Also adds a design doc (
.plans/t3-connect-remote-setup.html) and broad unit tests for auth and systemd rendering.Reviewed by Cursor Bugbot for commit 7e75fa2. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Add headless
t3 connectsetup with paste-code OAuth flow for SSH environmentspasteCodeLoginflow toCliTokenManagerthat performs PKCE-based OAuth without a local HTTP callback: the CLI prints an authorize URL, the user pastes back an encoded code+state blob, and tokens (including identity fromid_token) are exchanged and persisted.--pasteflag anddetectHeadlessSessionto theconnect,connect login, andconnect linkcommands; SSH sessions auto-select paste-code mode.BootServiceto manage a per-user systemd unit and pinned runtime for T3 Code on Linux, with install/uninstall/status and rollback on failure;t3 connectnow offers to set it up interactively./connectand/connect/callbackweb routes (behindconnectCliAuthRoutesEnabled) that handle PKCE redirect to Clerk and display the pasteable auth code blob for the user to copy.packages/shared/src/connectAuth.tswith shared URL builders, code encoder/validator, and constants used by both the CLI and web app.CliTokenManager.get()now falls back to a fresh login instead of failing when stored credentials are unreadable or refresh fails, which could trigger unexpected interactive prompts.📊 Macroscope summarized 7e75fa2. 17 files reviewed, 0 issues evaluated, 0 issues filtered, 0 comments posted
🗂️ Filtered Issues
No issues evaluated.