Skip to content

Commit 651586e

Browse files
fix(cli): clarify login method selection (#7578)
* fix(cli): clarify login method selection * fix(cli): correct API-key authentication guidance
1 parent 83544b1 commit 651586e

10 files changed

Lines changed: 206 additions & 109 deletions

File tree

apps/docs/content/docs/cli/authentication.mdx

Lines changed: 38 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,35 @@ description: Sign in from the terminal, authenticate CI with an API key, and kee
55

66
import { Callout } from 'fumadocs-ui/components/callout'
77

8-
`sim login` signs you in through your browser and stores a short-lived login
9-
that renews itself and can be revoked at any time. In CI you supply an API key
10-
through the environment instead.
8+
`sim login` signs you in through your browser. It prefers OAuth, which stores a
9+
short-lived login that renews itself, and selects API-key pairing for remote
10+
terminals or servers without OAuth support. In CI you supply an existing API
11+
key through the environment instead.
1112

1213
## Signing in
1314

1415
```bash
1516
sim login
1617
```
1718

18-
The CLI opens your browser on Sim's sign-in page, then on a consent page that
19+
Choose a method explicitly when the credential type matters:
20+
21+
```bash
22+
sim login --method oauth
23+
sim login --method api-key
24+
```
25+
26+
`--method oauth` requires OAuth support and never falls back to an API key. The
27+
server must enable `OAUTH_PROVIDER_ENABLED=true`. Explicit OAuth selection also
28+
overrides SSH/headless detection; your browser still needs to reach the CLI's
29+
local callback. `--method api-key` uses pairing-code approval to create a new
30+
permanent API key. To supply an existing key, set `SIM_API_KEY` instead.
31+
32+
API-key pairing requires a server that supports `platform` API keys. Upgrade
33+
older deployments that only issue `copilot` keys before starting login; those
34+
keys cannot authenticate the platform CLI.
35+
36+
OAuth login opens your browser on Sim's sign-in page, then on a consent page that
1937
names the Sim CLI and what it will be able to do. Approve, and the browser hands
2038
control back to the terminal:
2139

@@ -46,21 +64,21 @@ your login.
4664

4765
| Option | What it does |
4866
| --- | --- |
49-
| `--no-browser` | Print the URL instead of opening a browser |
50-
| `--browserless` | Use the pairing-code handoff instead (see below) |
67+
| `--method <method>` | `oauth` requires OAuth login; `api-key` creates a permanent key through pairing. Auto-selects when omitted |
68+
| `--no-browser` | Print the approval URL without opening it; works with either method |
5169
| `--read-only` | Ask only for permission to read, never to change anything |
5270
| `--callback-port <port>` | Pin the loopback callback port, primarily for an SSH session that forwards the same fixed port |
53-
| `--scope <scope>` | Key space for the pairing-code handoff. Only `copilot` changes anything, and it forces that flow |
5471
| `-y, --yes` | Overwrite an existing API-key profile without prompting |
5572

5673
### Over SSH or in a container
5774

58-
The browser login needs your browser to reach a listener on the machine running
75+
OAuth login needs your browser to reach a listener on the machine running
5976
`sim`. When it cannot — an SSH session, a dev container, a remote box — use the
60-
pairing-code handoff, which the CLI selects automatically in an SSH session:
77+
API-key pairing flow. The CLI selects it automatically in an SSH session when
78+
no method or callback port is specified:
6179

6280
```bash
63-
sim login --browserless
81+
sim login --method api-key --no-browser
6482
```
6583

6684
The terminal prints a pairing code and a URL you can open on any device:
@@ -85,16 +103,18 @@ approving. That check is what binds the approval to your terminal.
85103

86104
The handoff issues a permanent personal API key rather than a renewing login,
87105
so revoke it under **Settings → API keys** when you are done with that machine.
88-
It is also the path for a deployment that predates OAuth sign-in, or one with
89-
the provider switched off; the CLI detects that and falls back on its own.
106+
It also works when OAuth is unavailable or switched off, provided the server
107+
supports platform API-key pairing. When `--method` is omitted, the CLI checks
108+
OAuth availability and selects pairing if unavailable; that discovery does not
109+
verify pairing compatibility. An explicit `--method oauth` fails in that case.
90110

91-
`--read-only` and `--callback-port` belong to the browser login and have no
111+
`--read-only` and `--callback-port` belong to OAuth login and have no
92112
meaning here, so combining either with the handoff stops the login rather than
93113
storing a credential you did not ask for. If your SSH session forwards a port
94-
from the remote loopback interface to the browser's machine, pass that same
95-
`--callback-port <port>` on its own. An ordinary container port publication
114+
from the remote loopback interface to the browser's machine, use
115+
`--method oauth --callback-port <port>` with that port. An ordinary container port publication
96116
cannot reach a listener bound to the container's own loopback interface; use
97-
`--browserless` there.
117+
`--method api-key` there.
98118

99119
### Picking a workspace
100120

@@ -160,7 +180,7 @@ authentication profile entirely is refused until its workspace profiles are
160180
removed, so it cannot leave dangling references.
161181

162182
<Callout type="warn">
163-
For a login created with `--browserless`, `sim logout` removes the API key from
183+
For a login created with `--method api-key`, `sim logout` removes the API key from
164184
disk but does **not** revoke it. Revoke keys under **Settings → API keys**.
165185
</Callout>
166186

@@ -279,5 +299,5 @@ a copied token from surviving an ambiguous refresh.
279299
## Organization audit logs
280300

281301
`sim audit-logs` requires a **personal** credential — an OAuth login, or the
282-
personal API key `sim login --browserless` issues. A workspace-scoped key cannot
302+
personal API key `sim login --method api-key` issues. A workspace-scoped key cannot
283303
read organization-level audit logs.

apps/docs/content/docs/cli/commands.mdx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,8 @@ sim login [options]
6464

6565
| Option | Required | Description |
6666
| --- | --- | --- |
67-
| `--scope <scope>` | No | Key space for the pairing-code handoff; only "copilot" changes anything, and it forces that flow. Defaults to `platform`. |
68-
| `--no-browser` | No | Print the URL instead of opening a browser. |
69-
| `--browserless` | No | Use the pairing-code handoff for a terminal whose browser cannot reach it (SSH, containers). |
67+
| `--method <method>` | No | Credential to obtain: oauth requires OAuth support; api-key creates a permanent key through pairing (auto-selects when omitted). Accepted values: `oauth`, `api-key`. |
68+
| `--no-browser` | No | Print the approval URL without opening it (either login method). |
7069
| `--read-only` | No | Ask only for permission to read, never to change anything. |
7170
| `--callback-port <port>` | No | Pin the local port the browser returns to. |
7271
| `-y, --yes` | No | Overwrite an existing API-key profile without prompting. |

apps/docs/content/docs/cli/reference.mdx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,8 @@ sim login [options]
3838

3939
| Option | Required | Description |
4040
| --- | --- | --- |
41-
| `--scope <scope>` | No | Key space for the pairing-code handoff; only "copilot" changes anything, and it forces that flow. Defaults to `platform`. |
42-
| `--no-browser` | No | Print the URL instead of opening a browser. |
43-
| `--browserless` | No | Use the pairing-code handoff for a terminal whose browser cannot reach it (SSH, containers). |
41+
| `--method <method>` | No | Credential to obtain: oauth requires OAuth support; api-key creates a permanent key through pairing (auto-selects when omitted). Accepted values: `oauth`, `api-key`. |
42+
| `--no-browser` | No | Print the approval URL without opening it (either login method). |
4443
| `--read-only` | No | Ask only for permission to read, never to change anything. |
4544
| `--callback-port <port>` | No | Pin the local port the browser returns to. |
4645
| `-y, --yes` | No | Overwrite an existing API-key profile without prompting. |

packages/sim-cli/README.md

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,16 +33,27 @@ Sign in to the default profile:
3333
sim login
3434
```
3535

36-
The CLI opens Sim in your browser, asks you to approve the requested access,
36+
With no `--method`, the CLI prefers OAuth when the server offers it and a local
37+
browser callback is possible. It selects API-key pairing for remote terminals
38+
or servers without OAuth. Use `sim login --method oauth` to require OAuth;
39+
if the server does not offer it, login fails without creating an API key.
40+
41+
OAuth login opens Sim in your browser, asks you to approve the requested access,
3742
and receives the one-time authorization code on a loopback callback. It stores
3843
a short-lived OAuth login that renews automatically and can be revoked under
3944
**Settings → General → Authorized apps**. Choose a default workspace afterward with
4045
`sim configure --set-workspace <id>`.
4146

42-
Use `sim login --no-browser` to print the OAuth URL without opening it. The
43-
browser must still be able to reach the CLI's loopback callback. Over SSH or in
44-
a container without port forwarding, use `sim login --browserless`; that
45-
pairing-code fallback creates a permanent personal API key instead.
47+
Use `--no-browser` with either method to print the approval URL without opening
48+
it. OAuth still needs the browser to reach the CLI's loopback callback. Over SSH
49+
or in a container without port forwarding, use
50+
`sim login --method api-key --no-browser` to approve from another device and
51+
create a permanent personal API key. `--method api-key` creates a new key;
52+
set `SIM_API_KEY` to supply an existing one.
53+
54+
Pairing requires a server that supports `platform` API keys. Upgrade older
55+
deployments that only issue `copilot` keys before login; they are not compatible
56+
with the platform CLI. OAuth discovery does not check pairing compatibility.
4657

4758
Check the active profile and verify that its endpoint, credential, and workspace
4859
work together:

packages/sim-cli/src/auth/device-flow.test.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -190,9 +190,7 @@ describe('createAuthRequest', () => {
190190
const prefixed = 'https://host.test/sim'
191191
const auth = createAuthRequest()
192192

193-
expect(buildApprovalUrl(prefixed, auth, 'platform')).toMatch(
194-
/^https:\/\/host\.test\/sim\/cli\/auth\?/
195-
)
193+
expect(buildApprovalUrl(prefixed, auth)).toMatch(/^https:\/\/host\.test\/sim\/cli\/auth\?/)
196194

197195
// `spyOn`, like the rest of this file: `restoreAllMocks` in teardown undoes
198196
// it, whereas a `stubGlobal` would outlive the test and leak this
@@ -210,13 +208,18 @@ describe('createAuthRequest', () => {
210208

211209
it('omits an absent workspace rather than sending it blank', () => {
212210
const auth = createAuthRequest()
213-
expect(buildApprovalUrl(ENDPOINT, auth, 'platform')).not.toContain('workspace=')
214-
expect(buildApprovalUrl(ENDPOINT, auth, 'platform', 'ws_1')).toContain('workspace=ws_1')
211+
expect(buildApprovalUrl(ENDPOINT, auth)).not.toContain('workspace=')
212+
expect(buildApprovalUrl(ENDPOINT, auth, 'ws_1')).toContain('workspace=ws_1')
213+
})
214+
215+
it('always requests a platform API key', () => {
216+
const url = new URL(buildApprovalUrl(ENDPOINT, createAuthRequest()))
217+
expect(url.searchParams.get('scope')).toBe('platform')
215218
})
216219

217220
it('never puts the poll secret in the browser URL', () => {
218221
const auth = createAuthRequest()
219-
const url = buildApprovalUrl(ENDPOINT, auth, 'platform', 'ws_1')
222+
const url = buildApprovalUrl(ENDPOINT, auth, 'ws_1')
220223
expect(url).toContain(encodeURIComponent(auth.challenge))
221224
expect(url).not.toContain(auth.pollSecret)
222225
})

packages/sim-cli/src/auth/device-flow.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ const RETRYABLE_POLL_STATUSES = new Set([409, 429, 500, 502, 503, 504])
5454
*/
5555
const TRANSPORT_FAILURES_BEFORE_WARNING = 3
5656

57-
export type CliAuthScope = 'copilot' | 'platform'
57+
type CliAuthScope = 'copilot' | 'platform'
5858

5959
export interface AuthRequest {
6060
/** Semi-public rendezvous handle; travels in the browser URL. */
@@ -103,14 +103,13 @@ export function createAuthRequest(): AuthRequest {
103103
export function buildApprovalUrl(
104104
endpoint: string,
105105
auth: AuthRequest,
106-
scope: CliAuthScope,
107106
workspaceId?: string
108107
): string {
109108
return buildUrl(endpoint, APPROVAL_PATH, {
110109
request: auth.request,
111110
challenge: auth.challenge,
112111
pairing: auth.pairing,
113-
scope,
112+
scope: 'platform',
114113
workspace: workspaceId,
115114
})
116115
}

packages/sim-cli/src/auth/oauth-flow.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -300,15 +300,15 @@ describe('loginWithBrowser', () => {
300300
expect(response.headers['cache-control']).toBe('no-store')
301301
})
302302

303-
it('gives up after the timeout with the browserless fallback named', async () => {
303+
it('gives up after the timeout with an explicit API-key login alternative', async () => {
304304
vi.stubGlobal('fetch', vi.fn())
305305
await expect(
306306
loginWithBrowser(ENDPOINT, {
307307
scopes: ['offline_access', 'api:read'],
308308
onAuthorizeUrl: () => {},
309309
timeoutMs: 20,
310310
})
311-
).rejects.toThrow('--browserless')
311+
).rejects.toThrow('--method api-key')
312312
})
313313
})
314314

packages/sim-cli/src/auth/oauth-flow.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -432,7 +432,7 @@ function listenForCallback(
432432
finish({
433433
ok: false,
434434
error: new SimApiError(
435-
`Timed out after ${Math.round(timeoutMs / 60000)} minutes waiting for the browser. Run sim login again, or use --browserless if this terminal's browser cannot reach it.`,
435+
`Timed out after ${Math.round(timeoutMs / 60000)} minutes waiting for the browser. Run sim login again, or use --method api-key if this terminal's browser cannot reach it.`,
436436
0
437437
),
438438
}),
@@ -567,9 +567,9 @@ export async function loginWithBrowser(
567567

568568
/**
569569
* Whether this terminal's browser is unlikely to reach a loopback listener on
570-
* this machine: an SSH session, or a Linux box with no display. The signals
571-
* Railway and Stripe use to auto-select their pairing flows; `--browserless`
572-
* forces it and `--callback-port` overrides the guess.
570+
* this machine: an SSH session, or a Linux box with no display. An explicit
571+
* `--method` selects the flow without this guess; `--callback-port` opts into
572+
* OAuth when a forwarded port makes the loopback listener reachable.
573573
*/
574574
export function isLikelyRemoteSession(
575575
env: NodeJS.ProcessEnv = process.env,

0 commit comments

Comments
 (0)