diff --git a/apps/cli-docs/src/content/docs/library-usage.md b/apps/cli-docs/src/content/docs/library-usage.md index 18e685796..a866e4dd5 100644 --- a/apps/cli-docs/src/content/docs/library-usage.md +++ b/apps/cli-docs/src/content/docs/library-usage.md @@ -119,12 +119,14 @@ const help = await sdk.run("help", "issue"); ## Authentication The `token` option provides an auth token for the current invocation. When -omitted, it falls back to environment variables and stored credentials: +omitted, it falls back to stored credentials and environment variables: 1. `token` option (highest priority) -2. `SENTRY_AUTH_TOKEN` environment variable -3. `SENTRY_TOKEN` environment variable -4. Stored OAuth token from `sentry auth login` +2. Stored OAuth token from `sentry auth login` (if not expired) +3. `SENTRY_AUTH_TOKEN` environment variable +4. `SENTRY_TOKEN` environment variable + +Set `SENTRY_FORCE_ENV_TOKEN=1` to make environment variable tokens take priority over stored OAuth. ```typescript // Explicit token diff --git a/packages/cli/src/lib/sdk-invoke.ts b/packages/cli/src/lib/sdk-invoke.ts index 3991f22aa..a4755276b 100644 --- a/packages/cli/src/lib/sdk-invoke.ts +++ b/packages/cli/src/lib/sdk-invoke.ts @@ -40,6 +40,7 @@ function buildIsolatedEnv( const env: NodeJS.ProcessEnv = { ...process.env }; if (options?.token) { env.SENTRY_AUTH_TOKEN = options.token; + env.SENTRY_FORCE_ENV_TOKEN = "1"; } if (options?.url) { env.SENTRY_HOST = options.url; diff --git a/packages/cli/test/lib/index.test.ts b/packages/cli/test/lib/index.test.ts index 312e09400..62dd8ceeb 100644 --- a/packages/cli/test/lib/index.test.ts +++ b/packages/cli/test/lib/index.test.ts @@ -1,6 +1,9 @@ import { afterEach, beforeEach, describe, expect, test } from "vitest"; import createSentrySDK, { SentryError } from "../../src/index.js"; -import { mockFetch } from "../helpers.js"; +import { setAuthToken } from "../../src/lib/db/auth.js"; +import { mockFetch, useTestConfigDir } from "../helpers.js"; + +useTestConfigDir("sdk-library-"); describe("createSentrySDK() library API", () => { // Silence unmocked fetch calls from resolution cascade. @@ -8,13 +11,17 @@ describe("createSentrySDK() library API", () => { // the org/project resolution cascade which hits real API endpoints. // A silent 404 prevents preload warnings while preserving error behavior. let originalFetch: typeof globalThis.fetch; + let authorizationHeaders: string[]; beforeEach(() => { originalFetch = globalThis.fetch; + authorizationHeaders = []; // Return empty successes rather than 404s so the resolution cascade // terminates cleanly without triggering follow-up requests that could // outlive the test and spill into later test files. - globalThis.fetch = mockFetch(async (input) => { + globalThis.fetch = mockFetch(async (input, init) => { + const request = new Request(input, init); + authorizationHeaders.push(request.headers.get("Authorization") ?? ""); let url: string; if (typeof input === "string") { url = input; @@ -26,7 +33,7 @@ describe("createSentrySDK() library API", () => { if (url.includes("/regions/")) { return new Response(JSON.stringify({ regions: [] }), { status: 200 }); } - if (url.includes("/organizations/")) { + if (url.includes("/organizations")) { return new Response(JSON.stringify([]), { status: 200 }); } // Return empty 200 for all other endpoints (projects, issues, etc.) @@ -133,6 +140,16 @@ describe("createSentrySDK() library API", () => { } }); + test("explicit token overrides stored OAuth credentials", async () => { + setAuthToken("stored-oauth-token", 3600); + const sdk = createSentrySDK({ token: "explicit-sdk-token" }); + + await sdk.auth.status(); + + expect(authorizationHeaders).toContain("Bearer explicit-sdk-token"); + expect(authorizationHeaders).not.toContain("Bearer stored-oauth-token"); + }); + test("sdk.run returns AsyncIterable for streaming flag --follow", () => { const sdk = createSentrySDK(); const result = sdk.run("log", "list", "--follow");