Skip to content

Commit df25473

Browse files
authored
fix: only mention token scopes in 403 errors for env-var tokens (#512)
## Follow-up to PR #508 Addresses review feedback: the regular `sentry auth login` OAuth flow always grants all required scopes, so suggesting to check token scopes is misleading for those users. ## Fix Now checks `isEnvTokenActive()` to distinguish auth methods: **OAuth users** (regular login): ``` You may not have access to this organization. Re-authenticate with: sentry auth login Verify project membership: sentry project list <org>/ ``` **Env-var token users** (custom token): ``` Your SENTRY_AUTH_TOKEN token may lack the required scopes (org:read, project:read) Check token scopes at: https://sentry.io/settings/auth-tokens/ Verify project membership: sentry project list <org>/ ``` Applied to both locations: - `build403Detail()` in issue list (`handleResolvedTargets` + org-all) - `listOrganizationsInRegion()` in org listing
1 parent 9575ac1 commit df25473

File tree

4 files changed

+42
-21
lines changed

4 files changed

+42
-21
lines changed

AGENTS.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -836,6 +836,9 @@ mock.module("./some-module", () => ({
836836
<!-- lore:019c969a-1c90-7041-88a8-4e4d9a51ebed -->
837837
* **Multiple mockFetch calls replace each other — use unified mocks for multi-endpoint tests**: Bun test mocking gotchas: (1) \`mockFetch()\` replaces \`globalThis.fetch\` — calling it twice replaces the first mock. Use a single unified fetch mock dispatching by URL pattern. (2) \`mock.module()\` pollutes the module registry for ALL subsequent test files. Tests using it must live in \`test/isolated/\` and run via \`test:isolated\`. This also causes \`delta-upgrade.test.ts\` to fail when run alongside \`test/isolated/delta-upgrade.test.ts\` — the isolated test's \`mock.module()\` replaces \`CLI\_VERSION\` for all subsequent files. (3) For \`Bun.spawn\`, use direct property assignment in \`beforeEach\`/\`afterEach\`.
838838
839+
<!-- lore:019d0b36-5dae-722b-8094-aca5cfbb5527 -->
840+
* **Seer trial prompt requires interactive terminal — AI agents never see it**: The Seer trial prompt middleware in \`bin.ts\` (\`executeWithSeerTrialPrompt\`) checks \`isatty(0)\` before prompting. Most Seer callers are AI coding agents running non-interactively, so the prompt never fires and \`SeerError\` propagates with a generic message. Fix: \`SeerError.format()\` should include an actionable command like \`sentry trial start seer \<org>\` that non-interactive callers can execute directly. Also, \`handleSeerApiError\` was wrapping \`ApiError\` in plain \`Error\`, losing the type — the middleware's \`instanceof ApiError\` check then failed silently. Always return the original \`ApiError\` from error handlers to preserve type identity for downstream middleware.
841+
839842
<!-- lore:019c9741-d78e-73b1-87c2-e360ef6c7475 -->
840843
* **useTestConfigDir without isolateProjectRoot causes DSN scanning of repo tree**: \`useTestConfigDir()\` creates temp dirs under \`.test-tmp/\` in the repo tree. Without \`{ isolateProjectRoot: true }\`, \`findProjectRoot\` walks up and finds the repo's \`.git\`, causing DSN detection to scan real source code and trigger network calls against test mocks (timeouts). Always pass \`isolateProjectRoot: true\` when tests exercise \`resolveOrg\`, \`detectDsn\`, or \`findProjectRoot\`.
841844
@@ -844,6 +847,9 @@ mock.module("./some-module", () => ({
844847
<!-- lore:019d0b04-ccf7-7e74-a049-2ca6b8faa85f -->
845848
* **Cursor Bugbot review comments: fix valid bugs before merging**: Cursor Bugbot sometimes catches real logic bugs in PRs (not just style). In CLI-89, Bugbot identified that \`flatResults.length === 0\` was an incorrect proxy for "all regions failed" since fulfilled-but-empty regions are indistinguishable from rejected ones. Always read Bugbot's full comment body (via \`gh api repos/OWNER/REPO/pulls/NUM/comments\`) and fix valid findings before merging. Bugbot comments include a \`BUGBOT\_BUG\_ID\` HTML comment for tracking.
846849
850+
<!-- lore:019d0b36-5da2-750c-b26f-630a2927bd79 -->
851+
* **findProjectsByPattern as fuzzy fallback for exact slug misses**: When \`findProjectsBySlug\` returns empty (no exact match), use \`findProjectsByPattern\` as a fallback to suggest similar projects. \`findProjectsByPattern\` does bidirectional word-boundary matching (\`matchesWordBoundary\`) against all projects in all orgs — the same logic used for directory name inference. In the \`project-search\` handler, call it after the exact miss, format matches as \`\<org>/\<slug>\` suggestions in the \`ResolutionError\`. This avoids a dead-end error for typos like 'patagonai' when 'patagon-ai' exists. Note: \`findProjectsByPattern\` makes additional API calls (lists all projects per org), so only call it on the failure path.
852+
847853
<!-- lore:019c972c-9f11-7c0d-96ce-3f8cc2641175 -->
848854
* **Org-scoped SDK calls follow getOrgSdkConfig + unwrapResult pattern**: All org-scoped API calls in src/lib/api-client.ts: (1) call \`getOrgSdkConfig(orgSlug)\` for regional URL + SDK config, (2) spread into SDK function: \`{ ...config, path: { organization\_id\_or\_slug: orgSlug, ... } }\`, (3) pass to \`unwrapResult(result, errorContext)\`. Shared helpers \`resolveAllTargets\`/\`resolveOrgAndProject\` must NOT call \`fetchProjectId\` — commands that need it enrich targets themselves.
849855

src/commands/issue/list.ts

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
looksLikeIssueShortId,
2222
parseOrgProjectArg,
2323
} from "../../lib/arg-parsing.js";
24+
import { getActiveEnvVarName, isEnvTokenActive } from "../../lib/db/auth.js";
2425
import {
2526
buildPaginationContextKey,
2627
clearPaginationCursor,
@@ -1006,8 +1007,9 @@ function enrichIssueListError(
10061007
/**
10071008
* Build an enriched error detail for 403 Forbidden responses.
10081009
*
1009-
* Suggests checking token scopes and project membership — the most common
1010-
* causes of 403 on the issues endpoint (CLI-97, 41 users).
1010+
* Only mentions token scopes when using a custom env-var token
1011+
* (SENTRY_AUTH_TOKEN / SENTRY_TOKEN) since the regular `sentry auth login`
1012+
* OAuth flow always grants the required scopes.
10111013
*
10121014
* @param originalDetail - The API response detail (may be undefined)
10131015
* @returns Enhanced detail string with suggestions
@@ -1019,12 +1021,18 @@ function build403Detail(originalDetail: string | undefined): string {
10191021
lines.push(originalDetail, "");
10201022
}
10211023

1022-
lines.push(
1023-
"Suggestions:",
1024-
" • Your auth token may lack the required scopes (org:read, project:read)",
1025-
" • Re-authenticate with: sentry auth login",
1026-
" • Verify project membership: sentry project list <org>/"
1027-
);
1024+
lines.push("Suggestions:");
1025+
1026+
if (isEnvTokenActive()) {
1027+
lines.push(
1028+
` • Your ${getActiveEnvVarName()} token may lack the required scopes (org:read, project:read)`,
1029+
" • Check token scopes at: https://sentry.io/settings/auth-tokens/"
1030+
);
1031+
} else {
1032+
lines.push(" • Re-authenticate with: sentry auth login");
1033+
}
1034+
1035+
lines.push(" • Verify project membership: sentry project list <org>/");
10281036

10291037
return lines.join("\n ");
10301038
}

src/lib/api/organizations.ts

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
UserRegionsResponseSchema,
1717
} from "../../types/index.js";
1818

19+
import { getActiveEnvVarName, isEnvTokenActive } from "../db/auth.js";
1920
import { ApiError, withAuthGuard } from "../errors.js";
2021
import {
2122
getApiBaseUrl,
@@ -64,20 +65,25 @@ export async function listOrganizationsInRegion(
6465
const data = unwrapResult(result, "Failed to list organizations");
6566
return data as unknown as SentryOrganization[];
6667
} catch (error) {
67-
// Enrich 403 errors with token scope guidance (CLI-89, 24 users).
68-
// A 403 from the organizations endpoint usually means the auth token
69-
// lacks the org:read scope — either it's an internal integration token
70-
// with limited scopes, or a self-hosted token without the right permissions.
68+
// Enrich 403 errors with contextual guidance (CLI-89, 24 users).
69+
// Only mention token scopes when using a custom env-var token —
70+
// the regular `sentry auth login` OAuth flow always grants org:read.
7171
if (error instanceof ApiError && error.status === 403) {
7272
const lines: string[] = [];
7373
if (error.detail) {
7474
lines.push(error.detail, "");
7575
}
76-
lines.push(
77-
"Your auth token may lack the required 'org:read' scope.",
78-
"Re-authenticate with: sentry auth login",
79-
"Or check token scopes at: https://sentry.io/settings/auth-tokens/"
80-
);
76+
if (isEnvTokenActive()) {
77+
lines.push(
78+
`Your ${getActiveEnvVarName()} token may lack the required 'org:read' scope.`,
79+
"Check token scopes at: https://sentry.io/settings/auth-tokens/"
80+
);
81+
} else {
82+
lines.push(
83+
"You may not have access to this organization.",
84+
"Re-authenticate with: sentry auth login"
85+
);
86+
}
8187
throw new ApiError(
8288
error.message,
8389
error.status,

test/lib/api-client.multiregion.test.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ describe("listOrganizationsInRegion", () => {
192192
expect(orgs[0].slug).toBe("us-org-1");
193193
});
194194

195-
test("enriches 403 error with token scope guidance", async () => {
195+
test("enriches 403 error with re-auth guidance for OAuth users", async () => {
196196
globalThis.fetch = async () =>
197197
new Response(JSON.stringify({ detail: "You do not have permission" }), {
198198
status: 403,
@@ -209,9 +209,9 @@ describe("listOrganizationsInRegion", () => {
209209
expect(apiErr.status).toBe(403);
210210
// Should include the original detail
211211
expect(apiErr.detail).toContain("You do not have permission");
212-
// Should include scope guidance
213-
expect(apiErr.detail).toContain("org:read");
212+
// OAuth users: suggest re-auth (not token scopes)
214213
expect(apiErr.detail).toContain("sentry auth login");
214+
expect(apiErr.detail).not.toContain("org:read");
215215
}
216216
});
217217

@@ -556,7 +556,8 @@ describe("listOrganizations (fan-out)", () => {
556556
expect(error).toBeInstanceOf(ApiError);
557557
const apiErr = error as ApiError;
558558
expect(apiErr.status).toBe(403);
559-
expect(apiErr.detail).toContain("org:read");
559+
// OAuth users: re-auth guidance (no scope hint)
560+
expect(apiErr.detail).toContain("sentry auth login");
560561
}
561562
});
562563

0 commit comments

Comments
 (0)