Repository navigation
feat(cli): cross-device login via OAuth code + PKCE - #3965
Conversation
Replaces the loopback-only `cli/authorize`+`/api/cli/exchange` flow with Better Auth's `oauth-provider`. The CLI tries a loopback listener first (one-click happy path on local machines); when that isn't reachable (SSH, CI, sandboxed envs), it falls back to a copy-paste page at `/cli/auth/code` showing `<code>#<state>` — matching how Claude Code's CLI handles cross-device. PKCE (S256) + state binding + `resource` indicator so Better Auth issues a JWT, which tRPC's context now accepts as Bearer via JWKS verification (mirrors the existing MCP route's auth path). The `superset-cli` OAuth client is seeded idempotently at API boot with both loopback ports (51789–93) and the paste redirect URI.
📝 WalkthroughWalkthroughThe pull request removes deprecated CLI authentication endpoints and introduces a new OAuth2 PKCE-based authorization flow. The old flow via Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
Greptile SummaryReplaces the bespoke loopback-only Two functional gaps need attention before this lands:
Confidence Score: 3/5Not ready to merge — two P1 issues cause silently broken org-scoped tRPC calls and wasted refresh tokens for all CLI sessions. Two independent P1 findings on the core auth path: missing org claim in JWT sessions and discarded refresh token. Either alone would cap the score at 4, but both together on the primary feature path warrants 3. apps/api/src/trpc/context.ts (organizationId claim), packages/cli/src/commands/auth/login/command.ts (refresh token storage)
|
| Filename | Overview |
|---|---|
| apps/api/src/trpc/context.ts | Adds JWT bearer auth path to tRPC context; P1: activeOrganizationId silently null if JWT lacks custom organizationId claim, breaking all org-scoped operations for CLI sessions |
| packages/cli/src/commands/auth/login/command.ts | Wires loopback/paste callbacks to CLI UX; P1: refresh token from LoginResult is not persisted to config, wasting the offline_access scope |
| packages/cli/src/lib/auth.ts | Core OAuth PKCE + loopback/paste dual-path login flow; P2: paste redirect URI derived via heuristic that may differ from server-seeded value |
| packages/auth/src/lib/seed-cli-oauth-client.ts | Idempotent OAuth client seed (PKCE-only, public client); overwrites config on every boot which may clobber manual admin changes |
| apps/web/src/app/cli/auth/code/page.tsx | New paste-flow redirect page; renders code+state from query params with proper error handling, no issues |
| apps/web/src/app/cli/auth/code/components/CliAuthCodeDisplay/CliAuthCodeDisplay.tsx | Copy-to-clipboard component for auth code display; uses deprecated execCommand fallback (P2), primary clipboard API usage is correct |
| apps/api/src/instrumentation.ts | Calls seedCliOAuthClient at API boot with error swallowing; straightforward and safe |
| apps/web/src/proxy.ts | Adds /cli/auth/code to public routes — correct, this page must be accessible without a session |
| apps/web/next.config.ts | Adds allowedDevOrigins for 127.0.0.1 to unblock HMR from loopback callback server |
Sequence Diagram
sequenceDiagram
participant CLI
participant Browser
participant API as API (Better Auth oauth-provider)
participant WebApp as Web App (/cli/auth/code)
CLI->>CLI: generate codeVerifier, codeChallenge (S256), state
alt Loopback path (local machine)
CLI->>CLI: bindLoopbackServer() on 127.0.0.1:5178x
CLI->>Browser: open authorize URL with redirect_uri=127.0.0.1:5178x/callback
Browser->>API: GET /api/auth/oauth2/authorize (PKCE + state)
API->>Browser: consent page
Browser->>API: user approves
API->>CLI: GET /callback?code=...&state=...
CLI->>CLI: verify state
CLI->>API: POST /api/auth/oauth2/token (code + code_verifier)
API->>CLI: access_token + refresh_token
else Paste path (SSH / CI / no loopback)
CLI->>Browser: open authorize URL with redirect_uri=/cli/auth/code
Browser->>API: GET /api/auth/oauth2/authorize (PKCE + state)
API->>Browser: consent page
Browser->>API: user approves
API->>WebApp: redirect /cli/auth/code?code=...&state=...
WebApp->>Browser: display code#state for copy
Browser->>CLI: user pastes code#state
CLI->>CLI: verify state
CLI->>API: POST /api/auth/oauth2/token (code + code_verifier)
API->>CLI: access_token + refresh_token
end
CLI->>API: tRPC calls with Bearer access_token (JWT)
API->>API: JWKS verify then sessionFromOAuthBearer then activeOrganizationId=null
Comments Outside Diff (1)
-
apps/web/src/app/cli/auth/code/components/CliAuthCodeDisplay/CliAuthCodeDisplay.tsx, line 267-268 (link)document.execCommand('copy')is deprecatedThe
execCommand("copy")API is deprecated and may be removed from future browsers. Since this is used as a fallback for whennavigator.clipboardis unavailable, it's acceptable for now, but you could simplify the fallback to justreturn false— modern browsers that supportnavigator.clipboardwill handle 99%+ of cases, and the code/state text is always selectable for manual copy.Prompt To Fix With AI
This is a comment left during a code review. Path: apps/web/src/app/cli/auth/code/components/CliAuthCodeDisplay/CliAuthCodeDisplay.tsx Line: 267-268 Comment: **`document.execCommand('copy')` is deprecated** The `execCommand("copy")` API is deprecated and may be removed from future browsers. Since this is used as a fallback for when `navigator.clipboard` is unavailable, it's acceptable for now, but you could simplify the fallback to just `return false` — modern browsers that support `navigator.clipboard` will handle 99%+ of cases, and the code/state text is always selectable for manual copy. How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
Fix the following 4 code review issues. Work through them one at a time, proposing concise fixes.
---
### Issue 1 of 4
apps/api/src/trpc/context.ts:45-46
**`organizationId` claim may never be present — all org-scoped tRPC calls will see `null`**
Better Auth's oauth-provider issues standard JWT claims (`sub`, `iat`, `exp`, `iss`, `aud`). The custom `organizationId` claim is not a standard claim and is not emitted unless the oauth-provider plugin is explicitly configured to include it. As a result `activeOrganizationId` will almost always be `null` for CLI bearer sessions, and any tRPC procedure that reads the active org from the session context will silently behave as if no org is selected — even after the user chose one during login.
The `config.organizationId` written in `command.ts` is never forwarded back to the API in the JWT; you'd need either (a) a custom JWT "access-token claims" hook in your Better Auth config that embeds `organizationId`, or (b) the CLI passing the org explicitly on each request (e.g. a tRPC header), or (c) a separate token exchange step. Without one of these, `superset auth whoami` and every org-scoped command will return wrong/empty data.
### Issue 2 of 4
packages/cli/src/commands/auth/login/command.ts:56-59
**Refresh token silently discarded — re-login required on expiry**
`offline_access` is included in the OAuth scope, so the token endpoint returns a refresh token and `LoginResult.refreshToken` carries it. However the assignment at lines 56-59 only writes `accessToken` and `expiresAt` to config — the refresh token is never persisted. Once the access token expires, users must re-run `superset auth login` from scratch, entirely defeating the purpose of requesting `offline_access`.
The `refreshToken` field needs to be added to the config write here, added to the config type, and a silent-refresh path needs to consume it before the token expires.
### Issue 3 of 4
packages/cli/src/lib/auth.ts:53-55
**`getWebUrl()` string-replace heuristic can produce a mismatched redirect URI**
`getWebUrl()` derives the web URL by replacing `api.superset.sh` with `app.superset.sh` in `SUPERSET_API_URL`. The OAuth client is seeded with `NEXT_PUBLIC_WEB_URL` as the paste redirect URI. If a user sets `SUPERSET_WEB_URL` (or the heuristic produces a different value), the `redirect_uri` sent during authorization and token-exchange won't match the registered URI and the exchange will fail with a 400.
Consider a `SUPERSET_WEB_URL` default pre-built into the CLI at publish time (matching `NEXT_PUBLIC_WEB_URL`), or deriving it from a `/.well-known/oauth-authorization-server` metadata document to guarantee parity.
### Issue 4 of 4
apps/web/src/app/cli/auth/code/components/CliAuthCodeDisplay/CliAuthCodeDisplay.tsx:267-268
**`document.execCommand('copy')` is deprecated**
The `execCommand("copy")` API is deprecated and may be removed from future browsers. Since this is used as a fallback for when `navigator.clipboard` is unavailable, it's acceptable for now, but you could simplify the fallback to just `return false` — modern browsers that support `navigator.clipboard` will handle 99%+ of cases, and the code/state text is always selectable for manual copy.
Reviews (1): Last reviewed commit: "feat(cli): cross-device login via OAuth ..." | Re-trigger Greptile
| const activeOrganizationId = | ||
| typeof payload.organizationId === "string" ? payload.organizationId : null; |
There was a problem hiding this comment.
organizationId claim may never be present — all org-scoped tRPC calls will see null
Better Auth's oauth-provider issues standard JWT claims (sub, iat, exp, iss, aud). The custom organizationId claim is not a standard claim and is not emitted unless the oauth-provider plugin is explicitly configured to include it. As a result activeOrganizationId will almost always be null for CLI bearer sessions, and any tRPC procedure that reads the active org from the session context will silently behave as if no org is selected — even after the user chose one during login.
The config.organizationId written in command.ts is never forwarded back to the API in the JWT; you'd need either (a) a custom JWT "access-token claims" hook in your Better Auth config that embeds organizationId, or (b) the CLI passing the org explicitly on each request (e.g. a tRPC header), or (c) a separate token exchange step. Without one of these, superset auth whoami and every org-scoped command will return wrong/empty data.
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/api/src/trpc/context.ts
Line: 45-46
Comment:
**`organizationId` claim may never be present — all org-scoped tRPC calls will see `null`**
Better Auth's oauth-provider issues standard JWT claims (`sub`, `iat`, `exp`, `iss`, `aud`). The custom `organizationId` claim is not a standard claim and is not emitted unless the oauth-provider plugin is explicitly configured to include it. As a result `activeOrganizationId` will almost always be `null` for CLI bearer sessions, and any tRPC procedure that reads the active org from the session context will silently behave as if no org is selected — even after the user chose one during login.
The `config.organizationId` written in `command.ts` is never forwarded back to the API in the JWT; you'd need either (a) a custom JWT "access-token claims" hook in your Better Auth config that embeds `organizationId`, or (b) the CLI passing the org explicitly on each request (e.g. a tRPC header), or (c) a separate token exchange step. Without one of these, `superset auth whoami` and every org-scoped command will return wrong/empty data.
How can I resolve this? If you propose a fix, please make it concise.| config.auth = { | ||
| accessToken: result.accessToken, | ||
| expiresAt: result.expiresAt, | ||
| }; |
There was a problem hiding this comment.
Refresh token silently discarded — re-login required on expiry
offline_access is included in the OAuth scope, so the token endpoint returns a refresh token and LoginResult.refreshToken carries it. However the assignment at lines 56-59 only writes accessToken and expiresAt to config — the refresh token is never persisted. Once the access token expires, users must re-run superset auth login from scratch, entirely defeating the purpose of requesting offline_access.
The refreshToken field needs to be added to the config write here, added to the config type, and a silent-refresh path needs to consume it before the token expires.
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/cli/src/commands/auth/login/command.ts
Line: 56-59
Comment:
**Refresh token silently discarded — re-login required on expiry**
`offline_access` is included in the OAuth scope, so the token endpoint returns a refresh token and `LoginResult.refreshToken` carries it. However the assignment at lines 56-59 only writes `accessToken` and `expiresAt` to config — the refresh token is never persisted. Once the access token expires, users must re-run `superset auth login` from scratch, entirely defeating the purpose of requesting `offline_access`.
The `refreshToken` field needs to be added to the config write here, added to the config type, and a silent-refresh path needs to consume it before the token expires.
How can I resolve this? If you propose a fix, please make it concise.| export function getWebUrl(): string { | ||
| if (process.env.SUPERSET_WEB_URL) return process.env.SUPERSET_WEB_URL; | ||
| return env.SUPERSET_API_URL.replace("api.superset.sh", "app.superset.sh"); |
There was a problem hiding this comment.
getWebUrl() string-replace heuristic can produce a mismatched redirect URI
getWebUrl() derives the web URL by replacing api.superset.sh with app.superset.sh in SUPERSET_API_URL. The OAuth client is seeded with NEXT_PUBLIC_WEB_URL as the paste redirect URI. If a user sets SUPERSET_WEB_URL (or the heuristic produces a different value), the redirect_uri sent during authorization and token-exchange won't match the registered URI and the exchange will fail with a 400.
Consider a SUPERSET_WEB_URL default pre-built into the CLI at publish time (matching NEXT_PUBLIC_WEB_URL), or deriving it from a /.well-known/oauth-authorization-server metadata document to guarantee parity.
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/cli/src/lib/auth.ts
Line: 53-55
Comment:
**`getWebUrl()` string-replace heuristic can produce a mismatched redirect URI**
`getWebUrl()` derives the web URL by replacing `api.superset.sh` with `app.superset.sh` in `SUPERSET_API_URL`. The OAuth client is seeded with `NEXT_PUBLIC_WEB_URL` as the paste redirect URI. If a user sets `SUPERSET_WEB_URL` (or the heuristic produces a different value), the `redirect_uri` sent during authorization and token-exchange won't match the registered URI and the exchange will fail with a 400.
Consider a `SUPERSET_WEB_URL` default pre-built into the CLI at publish time (matching `NEXT_PUBLIC_WEB_URL`), or deriving it from a `/.well-known/oauth-authorization-server` metadata document to guarantee parity.
How can I resolve this? If you propose a fix, please make it concise.
🧹 Preview Cleanup CompleteThe following preview resources have been cleaned up:
Thank you for your contribution! 🎉 |
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/web/src/proxy.ts (1)
11-15:⚠️ Potential issue | 🟠 Major | ⚡ Quick winAvoid prefix-matching the new public callback route.
startsWith("/cli/auth/code")also exempts paths like/cli/auth/code-anything, so this change widens the unauthenticated surface beyond the single callback page. Match on equality or a slash boundary instead.Suggested fix
function isPublicRoute(pathname: string): boolean { - return publicRoutes.some((route) => pathname.startsWith(route)); + return publicRoutes.some( + (route) => pathname === route || pathname.startsWith(`${route}/`), + ); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/src/proxy.ts` around lines 11 - 15, The publicRoutes prefix check is too permissive: update isPublicRoute to avoid matching paths like "/cli/auth/code-anything" for the callback route by changing the match from a simple startsWith to either exact equality or a slash-boundary check; specifically, in the isPublicRoute function (and where publicRoutes is defined) replace the predicate with something like pathname === route || pathname.startsWith(route + "/") so callback entries such as "/cli/auth/code" only match themselves or subpaths under that exact segment.
🧹 Nitpick comments (1)
packages/cli/src/lib/auth.ts (1)
239-251: ⚡ Quick winValidate token response shape before use.
The type assertion on the JSON response doesn't provide runtime guarantees. If the server returns an unexpected shape (e.g., error response with different structure),
data.access_tokencould be undefined, resulting in an invalid token being stored and used for subsequent API calls.♻️ Suggested validation
const data = (await response.json()) as { access_token: string; token_type: string; expires_in?: number; refresh_token?: string; }; + if (!data.access_token || typeof data.access_token !== "string") { + throw new CLIError( + "Invalid token response", + "Server returned an unexpected response. Run `superset auth login` again.", + ); + } + const expiresIn = data.expires_in ?? 60 * 60 * 24 * 30;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/cli/src/lib/auth.ts` around lines 239 - 251, The JSON response is being blindly asserted into the expected shape which can yield undefined tokens; modify the code around the response.json() call to perform runtime validation on the parsed object (check that data is an object and data.access_token is a non-empty string, and validate optional fields like data.expires_in and data.refresh_token if present), and if validation fails throw or return a clear error instead of returning an invalid token; update the return logic that builds accessToken, expiresAt, and refreshToken to only use validated fields (e.g., use a default expiresIn only after confirming its numericity) so you never store or return undefined accessToken or malformed expiresAt from the current function.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@apps/api/src/instrumentation.ts`:
- Around line 6-13: The dynamic import of "@superset/auth/seed-cli-oauth-client"
is outside the try/catch so import-time failures still abort startup; move the
await import(...) inside the existing try block (or wrap it with its own try) so
both the module resolution/evaluation and the call to seedCliOAuthClient() are
caught; update the code paths around seedCliOAuthClient (the imported symbol) in
the register() initialization so any import or execution error is logged and
does not cause register() to throw.
In
`@apps/web/src/app/cli/auth/code/components/CliAuthCodeDisplay/CliAuthCodeDisplay.tsx`:
- Around line 39-46: The useCopiedFlag hook sets a timeout in flash but never
clears it, which can cause setState on an unmounted component; update
useCopiedFlag to store the timeout id (e.g. in a ref) and clearTimeout on
unmount (using useEffect cleanup) and also clear any previous timer when flash
runs again to avoid overlapping timers; reference the useCopiedFlag function and
the flash/setTimeout call when making these changes.
In `@packages/auth/src/lib/seed-cli-oauth-client.ts`:
- Around line 35-48: The seed logic using db.query.oauthClients.findFirst +
conditional insert can race on concurrent startups; replace it with a single
atomic upsert or a transaction with a unique constraint on clientId so two
processes cannot both insert. Concretely, change the flow around
db.query.oauthClients.findFirst / db.update(oauthClients).set /
db.insert(oauthClients).values to use the ORM's
upsert/insert...onConflictDoUpdate (or db.insert(...).onConflict(...).merge)
keyed on oauthClients.clientId (CLIENT_ID) so you perform an atomic
insert-or-update by clientId, or alternatively wrap the find/insert/update in a
serializable transaction that enforces the unique key on clientId. Ensure the
final operation updates the same fields currently in config when a conflict
occurs.
---
Outside diff comments:
In `@apps/web/src/proxy.ts`:
- Around line 11-15: The publicRoutes prefix check is too permissive: update
isPublicRoute to avoid matching paths like "/cli/auth/code-anything" for the
callback route by changing the match from a simple startsWith to either exact
equality or a slash-boundary check; specifically, in the isPublicRoute function
(and where publicRoutes is defined) replace the predicate with something like
pathname === route || pathname.startsWith(route + "/") so callback entries such
as "/cli/auth/code" only match themselves or subpaths under that exact segment.
---
Nitpick comments:
In `@packages/cli/src/lib/auth.ts`:
- Around line 239-251: The JSON response is being blindly asserted into the
expected shape which can yield undefined tokens; modify the code around the
response.json() call to perform runtime validation on the parsed object (check
that data is an object and data.access_token is a non-empty string, and validate
optional fields like data.expires_in and data.refresh_token if present), and if
validation fails throw or return a clear error instead of returning an invalid
token; update the return logic that builds accessToken, expiresAt, and
refreshToken to only use validated fields (e.g., use a default expiresIn only
after confirming its numericity) so you never store or return undefined
accessToken or malformed expiresAt from the current function.
🪄 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: defaults
Review profile: CHILL
Plan: Pro
Run ID: ff54795e-7c77-4785-ae17-50a127da3ec4
📒 Files selected for processing (16)
apps/api/src/app/api/cli/create-code/route.tsapps/api/src/app/api/cli/exchange/route.tsapps/api/src/instrumentation.tsapps/api/src/trpc/context.tsapps/web/next.config.tsapps/web/src/app/cli/auth/code/components/CliAuthCodeDisplay/CliAuthCodeDisplay.tsxapps/web/src/app/cli/auth/code/components/CliAuthCodeDisplay/index.tsapps/web/src/app/cli/auth/code/page.tsxapps/web/src/app/cli/authorize/components/CliAuthorizeForm/CliAuthorizeForm.tsxapps/web/src/app/cli/authorize/components/CliAuthorizeForm/index.tsapps/web/src/app/cli/authorize/page.tsxapps/web/src/proxy.tspackages/auth/package.jsonpackages/auth/src/lib/seed-cli-oauth-client.tspackages/cli/src/commands/auth/login/command.tspackages/cli/src/lib/auth.ts
💤 Files with no reviewable changes (5)
- apps/web/src/app/cli/authorize/components/CliAuthorizeForm/index.ts
- apps/api/src/app/api/cli/exchange/route.ts
- apps/web/src/app/cli/authorize/page.tsx
- apps/api/src/app/api/cli/create-code/route.ts
- apps/web/src/app/cli/authorize/components/CliAuthorizeForm/CliAuthorizeForm.tsx
| const { seedCliOAuthClient } = await import( | ||
| "@superset/auth/seed-cli-oauth-client" | ||
| ); | ||
| try { | ||
| await seedCliOAuthClient(); | ||
| } catch (error) { | ||
| console.error("[instrumentation] Failed to seed CLI OAuth client", error); | ||
| } |
There was a problem hiding this comment.
Wrap the dynamic import in the same failure path.
If @superset/auth/seed-cli-oauth-client fails to resolve or throws during module evaluation, register() still aborts because the try only covers seedCliOAuthClient(). That turns this best-effort seed into a startup failure.
Suggested fix
- const { seedCliOAuthClient } = await import(
- "@superset/auth/seed-cli-oauth-client"
- );
try {
+ const { seedCliOAuthClient } = await import(
+ "@superset/auth/seed-cli-oauth-client"
+ );
await seedCliOAuthClient();
} catch (error) {
console.error("[instrumentation] Failed to seed CLI OAuth client", error);
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const { seedCliOAuthClient } = await import( | |
| "@superset/auth/seed-cli-oauth-client" | |
| ); | |
| try { | |
| await seedCliOAuthClient(); | |
| } catch (error) { | |
| console.error("[instrumentation] Failed to seed CLI OAuth client", error); | |
| } | |
| try { | |
| const { seedCliOAuthClient } = await import( | |
| "@superset/auth/seed-cli-oauth-client" | |
| ); | |
| await seedCliOAuthClient(); | |
| } catch (error) { | |
| console.error("[instrumentation] Failed to seed CLI OAuth client", error); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/api/src/instrumentation.ts` around lines 6 - 13, The dynamic import of
"@superset/auth/seed-cli-oauth-client" is outside the try/catch so import-time
failures still abort startup; move the await import(...) inside the existing try
block (or wrap it with its own try) so both the module resolution/evaluation and
the call to seedCliOAuthClient() are caught; update the code paths around
seedCliOAuthClient (the imported symbol) in the register() initialization so any
import or execution error is logged and does not cause register() to throw.
The superset-cli OAuth client is one-time bootstrap data, not something that should re-run on every API boot. The prod row was applied manually with the matching shape (paste URI + 5 loopback ports + require_pkce: true), so we no longer need the seed in code.
- bake SUPERSET_WEB_URL into env.ts (already defined by cli.config.ts at build time) and drop the host-substitution heuristic in getWebUrl() - drop offline_access scope: refresh tokens were being requested but silently discarded; re-add the scope alongside actual refresh logic - clear the copied-flag timer on unmount in CliAuthCodeDisplay
Bundles the 5 feat(cli) commits since v0.2.2: - automations + tasks alignment with SDK and MCP-v2 (#3966) - cross-device login via OAuth code + PKCE (#3965) - 'automations prompt' split into 'prompt get' / 'prompt set' (#3959) - --name filter on automations list (#3952) - --assignee filter on tasks list (#3972) Push cli-v0.2.3 after this lands to fire the release pipeline.
Summary
/cli/authorize+/api/cli/exchangeflow with Better Auth's first-classoauth-providerplugin (PKCE S256 +state+resourceindicator)/cli/auth/code(showing<code>#<state>) when loopback isn't reachable (SSH, CI, sandboxed envs) — same pattern as Claude Code's CLIsuperset-cliOAuth client is seeded idempotently at API boot with both loopback ports (127.0.0.1:51789-93/callback) and the paste redirect URIWhy now
superset auth loginpreviously bound a local HTTP server and required the browser to redirect to it. That breaks for any CLI not running on the same machine as the browser — SSH hosts, dev containers, CI runners, cloud sandboxes — which is most of the realistic deploy surface for a developer CLI.The OAuth code + PKCE + paste flow (rather than the OAuth device-grant alternative) was chosen for parity with Claude Code's UX: short user codes are phishable and the paste flow feels more polished to technical users.
Test plan
bun run lintpassesbun run typecheckpasses (all 27 packages)superset auth loginon a local Mac binds127.0.0.1:51789, browser auto-redirects, no paste neededSSH_CONNECTION=1 superset auth login(or run from an SSH'd host) prints the URL → browser → consent → copy code from/cli/auth/code→ paste back into CLIsuperset auth whoamiand any tRPC-backed command succeed with the returned bearer127.0.0.1:5760no longer blocks HMR (addedallowedDevOrigins)Summary by cubic
Adds cross-device CLI login using OAuth 2.0 Authorization Code + PKCE via
better-auth/oauth2, replacing the loopback-only flow. The API now accepts Bearer JWTs, and a copy-paste page enables SSH/CI login.New Features
/cli/auth/code(<code>#<state>), with an in-terminal paste prompt; usesSUPERSET_WEB_URLfor the paste redirect and drops theoffline_accessscope.state, andresource, exchanging via/api/auth/oauth2/token; browser opens only when possible.tRPCcontext verifies OAuth-issued JWTs via JWKS and accepts Bearer tokens; removed/cli/authorizeand/api/cli/{create-code,exchange}; added public/cli/auth/code.Migration
supersetCLI (old endpoints are removed).superset-cliOAuth client exists with PKCE, the paste redirect, and loopback ports; runtime seeding was removed.Written for commit 7afd198. Summary will update on new commits.
Summary by CodeRabbit
New Features
Chores
SUPERSET_WEB_URLenvironment configuration.