Skip to content

Print auth URL to console to support a remote auth flow#32

Merged
luanvdw merged 3 commits into
mainfrom
feat/add-remote-auth-workaround
Jun 2, 2026
Merged

Print auth URL to console to support a remote auth flow#32
luanvdw merged 3 commits into
mainfrom
feat/add-remote-auth-workaround

Conversation

@sneub
Copy link
Copy Markdown
Contributor

@sneub sneub commented May 25, 2026

Summary

prisma-cli auth login opens a browser and waits on a localhost:<port>/auth/callback
HTTP server for the OAuth redirect. This breaks on remote machines (SSH, Docker,
cloud VMs): no browser to open, and even if the user copies the URL to a local
browser, the redirect lands on the local machine's localhost, never reaching
the remote CLI.

This change adds a second completion path — paste-the-callback-URL — that runs in
parallel with the existing browser callback. No backend changes, no SDK changes,
no new flag. Laptop users see no behavior change; remote users get a path that
actually works.

How it works

  • The auth URL is now also printed to stderr with brief instructions.
  • The local HTTP server still runs as before.
  • A readline prompt (Paste URL here:) runs in parallel when stdin is a TTY.
  • Promise.race([httpCallback, pastedUrl]) — whichever delivers a callback URL
    first wins. Both paths call the same state.handleCallback(url).
  • A completed guard ensures the OAuth code exchange runs exactly once even if
    both fire close together; the losing path is aborted via AbortController and
    the existing server-close finally.
  • open(url) failures are now swallowed instead of bubbling — the paste path
    is a viable fallback when the browser launch silently fails on headless boxes.

UX notes

  • On non-TTY stdin (CI, piped, tests) the paste prompt is skipped entirely;
    behavior is identical to today.
  • Accepts the full callback URL (not just the code) so we can keep state
    validation intact with zero new parsing logic.

Scope

Single file: packages/cli/src/lib/auth/login.ts (~70 net lines). No changes to
the controller, use-cases, types, SDK, or any backend/OAuth-provider config.

@sneub sneub requested a review from luanvdw May 25, 2026 13:14
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 25, 2026

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 13c17730-4d01-4948-a54a-e464796b0926

📥 Commits

Reviewing files that changed from the base of the PR and between d0da531 and 20dbbcb.

📒 Files selected for processing (2)
  • packages/cli/src/lib/auth/login.ts
  • packages/cli/tests/auth-login.test.ts

Summary by CodeRabbit

  • New Features

    • Added alternate "paste URL" login flow for TTY/interactive environments.
  • Bug Fixes

    • Prevented duplicate token exchanges for late authentication callbacks.
    • Normalized callback error handling to surface clearer auth failures.
  • Improvements

    • Interactive login prints fallback instructions and treats browser-open failures as non-fatal.
    • Trimmed/validated workspace name handling during login.
  • Tests

    • Added tests covering the remote paste callback URL sign-in path.

Walkthrough

The login function adds a TTY “paste callback URL” alternative that races against the HTTP /auth/callback handler using a shared completion promise/flag. LoginOptions/LoginState accept configurable input/output streams; interactive mode prints instructions and treats browser-open failures as non-fatal. Callback error normalization and workspace-name trimming were clarified.

Changes

CLI login paste URL alternative flow

Layer / File(s) Summary
Stream integration and state coordination
packages/cli/src/lib/auth/login.ts
Adds readline/promises and Readable/Writable imports. LoginOptions gains input?: Readable and output?: Writable. LoginState stores output and uses a completed flag to coordinate completion between paths.
Paste prompt and HTTP callback racing
packages/cli/src/lib/auth/login.ts
Introduces paste-prompt control flow (consumePastedCallback) used only for interactive TTY input; Promise.race between paste and HTTP callback coordinates completion with cleanup; HTTP /auth/callback short-circuits when completed is already true and returns success HTML without re-exchanging tokens.
Login instructions and error handling
packages/cli/src/lib/auth/login.ts
openLoginPage prints interactive instructions to configured output and wraps openUrl so browser-open failures are non-fatal when interactive. Non-Error throws during token exchange are normalized into an AuthError. resolveWorkspaceName trimming/return logic is made explicit.
Tests: remote paste flow and formatting
packages/cli/tests/auth-login.test.ts
Adds PassThrough-based tests exercising the remote paste flow and helpers (runLogin, PASTE_CALLBACK_URL). Reformats several test helpers and mocks without changing their behavior.

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% 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
Title check ✅ Passed The title clearly summarizes the main change: adding support for a remote auth flow by printing the auth URL to console as an alternative to browser-based login.
Description check ✅ Passed The description is directly related to the changeset, providing detailed context about the problem (remote machines cannot use browser-based auth), the solution (paste-the-callback-URL flow), implementation details, and UX considerations.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/add-remote-auth-workaround
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch feat/add-remote-auth-workaround

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@sneub sneub marked this pull request as ready for review May 25, 2026 13:46
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/cli/src/lib/auth/login.ts (1)

65-112: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Serialize the callback exchange instead of guarding it with completed.

completed is only set after await state.handleCallback(...) returns. If the browser redirect and the pasted URL arrive close together, both branches can enter handleCallback(...) with the same auth code. That makes the login outcome timing-dependent: one path can spend the code while the other rejects, and Promise.race(...) may fail even though the other path already completed successfully.

Suggested fix
-    let completed = false;
+    let completed = false;
+    let completionPromise: Promise<void> | undefined;
+
+    const completeOnce = (callbackUrl: URL): Promise<void> => {
+      if (!completionPromise) {
+        completionPromise = (async () => {
+          await state.handleCallback(callbackUrl);
+          completed = true;
+        })();
+      }
+
+      return completionPromise;
+    };

     const httpResult = new Promise<void>((resolve, reject) => {
       server.on("request", async (req, res) => {
@@
         try {
-          await state.handleCallback(url);
-          completed = true;
+          await completeOnce(url);
         } catch (error) {
@@
     const pasteResult = waitForPastedCallback({
       input,
       output,
       signal: pasteAbort.signal,
     }).then(async (pastedUrl) => {
       if (pastedUrl === null || completed) return;
-      await state.handleCallback(pastedUrl);
-      completed = true;
+      await completeOnce(pastedUrl);
     });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/lib/auth/login.ts` around lines 65 - 112, The race exists
because both the HTTP handler and the pasteResult branch call
state.handleCallback concurrently and only set completed after awaiting it;
replace the boolean-guard pattern by serializing the exchange: introduce a
single shared "inflight" promise/lock (e.g., handleCallbackPromise or similar)
that both the server request handler and the waitForPastedCallback .then
callback consult—if it's null they assign it to the Promise returned by
state.handleCallback(url) and await it, otherwise they await the existing
promise; set completed (or resolve the inflight promise) only after that promise
settles and use that shared symbol in place of the current completed checks so
handleCallback is invoked at most once (refer to completed,
state.handleCallback, the server "request" handler, and the pasteResult
callback).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/cli/src/lib/auth/login.ts`:
- Around line 76-82: The server is being closed immediately when the pasted-flow
loses the Promise.race, which prevents late browser redirects from reaching the
request handler's completed branch (the branch that calls
state.resolveWorkspaceName() and renderSuccessPage). Modify the shutdown logic
so the HTTP server isn't closed until after any in-flight request can be
handled: either move the server.close() call into the request handler after
sending the success response (ensure you call server.close() only once), or, if
you must close from the race winner path, wait for a short grace period or check
a shared completed flag and defer closing until completed is true or the current
response finishes; update the code paths that call server.close() around the
Promise.race handling (the shutdown block currently invoked after the race) to
use this deferred-close approach so renderSuccessPage and
state.resolveWorkspaceName() can complete.

---

Outside diff comments:
In `@packages/cli/src/lib/auth/login.ts`:
- Around line 65-112: The race exists because both the HTTP handler and the
pasteResult branch call state.handleCallback concurrently and only set completed
after awaiting it; replace the boolean-guard pattern by serializing the
exchange: introduce a single shared "inflight" promise/lock (e.g.,
handleCallbackPromise or similar) that both the server request handler and the
waitForPastedCallback .then callback consult—if it's null they assign it to the
Promise returned by state.handleCallback(url) and await it, otherwise they await
the existing promise; set completed (or resolve the inflight promise) only after
that promise settles and use that shared symbol in place of the current
completed checks so handleCallback is invoked at most once (refer to completed,
state.handleCallback, the server "request" handler, and the pasteResult
callback).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 48967437-4948-48bd-acb9-26b22d407a66

📥 Commits

Reviewing files that changed from the base of the PR and between b3c4e13 and d0da531.

📒 Files selected for processing (1)
  • packages/cli/src/lib/auth/login.ts

Comment thread packages/cli/src/lib/auth/login.ts
Copy link
Copy Markdown
Member

@luanvdw luanvdw left a comment

Choose a reason for hiding this comment

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

Thanks @sneub, this adds a solid QoL improvement and gives users a practical escape hatch when the CLI is running on one machine and the browser is on another machine!

Could you/your agent review these small changes before we merge this in?

  1. Only enable the paste fallback when we can actually prompt
    If stdin is not a TTY, there is nowhere for the user to paste the callback URL. In that case, let’s keep the existing browser-callback behaviour, i.e. don’t start/race the paste flow, and don’t treat paste as an available fallback if opening the browser fails.

Put differently, the paste path should only change behaviour in interactive terminals.

  1. Make completion run once
    Since the browser callback and pasted callback can both arrive, let’s centralize completion through a small completeOnce(callbackUrl) helper. That helper should make sure state.handleCallback(...) runs only once, and both paths await the same completion if they arrive close together.

  2. Make the remote-machine instruction more concrete
    I think “resulting callback URL” may be unclear to users. Could we make it more explicit
    Suggested copy:

Open this URL to sign in: <auth-url>

If the browser opens on another machine, finish sign-in there. When it redirects to localhost, copy the full localhost URL from the address bar and paste it here.
  1. Keep login alive after a bad paste
    If the user presses Enter too early or pastes the wrong thing, we should not end the whole login. Can we show a short message and ask again, while still leaving the browser callback path open? If this opens a big conditional loop, skip this for now - we can always return to this if needed.

- Gate the paste fallback on a TTY: only swallow browser-launch failures
  when stdin is interactive, and only start/race the paste prompt then.
  Non-TTY environments keep the existing browser-callback behaviour and
  surface a failed browser launch instead of hanging.
- Funnel both completion paths through a single completeOnce() so the
  token exchange runs at most once when the browser redirect and a pasted
  URL arrive together; clear the in-flight promise on failure to allow
  retries.
- Make the remote sign-in instructions concrete.
- Re-prompt after a premature Enter, an unparseable paste, or a callback
  the exchange rejects, instead of ending the whole login. The browser
  callback path stays open throughout.
@sneub
Copy link
Copy Markdown
Contributor Author

sneub commented Jun 2, 2026

Thanks @luanvdw — all four addressed in 20dbbcb (single file login.ts + tests):

  1. Paste fallback gated on a TTY. The paste prompt is now only created and raced when stdin.isTTY — non-TTY (CI, pipes, tests) keeps the existing browser-callback-only behaviour. Relatedly, the openUrl failure is only swallowed on a TTY; without one there's no paste escape hatch, so a failed browser launch now surfaces as an error again instead of hanging on a callback that'll never arrive.

  2. Completion runs once. Both paths now funnel through a completeOnce(callbackUrl) helper backed by a shared in-flight promise, so state.handleCallback(...) runs at most once even when the browser redirect and the paste land together — and both paths await the same result. The promise is cleared on failure so a retry can still try again.

  3. Concrete instruction copy. Adopted your suggested wording:

    Open this URL to sign in:

    If the browser opens on another machine, finish sign-in there. When it redirects to localhost, copy the full localhost URL from the address bar and paste it here.

  4. Login stays alive after a bad paste. A premature Enter, an unparseable paste, or a callback the exchange rejects now prints a short hint and re-prompts, with the browser callback path left open the whole time. It stayed contained (a small loop in one function), so I kept it in rather than deferring.

One CodeRabbit point I intentionally didn't act on: it flagged that the HTTP listener is torn down right after the race resolves, so a late browser redirect that arrives after the paste path already won won't get the success page. Once the paste path completes, the user is already signed in at their terminal — the only cost is a stray browser tab showing a connection error, which I judged not worth the deferred-shutdown complexity. Happy to revisit if you'd rather close that gap.

Tests added for: paste-only completion, run-once when both paths fire, re-prompt on bad paste / rejected exchange, and non-TTY browser-failure surfacing. Full suite green (232 tests).

Copy link
Copy Markdown
Member

@luanvdw luanvdw left a comment

Choose a reason for hiding this comment

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

Re-reviewed after branch update. Previous feedback is addressed, and checks are green.

@luanvdw luanvdw merged commit 2ffc6dd into main Jun 2, 2026
5 checks passed
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