Skip to content

Commit cec53e8

Browse files
committed
fix: only mention token scopes in 403 errors for env-var tokens
The regular 'sentry auth login' OAuth flow always grants all required scopes, so suggesting to check token scopes is misleading for those users. Now only shows scope guidance when using a custom env-var token (SENTRY_AUTH_TOKEN / SENTRY_TOKEN). OAuth users see 'Re-authenticate with: sentry auth login' instead. Applied to both issue list (build403Detail) and org listing (listOrganizationsInRegion) 403 handlers.
1 parent 9575ac1 commit cec53e8

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)