Skip to content

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

Open
sneub wants to merge 1 commit into
mainfrom
feat/add-remote-auth-workaround
Open

Print auth URL to console to support a remote auth flow#32
sneub wants to merge 1 commit 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

Summary by CodeRabbit

  • New Features

    • Added alternate "paste URL" login flow for environments where browser-based authentication is unavailable.
  • Bug Fixes

    • Improved handling of late authentication callback requests to prevent duplicate token exchanges.
    • Enhanced error message handling to provide clearer feedback during authentication failures.
  • Improvements

    • Login instructions now include fallback guidance for systems without browser access.

Walkthrough

The login function now supports a paste URL flow as an alternative to HTTP browser callback. The HTTP handler detects late requests after token exchange and returns success without re-processing. LoginOptions and LoginState accept configurable input/output streams, with a completed flag coordinating between both completion paths. Login instructions print before opening the browser, and browser failures are non-fatal.

Changes

CLI login paste URL alternative flow

Layer / File(s) Summary
Stream integration and state coordination
packages/cli/src/lib/auth/login.ts
LoginOptions interface adds optional input/output streams. LoginState constructor accepts and stores output stream while adding a completed flag for HTTP/paste coordination. Imports readline/promises and Stream types.
Paste prompt and HTTP callback racing
packages/cli/src/lib/auth/login.ts
waitForPastedCallback prompts for a pasted callback URL (only when input is TTY) with abort handling. Promise.race coordinates between HTTP callback and paste completion paths with cleanup in finally. HTTP /auth/callback handler detects late requests after completed flag is set and returns success HTML without token re-exchange.
Login instructions and error handling
packages/cli/src/lib/auth/login.ts
openLoginPage prints instructions to output stream (TTY-only) before opening browser; openUrl failure is wrapped in try/catch for graceful fallback. Callback error handling normalizes non-Error thrown values to generic "Unknown error during login". resolveWorkspaceName return logic refactored for clearer trimming and null validation.

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

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.
Description check ❓ Inconclusive No pull request description was provided by the author, making it impossible to assess whether it relates to the changeset. Add a pull request description explaining the changes, the motivation for the remote auth flow support, and any relevant implementation details.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: printing auth URL to console for remote auth flow, which aligns with the conditional paste URL prompt and fallback for browser-unavailable scenarios.
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

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint skipped: no ESLint configuration detected in root package.json. To enable, add eslint to devDependencies.


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 on lines +76 to +82
if (completed) {
// The paste path already completed the token exchange. Render the
// success page anyway so a late browser callback isn't left dangling.
const workspaceName = await state.resolveWorkspaceName();
res.setHeader("Content-Type", "text/html; charset=utf-8");
res.end(renderSuccessPage(workspaceName));
return;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Don't tear down the listener before a late browser redirect can hit the success branch.

Once the pasted flow wins Promise.race(...), Line 117 starts closing the server immediately. A browser redirect that arrives after that point never reaches the completed branch on Lines 76-82, so the browser still gets a connection failure instead of the success page this branch is trying to preserve.

Also applies to: 115-119

🤖 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 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.

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.

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