Skip to content

feat: [AI-7520] Altimate LLM Gateway CLI signup + onboarding UX#1001

Draft
saravmajestic wants to merge 10 commits into
mainfrom
feat/AI-7520-social-signup-authorize
Draft

feat: [AI-7520] Altimate LLM Gateway CLI signup + onboarding UX#1001
saravmajestic wants to merge 10 commits into
mainfrom
feat/AI-7520-social-signup-authorize

Conversation

@saravmajestic

@saravmajestic saravmajestic commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Summary

CLI onboarding for the Altimate LLM Gateway + a welcoming first-run experience, plus quick sign-in/out commands.

Gateway sign-in

  • Loopback (:7317) + browser → web sign-up page → credential delivered back to the CLI; saved to ~/.altimate/altimate.json (overridable via ALTIMATE_WEB_URL).
  • On success, an inline green "Authentication successful" confirmation that auto-selects a model and auto-closes (no drop into the model picker).
  • Don't force Google: the CLI lands on the sign-up page and lets the user choose their method.

First-run experience

  • WelcomePanel boot box on the home screen with readiness-aware tips (/connect/discover); update toast suppressed until a model is ready.
  • useReady/markSetupComplete gate the first-run chat lock (unlocks after connecting, choosing Big Pickle, or picking a ready model).

Provider picker

  • /connect opens the curated DialogModelWelcome — top 5 providers (Altimate Gateway first) + "Search all providers…", a Big Pickle fallback, and a bright-green ✓ "selected" marker on the currently-active provider/model.
  • The /model dialog is restructured into READY / NEEDS-SETUP sections.

New TUI commands

  • /auth — start the Altimate LLM Gateway sign-in directly (DialogAltimateAuth).
  • /logout — clear the saved gateway credential (AltimateApi.clearCredentials), disconnect, and reload.

Rebase safety

  • Onboarding code isolated into an altimate-owned component/altimate-onboarding.tsx; dialog-model.tsx trimmed to pristine useConnected + the marked DialogModel restructure, so future upstream (opencode) merges have a minimal, clearly-marked conflict surface. No behavior change.

Test plan

  • /connect → Altimate LLM Gateway → browser → provisioned + connected
  • Gateway key authenticates (/agents/v1/models200)
  • /auth launches gateway sign-in; /logout clears creds + disconnects
  • Selected provider shows the green ✓
  • bun run typecheck clean

🤖 Generated with Claude Code

saravmajestic and others added 2 commits July 9, 2026 07:56
- Add an `oauth` `method:"auto"` to the Altimate auth plugin: bind a loopback
  server on `localhost:7317`, open the browser to the web authorize page,
  verify `state`, and save the gateway credential to `~/.altimate/altimate.json`
- Surface `altimate-backend` first in provider selection (TUI + clack) with the
  "Recommended · best tool-calling · 10M free tokens" hint

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…uth success

- Add `WelcomePanel` boot box (readiness-aware tips + "What is Altimate Code")
  on the home screen; first run is a welcoming panel, not an auto-opened modal
- Restructure the model picker into READY / NEEDS-SETUP; add the curated
  `DialogModelWelcome` (top 5 providers + "Search all providers"), Big Pickle
  fallback, and `useReady` / `markSetupComplete`
- `/connect` opens the curated picker
- Altimate LLM Gateway sign-in confirms inline ("Authentication successful") and
  auto-closes (auto-selecting a model) instead of dropping into the model picker
- Don't force Google: land on the sign-up page and let the user choose
  (drop `google_start`); reword the sign-in instruction

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@saravmajestic saravmajestic self-assigned this Jul 9, 2026
@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 7733e739-3c59-4e73-a467-0d375197071e

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/AI-7520-social-signup-authorize

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.

@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown

This PR doesn't fully meet our contributing guidelines and PR template.

What needs to be fixed:

  • PR description is missing required template sections. Please use the PR template.

Please edit this PR description to address the above within 2 hours, or it will be automatically closed.

If you believe this was flagged incorrectly, please let a maintainer know.

saravmajestic and others added 3 commits July 13, 2026 15:48
- /auth: sign in to the Altimate LLM Gateway directly via the OAuth loopback,
  skipping the provider picker (new `DialogAltimateAuth`)
- /logout: clear the stored gateway credential (`AltimateApi.clearCredentials`)
  and disconnect (dispose + bootstrap; the provider loader drops the now-stale
  auth-store entry on reload)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
DialogModelWelcome now tags each row with its providerID (and modelID for Big
Pickle) and compares against local.model.current(), rendering a bright-green ✓
plus a "· selected" note on the active provider — so the user can see at a glance
that e.g. Altimate LLM Gateway is already selected. Bright green (diffHighlightAdded)
is used because plain ANSI green renders dim in some terminals.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ebase safety)

Shrink our footprint on the upstream opencode `dialog-model.tsx` so future
upstream merges are easier to rebase:

- Move `useReady`/`markSetupComplete`, `DialogModelWelcome`, and
  `DialogBigPickleConfirm` into a new altimate-owned `component/altimate-onboarding.tsx`
  (zero rebase surface — our file)
- `dialog-model.tsx` (424 → 160 lines) now holds only pristine `useConnected` plus
  the `DialogModel` READY/NEEDS-SETUP restructure, fully wrapped in `altimate_change`
  markers so an upstream conflict is confined and human-resolvable
- Repoint imports in `app.tsx`, `home.tsx`, `dialog-provider.tsx`, `welcome-panel.tsx`

The onboarding file imports `DialogModel`/`useConnected` back from `dialog-model`,
but only inside callbacks/JSX — the circular reference is runtime-only and safe.
No behavior change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@sahrizvi sahrizvi 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.

Consensus code review — PR #1001

Independent reviews from 6 models (Claude, GPT 5.4 Codex, Kimi K2.5, MiniMax M2.7, Qwen 3.6, MiMo V2.5 Pro), converged and then re-validated line-by-line against the checked-out code.

Verdict: changes requested. The onboarding refactor and OAuth loopback design are well-structured (crypto-random state, CSRF state check, 0o600 credential file, cleanly isolated onboarding). But the new loopback auth server has correctness/reliability bugs and localhost security-hardening gaps — 7 major, 9 minor — all in the auth path. See inline comments.

Nits (not inlined): recent-models section dropped from the model picker; Big Pickle won't show a ✓ in the full picker after selection; error page doesn't window.close() like the success page; documented circular import between altimate-onboarding.tsx and dialog-model.tsx; silent open() failure (mitigated — the URL is still rendered); no ALTIMATE_CALLBACK_PORT override.

Positives: crypto-random state + CSRF check, 0o600 credentials, clean rebase-safe isolation of onboarding, reuse of existing provider onSelect handlers, reliable server teardown on the normal path.

Missing tests: the entire OAuth loopback flow (early callback, invalid/missing state, error branch, timeout, port collision, persistence) and the /auth · /logout · Big Pickle · mixed-row picker paths are untested.

// Bind the port BEFORE opening the browser so the credential can
// only be delivered to this process.
const state = randomBytes(16).toString("hex")
await startCallbackServer()

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.

The browser can hand back the login before we're listening for it

MAJORRace — pending is registered too late. startCallbackServer() binds the port and starts accepting requests here inside authorize(), but the module-level pending (state + resolve/reject) is only set later when the framework calls the returned callback() (waitForCallback, ~L114). If the browser redirects before callback() runs (an already-signed-in user gets an instant redirect, or any scheduling gap), /callback sees pending === undefined and rejects as "Invalid state — possible CSRF", silently dropping the credential. The singleton also can't support two concurrent /auth flows — the second overwrites the first.

Fix: register pending keyed by state (e.g. a Map) synchronously in authorize() before opening the browser; have callback() await that promise, and buffer an early callback by state until it's awaited.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in e46fdd48b. registerPending(state) now runs synchronously inside authorize() before open(), keying the pending flow by the unguessable state. The /callback handler resolves/rejects by that state, so an instant redirect (already-signed-in user) is matched instead of dropped as CSRF. See altimate.ts:162-171.

: undefined
if (model) local.model.set({ providerID: props.providerID, modelID: model }, { recent: true })
setConnected(true)
setTimeout(() => dialog.clear(), 5000)

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.

Backing out of the sign-in screen doesn't actually stop the sign-in

MAJORNo cancellation/cleanup when the dialog is dismissed. Pressing esc (or unmounting) only calls dialog.clear(); it never cancels the pending callback, so the loopback server can stay open for ~5 minutes and a late browser callback can still mutate app state after dismissal. This success-path setTimeout(() => dialog.clear(), 5000) is also never retained or cleared on unmount, so it can fire later and dismiss an unrelated dialog opened in the meantime.

Fix: guard post-await UI updates with an onCleanup disposed flag, clearTimeout in onCleanup, and stop the callback server / abort the SDK callback when the dialog is dismissed.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in f5596b40a (UI leak) + 40c80361c. AutoMethod now has an onCleanup/disposed guard and the success setTimeout is retained and cleared on unmount, so a late callback can no longer dismiss an unrelated dialog. The server-side half (eagerly cancelling the pending flow on dismiss) is tracked as NEW-3 below — the dismiss path can't reach the loopback pending map, so the 5-min timeout + callback()'s finally (stops the server when it's the last flow) is the backstop.

let pending: Pending | undefined

async function startCallbackServer(): Promise<void> {
if (server) return

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.

If port 7317 is busy, sign-in silently hangs forever with no retry

MAJORPort collision leaves a broken, silently-failing server. server is assigned by createServer() before listen(). On EADDRINUSE the listen promise rejects (L94-96) but server is never reset to undefined, so this if (server) return early-returns on the next attempt and the flow proceeds as if listening — the callback never arrives and the user waits out the 5-minute timeout with no message.

Fix: in the error handler set server = undefined (and close it), and surface a clear "port 7317 in use" error to the TUI. Consider an ephemeral port if the redirect contract allows.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in e46fdd48b. On listen error we now reset server = undefined (clearing the if (server) return guard so a retry can re-bind) and throw an EADDRINUSE-specific message instead of hanging: "Port 7317 is already in use — close whatever is using it and try again." See altimate.ts:112-122.

<h2>Signed in ✓</h2><p>You can return to your terminal.</p>
<script>setTimeout(()=>window.close(),1500)</script></body>`

const HTML_ERROR = (msg: string) => `<!doctype html><meta charset="utf-8"><title>Altimate Code</title>

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.

The error page echoes attacker-controlled text straight into HTML

MAJORReflected, unescaped input in the callback error page (localhost XSS). HTML_ERROR(msg) interpolates msg into HTML with no escaping, and the value ultimately comes from url.searchParams (the error branch at L57, which runs before the state check). Any page that drives the browser to http://localhost:7317/callback?error=<script>... during the auth window yields reflected XSS on the localhost origin. Impact is limited (transient page, no secrets/cookies there) but it's avoidable.

Fix: HTML-escape all reflected values, and validate state before handling the error branch.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in e46fdd48b. Added escapeHtml() and applied it to the reflected value in HTML_ERROR (altimate.ts:20-25,37). The related root cause is also closed — state is now validated before the ?error= branch runs (see the abort-path comment), so untrusted input no longer reaches that branch without a valid state.

})

await new Promise<void>((resolve, reject) => {
server!.listen(CALLBACK_PORT, () => resolve())

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.

The sign-in callback server is reachable from the whole network, not just this machine

MAJORCallback server listens on all interfaces, not loopback. listen(CALLBACK_PORT) with no host binds to ::/0.0.0.0, so the auth callback server is reachable from the LAN — despite the comment claiming "the credential can only be delivered to this process." The 128-bit state still protects credential delivery, but the listener still processes remote requests (probing, plus the pre-state-check error abort below).

Fix: server!.listen(CALLBACK_PORT, "127.0.0.1", () => resolve()).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in e46fdd48b. server.listen(CALLBACK_PORT, "127.0.0.1", ...) now binds loopback only, so the callback server is unreachable from the LAN. altimate.ts:109.

// A provider is "ready" (usable now) when it has valid credentials: it is present
// in the live provider list with at least one model — and, for the free OpenCode
// provider, with at least one paid model (a Zen key entered).
function providerReady(id: string) {

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.

A provider with unknown pricing can be treated as "ready" without a key

MINORproviderReady("opencode") mishandles undefined cost. m.cost?.input !== 0 evaluates to true when cost is undefined, so OpenCode can be counted as "ready" (usable now) even without a Zen key entered.

Fix: m.cost?.input != null && m.cost.input !== 0. (Same defect exists in useConnected() — see the separate comment.)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in f5596b40a. providerReady("opencode") now uses m.cost?.input != null && m.cost.input !== 0, so undefined cost is no longer treated as ready. The matching fix in useConnected() is in the same commit.

// Persist to ~/.altimate/altimate.json — the provider loader
// reads this first (it carries the instance/tenant + api_url
// the generic auth.json store can't).
await AltimateApi.saveCredentials({

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.

Browser sign-in skips the tenant-name check that manual entry enforces

MINOROAuth callback skips the tenant-name validation the manual path enforces. The manual paste path runs validateCredentials() (tenant regex + API check) before saving; this OAuth path calls saveCredentials() directly on the callback instance. The source is the trusted web app, so risk is low, but the invariant differs between the two entry points.

Fix: validate instance against VALID_TENANT_REGEX before persisting, or document why the OAuth source is trusted.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in f5596b40a. The OAuth path now validates AltimateApi.isValidInstanceName(creds.instance) against VALID_TENANT_REGEX before exchange/persist, matching the manual paste path's invariant. altimate.ts:195.

// useConnected() (real credentials) via useReady(), it gates the first-run chat
// lock. Module-global so it is shared across the app and resets on every process
// launch (so a fresh relaunch is a clean fresh-user state).
const [setupComplete, setSetupComplete] = createSignal(false)

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.

After signing out, the app still shows "you're all set" guidance

MINORsetupComplete is never reset on /logout. This module-global signal is OR'd with connected() in useReady(). /logout clears credentials (so connected() → false) but leaves setupComplete === true, so useReady() stays true for the rest of the process. Verified scope: useReady() only drives tips and the update-toast (not the chat/prompt), so the real impact is that the post-logout panel keeps showing "ready" tips (/discover) instead of /connect until relaunch. (Reviewers split on severity; kept minor given the cosmetic impact.)

Fix (if desired): gate the post-logout tips on connected() rather than useReady(), or add a resetSetupComplete() called from /logout.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 9c481ecf8. /logout now calls resetSetupComplete(), so setupComplete is cleared alongside credentials and useReady() returns to the /connect guidance without a relaunch.

res.end(body)
}

const error = url.searchParams.get("error")

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.

Anyone on the network can cancel your sign-in without any secret

MAJORUnauthenticated ?error= can abort an in-progress sign-in. This error branch runs before state validation and rejects/clears the current pending flow. Combined with the all-interface bind above, any local or LAN requester can cancel a user's sign-in without knowing state — there's no CSRF protection on the abort path.

Fix: validate state before honoring the error branch (this also closes the reflected-XSS issue on the same branch).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in e46fdd48b. state is now validated first — before the ?error= branch. An unknown/missing state returns 400 and can neither cancel an in-progress flow nor deliver anything, so the abort path is no longer reachable without the 128-bit secret. altimate.ts:76-89.

// are used by the restructured DialogModel below.
import { markSetupComplete, DialogBigPickleConfirm } from "./altimate-onboarding"

export function useConnected() {

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.

The same unknown-pricing bug also mislabels providers as connected

MINORuseConnected() has the same undefined-cost defect as providerReady(). It treats OpenCode as connected when any model has cost?.input !== 0, which is also true for undefined cost. This one has a wider blast radius: it drives the suggested: !connected() flag on the /connect and /auth commands, not just the model picker.

Fix: apply the same != null && !== 0 guard here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in f5596b40a. useConnected() now applies the same m.cost?.input != null && m.cost.input !== 0 guard, so undefined cost no longer marks OpenCode connected (and the suggested: !connected() flag on /connect//auth is correct).

@sahrizvi sahrizvi 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.

Requesting changes — PR #1001

Consensus review by 6 models (Claude, GPT 5.4 Codex, Kimi K2.5, MiniMax M2.7, Qwen 3.6, MiMo V2.5 Pro), converged and re-validated line-by-line against the code. Formalizing the verdict from the inline review above (#pullrequestreview-4705485161).

7 major, 9 minor findings — all in the new OAuth loopback auth path. The design is sound (crypto-random state, CSRF check, 0o600 credentials, cleanly isolated onboarding), but the loopback server has correctness/reliability bugs and localhost hardening gaps that should be fixed before merge.

Blocking (major):

  1. Race — pending registered after the server starts accepting requests; an instant redirect drops the credential (altimate.ts:140).
  2. Dismissing the dialog doesn't cancel the flow; server + setTimeout leak (dialog-provider.tsx:209).
  3. Port collision (EADDRINUSE) leaves server set → silent 5-min hang on retry (altimate.ts:43).
  4. Reflected, unescaped error in the callback HTML — localhost XSS (altimate.ts:23).
  5. Callback server binds all interfaces instead of 127.0.0.1 (altimate.ts:94).
  6. API key delivered in the URL query string (altimate.ts:76).
  7. Unauthenticated ?error= aborts an in-progress sign-in without knowing state (altimate.ts:57).

Minor findings and fixes are in the inline comments. Nothing here blocks the onboarding UX direction — the issues are contained to auth-path correctness, security hardening, and error handling. Happy to walk through any of them.

saravmajestic and others added 3 commits July 16, 2026 12:53
Address sahrizvi's review on #1001:
- Register the pending flow in a state-keyed Map synchronously in authorize()
  BEFORE opening the browser, and have callback() await that promise — an
  already-signed-in user's instant redirect is no longer dropped as CSRF, and
  concurrent /auth flows no longer clobber each other
- Bind the callback server to 127.0.0.1 (was all-interfaces / LAN-reachable)
- Validate `state` BEFORE honoring the `?error=` branch, so an unauthenticated
  request can't cancel an in-progress sign-in
- HTML-escape reflected values in the callback error page (localhost XSS)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- altimate.ts: reset the callback server on a failed listen (EADDRINUSE no longer
  wedges the singleton) + surface the reason; validate the callback instance name
  before persisting; log the failure reason on the auth catch path
- dialog-provider: DialogAltimateAuth onMount wrapped in try/catch (no more stuck
  "Starting sign-in…"); AutoMethod guards post-await updates + clears its auto-close
  timer on unmount (onCleanup); on connect-with-no-model, open the picker instead of
  faking a green ✓
- dialog-model: guard the favorite keybind against string (NEEDS-SETUP) rows; treat
  undefined model cost as not-paid in providerReady() and useConnected()
- app.tsx /logout: try/catch with error toast + reset the setupComplete flag
- altimate-onboarding: add resetSetupComplete()

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…oopback URL)

The loopback now receives a short-lived `token` (login_token) instead of the raw
key; callback() exchanges it via POST /auth/social/exchange (AltimateApi.
exchangeSocialToken) for the auth_token and saves that. Single code path for both
the Google and email/password connect flows. State validation, loopback bind,
HTML escaping, and the pending-Map/one-time-server logic are unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@sahrizvi sahrizvi 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.

Follow-up review — PR #1001 (still requesting changes)

Consensus re-review of the fix commits (e46fdd4, f5596b4, 7e7ac7c) by 6 models. All 16 prior findings are resolved (15 fully, #2 partially) — thank you. The fixes surfaced new issues, and one is a blocker, so keeping this at request changes.

Blocker (top priority): the OAuth callback's api_url is trusted with no scheme/host validation, and the CLI POSTs the one-time login_token to it (altimate.ts:90client.ts exchangeSocialToken). Unlike the manual-paste path, the OAuth path validates neither scheme nor host. A malicious browser extension or a web-app open-redirect could exfiltrate the token + tenant. Fix: require https:// and allowlist known Altimate hosts before exchanging (except in an explicit dev mode).

The five inline comments below (NEW-2 … NEW-6) are the remaining items — NEW-2 is a quick correctness fix worth doing now; the rest are fast-follows. Once the api_url validation and NEW-2 land, this is good to go.

const result = registerPending(state)

const webUrl = (process.env.ALTIMATE_WEB_URL || DEFAULT_WEB_URL).replace(/\/+$/, "")
const redirect = `http://localhost:${CALLBACK_PORT}/callback`

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.

The browser is sent to localhost but we only listen on IPv4 — sign-in can miss

MAJORNEW-2 — loopback binds 127.0.0.1 but the redirect uses http://localhost. altimate.ts:106 binds IPv4 loopback only, while the redirect here is http://localhost:${CALLBACK_PORT}/callback. On hosts where localhost resolves to ::1 first, the browser callback can hit a closed IPv6 port. Browser Happy-Eyeballs fallback often masks this, but it's fragile.

Fix: use http://127.0.0.1:${CALLBACK_PORT}/callback in the redirect to match the bind (or bind both loopback families).

Flagged by: GPT (MAJOR)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 40c80361c (NEW-2). The redirect is now http://127.0.0.1:${CALLBACK_PORT}/callback to match the IPv4 loopback bind, so a localhost::1 resolution can't hit a closed port. altimate.ts:176.

await startCallbackServer()
// Register the pending flow BEFORE opening the browser so an instant
// redirect can be matched by state rather than dropped as CSRF.
const result = registerPending(state)

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.

Backing out of sign-in leaves the server and pending flow running for 5 minutes

MINORNEW-3 (residual of prior #2) — dismissing the dialog doesn't cancel the server-side flow. The onCleanup+disposed guard fixes the UI leak, but nothing rejects this eagerly-created registerPending promise on dismiss, so callback() keeps awaiting, the loopback server stays up, and the pending entry lingers until the 5-minute timeout.

Fix: on dismiss, reject/cancel the pending flow by state (and stop the server if it was the last one). Also attach a no-op .catch() to this registerPending promise so a never-awaited rejection can't become an unhandled rejection.

Flagged by: Kimi (MAJOR), MiMo, Claude — consensus

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Partially addressed in 40c80361c (NEW-3). Added void result.catch(() => {}) so the eagerly-created registerPending promise can't surface as an unhandled rejection when the dialog is dismissed. The eager reject-by-state on dismiss isn't feasible from here: the TUI dismiss path never receives state, so it can't reach the loopback pending map. The backstop remains the 5-min timeout (rejects the pending entry) and callback()'s finally (stops the server when it was the last flow). Happy to open a follow-up if we want true immediate cancel — it'd require threading state back out to the dialog.

// Log the reason (CSRF / timeout / invalid instance / …). Runs in the
// server process, so this goes to the log, not the TUI display.
console.error("[altimate] gateway sign-in failed:", err instanceof Error ? err.message : err)
return { type: "failed" }

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.

When sign-in fails, the user sees a generic error with no reason

MINORNEW-4 (UX) — auth/callback failures are opaque to the user. The plugin callback() logs the reason via console.error but returns bare { type: "failed" }; OauthCallbackFailed carries no message (auth-service.ts), so the TUI shows a generic failure with no reason (port busy, timeout, exchange 401, invalid instance).

Fix: thread a reason string through the failed result (or surface it via toast.error in the callback path).

Flagged by: MiniMax (rated CRITICAL), GPT, Kimi, MiMo — consensus

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 40c80361c (NEW-4). AutoMethod now calls toast.error(...) on result.error instead of clearing silently, so port-busy/timeout/exchange-401/invalid-instance failures are visible in the TUI. The precise reason is still logged server-side via console.error.

}[provider.id],
// altimate_change end
category: provider.id in PROVIDER_PRIORITY ? "Popular" : "Other",
async onSelect() {

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.

The recommended /connect path can still hang on a startup failure

MINORNEW-5 (UX) — /connect welcome path lacks the try/catch that /auth got. The onboarding fix wrapped DialogAltimateAuth, but the recommended /connect path routes through this createDialogProviderOptions().onSelectoauth.authorize() (just below, ~L95) with no equivalent guard, so a port-collision/startup failure there still fails without the new toast.

Fix: wrap the OAuth authorize block in createDialogProviderOptions() with the same toast/clear behavior as DialogAltimateAuth.

Flagged by: GPT (MAJOR)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 40c80361c (NEW-5). The OAuth authorize block in createDialogProviderOptions() (the /connect path) is now wrapped in try/catch with the same toast.error(...) + dialog.clear() behavior as DialogAltimateAuth.

}

entry.resolve({ api_url: apiUrl, instance, token })
html(200, HTML_SUCCESS)

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.

The browser says "Signed in" before we've actually finished signing in

MINORNEW-6 — browser shows "Signed in ✓" before the token exchange/persist succeeds. The loopback returns HTML_SUCCESS here as soon as it receives the token; if exchangeSocialToken()/save then fails, the browser says success while the terminal reports failure.

Fix: use neutral "authorization received, return to terminal" copy, or delay success until exchange + persist succeed.

Flagged by: GPT, MiniMax

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 40c80361c (NEW-6). HTML_SUCCESS is now neutral — "Authorization received / Return to your terminal to finish connecting." — so the browser no longer claims success before the exchange + persist complete. The terminal remains the source of truth for actual success/failure.

saravmajestic and others added 2 commits July 16, 2026 19:39
- NEW-2: callback redirect uses http://127.0.0.1 to match the loopback bind
  (a plain `localhost` redirect can resolve to ::1 and hit a closed port)
- NEW-6: neutral browser copy ("Authorization received") — the loopback replies
  before the CLI has exchanged/persisted, so it must not claim "Signed in"
- NEW-5: guard the /connect OAuth authorize path with try/catch → toast + clear
  (parity with DialogAltimateAuth; port-collision no longer fails silently)
- NEW-4: AutoMethod surfaces a toast on callback failure instead of clearing
  silently (the precise reason is logged server-side)
- NEW-3: swallow a never-awaited pending rejection (void result.catch) to avoid
  an unhandled rejection when the dialog is dismissed before callback() runs

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The welcome/top-5 picker already leads with the Altimate LLM Gateway. Order
the full DialogModel list the same way: export `PROVIDER_PRIORITY` and sort the
READY section's providers by it (the NEEDS-SETUP section already used it), so
the gateway leads whether or not it is connected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants