Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 38 additions & 18 deletions apps/docs/content/docs/cli/authentication.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,35 @@ description: Sign in from the terminal, authenticate CI with an API key, and kee

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

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

## Signing in

```bash
sim login
```

The CLI opens your browser on Sim's sign-in page, then on a consent page that
Choose a method explicitly when the credential type matters:

```bash
sim login --method oauth
sim login --method api-key
```

`--method oauth` requires OAuth support and never falls back to an API key. The
server must enable `OAUTH_PROVIDER_ENABLED=true`. Explicit OAuth selection also
overrides SSH/headless detection; your browser still needs to reach the CLI's
local callback. `--method api-key` uses pairing-code approval to create a new
permanent API key. To supply an existing key, set `SIM_API_KEY` instead.

API-key pairing requires a server that supports `platform` API keys. Upgrade
older deployments that only issue `copilot` keys before starting login; those
keys cannot authenticate the platform CLI.

OAuth login opens your browser on Sim's sign-in page, then on a consent page that
names the Sim CLI and what it will be able to do. Approve, and the browser hands
control back to the terminal:

Expand Down Expand Up @@ -46,21 +64,21 @@ your login.

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

### Over SSH or in a container

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

```bash
sim login --browserless
sim login --method api-key --no-browser
```

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

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

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

### Picking a workspace

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

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

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

`sim audit-logs` requires a **personal** credential — an OAuth login, or the
personal API key `sim login --browserless` issues. A workspace-scoped key cannot
personal API key `sim login --method api-key` issues. A workspace-scoped key cannot
read organization-level audit logs.
5 changes: 2 additions & 3 deletions apps/docs/content/docs/cli/commands.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,8 @@ sim login [options]

| Option | Required | Description |
| --- | --- | --- |
| `--scope <scope>` | No | Key space for the pairing-code handoff; only "copilot" changes anything, and it forces that flow. Defaults to `platform`. |
| `--no-browser` | No | Print the URL instead of opening a browser. |
| `--browserless` | No | Use the pairing-code handoff for a terminal whose browser cannot reach it (SSH, containers). |
| `--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`. |
| `--no-browser` | No | Print the approval URL without opening it (either login method). |
| `--read-only` | No | Ask only for permission to read, never to change anything. |
| `--callback-port <port>` | No | Pin the local port the browser returns to. |
| `-y, --yes` | No | Overwrite an existing API-key profile without prompting. |
Expand Down
5 changes: 2 additions & 3 deletions apps/docs/content/docs/cli/reference.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,8 @@ sim login [options]

| Option | Required | Description |
| --- | --- | --- |
| `--scope <scope>` | No | Key space for the pairing-code handoff; only "copilot" changes anything, and it forces that flow. Defaults to `platform`. |
| `--no-browser` | No | Print the URL instead of opening a browser. |
| `--browserless` | No | Use the pairing-code handoff for a terminal whose browser cannot reach it (SSH, containers). |
| `--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`. |
| `--no-browser` | No | Print the approval URL without opening it (either login method). |
| `--read-only` | No | Ask only for permission to read, never to change anything. |
| `--callback-port <port>` | No | Pin the local port the browser returns to. |
| `-y, --yes` | No | Overwrite an existing API-key profile without prompting. |
Expand Down
21 changes: 16 additions & 5 deletions packages/sim-cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,16 +33,27 @@ Sign in to the default profile:
sim login
```

The CLI opens Sim in your browser, asks you to approve the requested access,
With no `--method`, the CLI prefers OAuth when the server offers it and a local
browser callback is possible. It selects API-key pairing for remote terminals
or servers without OAuth. Use `sim login --method oauth` to require OAuth;
if the server does not offer it, login fails without creating an API key.

OAuth login opens Sim in your browser, asks you to approve the requested access,
and receives the one-time authorization code on a loopback callback. It stores
a short-lived OAuth login that renews automatically and can be revoked under
**Settings → General → Authorized apps**. Choose a default workspace afterward with
`sim configure --set-workspace <id>`.

Use `sim login --no-browser` to print the OAuth URL without opening it. The
browser must still be able to reach the CLI's loopback callback. Over SSH or in
a container without port forwarding, use `sim login --browserless`; that
pairing-code fallback creates a permanent personal API key instead.
Use `--no-browser` with either method to print the approval URL without opening
it. OAuth still needs the browser to reach the CLI's loopback callback. Over SSH
or in a container without port forwarding, use
`sim login --method api-key --no-browser` to approve from another device and
create a permanent personal API key. `--method api-key` creates a new key;
set `SIM_API_KEY` to supply an existing one.

Pairing requires a server that supports `platform` API keys. Upgrade older
deployments that only issue `copilot` keys before login; they are not compatible
with the platform CLI. OAuth discovery does not check pairing compatibility.

Check the active profile and verify that its endpoint, credential, and workspace
work together:
Expand Down
15 changes: 9 additions & 6 deletions packages/sim-cli/src/auth/device-flow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,9 +190,7 @@ describe('createAuthRequest', () => {
const prefixed = 'https://host.test/sim'
const auth = createAuthRequest()

expect(buildApprovalUrl(prefixed, auth, 'platform')).toMatch(
/^https:\/\/host\.test\/sim\/cli\/auth\?/
)
expect(buildApprovalUrl(prefixed, auth)).toMatch(/^https:\/\/host\.test\/sim\/cli\/auth\?/)

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

it('omits an absent workspace rather than sending it blank', () => {
const auth = createAuthRequest()
expect(buildApprovalUrl(ENDPOINT, auth, 'platform')).not.toContain('workspace=')
expect(buildApprovalUrl(ENDPOINT, auth, 'platform', 'ws_1')).toContain('workspace=ws_1')
expect(buildApprovalUrl(ENDPOINT, auth)).not.toContain('workspace=')
expect(buildApprovalUrl(ENDPOINT, auth, 'ws_1')).toContain('workspace=ws_1')
})

it('always requests a platform API key', () => {
const url = new URL(buildApprovalUrl(ENDPOINT, createAuthRequest()))
expect(url.searchParams.get('scope')).toBe('platform')
})

it('never puts the poll secret in the browser URL', () => {
const auth = createAuthRequest()
const url = buildApprovalUrl(ENDPOINT, auth, 'platform', 'ws_1')
const url = buildApprovalUrl(ENDPOINT, auth, 'ws_1')
expect(url).toContain(encodeURIComponent(auth.challenge))
expect(url).not.toContain(auth.pollSecret)
})
Expand Down
5 changes: 2 additions & 3 deletions packages/sim-cli/src/auth/device-flow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ const RETRYABLE_POLL_STATUSES = new Set([409, 429, 500, 502, 503, 504])
*/
const TRANSPORT_FAILURES_BEFORE_WARNING = 3

export type CliAuthScope = 'copilot' | 'platform'
type CliAuthScope = 'copilot' | 'platform'

export interface AuthRequest {
/** Semi-public rendezvous handle; travels in the browser URL. */
Expand Down Expand Up @@ -103,14 +103,13 @@ export function createAuthRequest(): AuthRequest {
export function buildApprovalUrl(
endpoint: string,
auth: AuthRequest,
scope: CliAuthScope,
workspaceId?: string
): string {
return buildUrl(endpoint, APPROVAL_PATH, {
request: auth.request,
challenge: auth.challenge,
pairing: auth.pairing,
scope,
scope: 'platform',
workspace: workspaceId,
})
}
Expand Down
4 changes: 2 additions & 2 deletions packages/sim-cli/src/auth/oauth-flow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -300,15 +300,15 @@ describe('loginWithBrowser', () => {
expect(response.headers['cache-control']).toBe('no-store')
})

it('gives up after the timeout with the browserless fallback named', async () => {
it('gives up after the timeout with an explicit API-key login alternative', async () => {
vi.stubGlobal('fetch', vi.fn())
await expect(
loginWithBrowser(ENDPOINT, {
scopes: ['offline_access', 'api:read'],
onAuthorizeUrl: () => {},
timeoutMs: 20,
})
).rejects.toThrow('--browserless')
).rejects.toThrow('--method api-key')
})
})

Expand Down
8 changes: 4 additions & 4 deletions packages/sim-cli/src/auth/oauth-flow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -432,7 +432,7 @@ function listenForCallback(
finish({
ok: false,
error: new SimApiError(
`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.`,
`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.`,
0
),
}),
Expand Down Expand Up @@ -567,9 +567,9 @@ export async function loginWithBrowser(

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