Skip to content
12 changes: 8 additions & 4 deletions apps/cli-docs/src/fragments/commands/auth.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ server has none); accounts on other servers are never replaced.
# Add (or log in again to) a specific account without prompting
sentry auth login jane@example.com
sentry auth login jane@example.com --url https://sentry.example.com
sentry auth login jane@example.com:sentry.example.com # same as --url

# Show stored accounts; * marks the active one
sentry auth list
Expand All @@ -90,16 +91,19 @@ nothing) if it belongs to someone else.
Select an account by email, username, user ID, or account ID (from
`sentry auth list --json`). `sentry auth switch` also accepts a close match —
`sentry auth switch jane` picks `jane@example.com` with a warning when it's the
only candidate. If the same email is stored on several servers, add `--url`:
only candidate. If the same email is stored on several servers, add the server
after a colon, as `sentry org list` shows it
(`sentry auth switch jane@example.com:sentry.example.com`), or pass `--url`:
`sentry auth switch jane@example.com --url https://sentry.example.com`.

Most of the time you don't need to switch: commands pick the stored account
that fits what they target, without changing the active account:

- **Organization** — when the target organization isn't in the active
account's organization list (cached at login and by `sentry org list`) but
another stored account has used it, that account is used. This includes
`oNNNN` IDs from DSNs.
account's organization list (cached at login and by `sentry org list`,
which lists every stored account's organizations) but another stored
account has used it, that account is used. This includes `oNNNN` IDs from
DSNs.
- **Server** — `SENTRY_HOST`/`SENTRY_URL`, `.sentryclirc`, or a pasted Sentry
URL for another account's server (for example a self-hosted issue link) uses
the account stored for that server.
Expand Down
34 changes: 34 additions & 0 deletions apps/cli-docs/src/fragments/commands/org.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,40 @@ my-org My Organization owner
another-org Another Org member
```

With several stored accounts (see `sentry auth list`), `sentry org list` lists
the organizations of every logged-in account together, each fetched with that
account's own credentials on its own server, plus an `ACCOUNT` column naming
each account as `<email>:<server>` — the form `--account` and
`sentry auth login` accept. An
organization reachable from several accounts on the same server is listed once,
under the active account if it is one of them, else the most recently used.
Accounts whose login fails are skipped with a warning. `SENTRY_HOST`/`SENTRY_URL` limits the list to accounts on that server,
and `--account` to a single account. The stored active account never changes.
With `--json`, each entry's `account` carries that notation as `name`, with its
parts alongside: `email` (or `username`) and `server`.

```
$ sentry org list
SLUG NAME ACCOUNT
my-org My Organization jane@example.com:sentry.io
acme-internal Acme Internal jane@acme.com:sentry.acme.com
```

```bash
# Only one account's organizations
sentry org list --account jane@acme.com
# The same email on several servers: add the server
sentry org list -a jane@example.com:sentry.io
```

```bash
# Only organizations whose slug or name matches "acme", best matches first
# (case-insensitive; prefix, substring, and small typos match too)
sentry org list acme
# ...or matches a glob
sentry org list 'acme-*'
```

```bash
# View organization details
sentry org view my-org
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/plugins/sentry-cli/skills/sentry-cli/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -394,7 +394,7 @@ Authenticate with Sentry

Work with Sentry organizations

- `sentry org list` — List organizations
- `sentry org list [<filter>]` — List organizations
- `sentry org view [<org>]` — View details of an organization

→ Full flags and examples: `references/org.md`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ Switch the active account
# Add (or log in again to) a specific account without prompting
sentry auth login jane@example.com
sentry auth login jane@example.com --url https://sentry.example.com
sentry auth login jane@example.com:sentry.example.com # same as --url

# Show stored accounts; * marks the active one
sentry auth list
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,14 @@ requires:

Work with Sentry organizations

### `sentry org list`
### `sentry org list [<filter>]`

List organizations

**Flags:**
- `-n, --limit <value> - Maximum number of organizations to list - (default: "25")`
- `-f, --fresh - Bypass cache, re-detect projects, and fetch fresh data`
- `-a, --account <value> - Only list organizations of this stored account (email, username, or account id; <who>:<server> for one server)`

### `sentry org view [<org>]`

Expand All @@ -33,6 +34,17 @@ View details of an organization
# List organizations
sentry org list

# Only one account's organizations
sentry org list --account jane@acme.com
# The same email on several servers: add the server
sentry org list -a jane@example.com:sentry.io

# Only organizations whose slug or name matches "acme", best matches first
# (case-insensitive; prefix, substring, and small typos match too)
sentry org list acme
# ...or matches a glob
sentry org list 'acme-*'

# View organization details
sentry org view my-org

Expand Down
102 changes: 83 additions & 19 deletions packages/cli/src/commands/auth/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,11 @@ import {
setActiveAccount,
setAuthToken,
} from "../../lib/db/auth.js";
import { getAccountRow, isAccountRowUsable } from "../../lib/db/accounts.js";
import {
getAccountRow,
isAccountRowUsable,
splitAccountSelector,
} from "../../lib/db/accounts.js";
import { getDbPath } from "../../lib/db/index.js";
import { getUserInfo, setUserInfo } from "../../lib/db/user.js";
import { getEnv } from "../../lib/env.js";
Expand Down Expand Up @@ -154,20 +158,77 @@ function resolveLoginScope(flags: LoginFlags): string | undefined {
*/
/** @internal exported for testing */
/**
* Parse the optional `email` argument of `sentry auth login`.
* Parse the optional `email` argument of `sentry auth login`: an email, or
* `<email>:<server>` (the notation `sentry org list` shows, e.g.
* `jane@acme.com:sentry.acme.com`) to also pick the server like `--url`.
* Returns the trimmed argument; {@link splitLoginTarget} splits it.
*
* @throws {ValidationError} when the value is not an email address
* @throws {ValidationError} when the value is not an email address, or the
* server part is not a valid URL
*/
export function parseLoginEmail(raw: string): string {
const email = raw.trim();
const at = email.indexOf("@");
if (at <= 0 || at === email.length - 1 || /\s/.test(email)) {
const target = raw.trim();
splitLoginTarget(target, raw);
return target;
}

/**
* Split a `sentry auth login` target into its email and, for
* `<email>:<server>`, the server's normalized origin. Uses the same
* {@link splitAccountSelector} as `--account`, plus an email check.
*
* @throws {ValidationError} when either part is invalid
*/
export function splitLoginTarget(
target: string,
raw = target,
): { email: string; url?: string } {
const parts = splitAccountSelector(target);
if (!(parts && isEmailAddress(parts.who))) {
throw new ValidationError(
`Expected an email address to log in as, got '${raw}'.`,
`Expected an email address, or <email>:<server> with a valid server, to log in as; got '${raw}'.`,
"email",
);
}
return email;
return parts.server
? { email: parts.who, url: parts.server }
: { email: parts.who };
}

/** One `@` with text on both sides, and no whitespace. */
function isEmailAddress(value: string): boolean {
const at = value.indexOf("@");
return (
at > 0 &&
at < value.length - 1 &&
value.indexOf("@", at + 1) === -1 &&
!/\s/.test(value)
);
}

/**
* Apply the server of an `<email>:<server>` login target as `--url`.
*
* @throws {ValidationError} when `--url` names a different server
*/
function applyLoginTarget(
flags: LoginFlags,
target: string | undefined,
): { flags: LoginFlags; email?: string } {
if (target === undefined) {
return { flags };
}
const { email, url } = splitLoginTarget(target);
if (!url) {
return { flags, email };
}
if (flags.url && flags.url !== url) {
throw new ValidationError(
`'${target}' names ${url}, but --url is ${flags.url}. Pass only one server.`,
"url",
);
}
return { flags: { ...flags, url }, email };
}

export function parseLoginUrl(raw: string): string {
Expand Down Expand Up @@ -470,21 +531,22 @@ export const loginCommand = buildCommand({
"Log in to Sentry using OAuth or an API token.\n\n" +
"The OAuth flow uses a device code - you'll be given a code to enter at a URL.\n" +
"Alternatively, use --token to authenticate with an existing API token.\n\n" +
"For self-hosted Sentry, pass --url <url> to authenticate against that\n" +
"instance. This is the ONLY way to trust a new Sentry host — URL\n" +
"arguments and config files are refused when they don't match the\n" +
"currently-authenticated host.\n\n" +
"For self-hosted Sentry, pass --url <url> (or an <email>:<server>\n" +
"argument) to authenticate against that instance. This is the ONLY way\n" +
"to trust a new Sentry host — URL arguments and config files are\n" +
"refused when they don't match the currently-authenticated host.\n\n" +
"When already logged in, the new login is stored as another account\n" +
"(see 'sentry auth list' / 'sentry auth switch'); --force instead\n" +
"replaces the stored login for that server (accounts on other servers\n" +
"are kept). Pass an email to add or log in\n" +
"again to that account without prompting: Sentry approves the request\n" +
"as whoever is signed in to the browser, so a login approved as another\n" +
"user is refused and nothing is saved.\n\n" +
"are kept). Pass an email (or <email>:<server>, as 'sentry org list'\n" +
"shows it) to add or log in again to that account without prompting:\n" +
"Sentry approves the request as whoever is signed in to the browser,\n" +
"so a login approved as another user is refused and nothing is saved.\n\n" +
"Examples:\n" +
" sentry auth login\n" +
" sentry auth login jane@acme.com\n" +
" sentry auth login jane@acme.com --url https://sentry.acme.com",
" sentry auth login jane@acme.com --url https://sentry.acme.com\n" +
" sentry auth login jane@acme.com:sentry.acme.com",
},
parameters: {
flags: {
Expand Down Expand Up @@ -539,15 +601,17 @@ export const loginCommand = buildCommand({
{
placeholder: "email",
brief:
"Email of the account to add or log in to again; the login is refused if you approve it as someone else",
"Email of the account to add or log in to again (<email>:<server> also sets --url); the login is refused if you approve it as someone else",
parse: parseLoginEmail,
optional: true,
},
],
},
},
output: { human: formatLoginResult },
async *func(this: SentryContext, flags: LoginFlags, email?: string) {
async *func(this: SentryContext, rawFlags: LoginFlags, target?: string) {
// `<email>:<server>` picks the server just like `--url <server>`.
const { flags, email } = applyLoginTarget(rawFlags, target);
// Resolve OAuth scopes up front so conflicting flag combinations
// (--token + --read-only/--scope, --read-only + --scope) and invalid
// scope values fail fast before any network or DB work.
Expand Down
Loading
Loading