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
2 changes: 1 addition & 1 deletion .env.example
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Copy this to .env.local and fill in your values
SENTRY_CLIENT_ID=your-sentry-oauth-client-id
# SENTRY_CLIENT_ID=your-sentry-oauth-client-id # Only needed for self-hosted or a custom OAuth app
# SENTRY_URL=https://sentry.io # Uncomment for self-hosted

# Test credentials (for running E2E tests)
Expand Down
34 changes: 15 additions & 19 deletions src/lib/oauth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ import { getEnv } from "./env.js";
import {
ApiError,
AuthError,
ConfigError,
DeviceFlowError,
HostScopeError,
} from "./errors.js";
Expand All @@ -43,11 +42,23 @@ function getSentryUrl(): string {
return getConfiguredSentryUrl() ?? DEFAULT_SENTRY_URL;
}

/**
* Public OAuth client ID for sentry.io.
*
* Device Authorization Grant (RFC 8628) is a public-client flow — no client
* secret is involved, so this value is safe to commit. It is equivalent to the
* `SENTRY_CLIENT_ID` repo variable used by CI.
*
* Self-hosted instances must override this via the `SENTRY_CLIENT_ID` env var
* or the `SENTRY_CLIENT_ID_BUILD` build-time define.
*/
const DEFAULT_OAUTH_CLIENT_ID =
"1d673b81d60ef84c951359c36296972ca6fd41bd8f45acd2d3a783a3b3c28e41";

/**
* OAuth client ID
*
* Build-time: Injected via esbuild define: { SENTRY_CLIENT_ID_BUILD: "..." }
* Runtime: Can be overridden via SENTRY_CLIENT_ID env var (for self-hosted)
* Priority: SENTRY_CLIENT_ID env var → SENTRY_CLIENT_ID_BUILD (build-time) → committed default
*
* Read at call time (not module load time) so tests can override SENTRY_CLIENT_ID
* after module initialization.
Expand All @@ -60,7 +71,7 @@ function getClientId(): string {
getEnv().SENTRY_CLIENT_ID ??
(typeof SENTRY_CLIENT_ID_BUILD !== "undefined"
? SENTRY_CLIENT_ID_BUILD
: "")
: DEFAULT_OAUTH_CLIENT_ID)
Comment thread
cursor[bot] marked this conversation as resolved.
);
}

Expand Down Expand Up @@ -162,13 +173,6 @@ function assertRefreshHostTrusted(): void {
/** Request a device code from Sentry's device authorization endpoint */
function requestDeviceCode() {
const clientId = getClientId();
if (!clientId) {
throw new ConfigError(
"SENTRY_CLIENT_ID is required for authentication",
"Set SENTRY_CLIENT_ID environment variable or use a pre-built binary"
);
}

return withHttpSpan("POST", "/oauth/device/code/", async () => {
const response = await fetchWithConnectionError(
`${getSentryUrl()}/oauth/device/code/`,
Expand Down Expand Up @@ -301,7 +305,6 @@ async function attemptPoll(deviceCode: string): Promise<PollResult> {
* @param callbacks - Callbacks for UI updates during the flow
* @param timeout - Maximum time to wait for authorization in ms (default: 10 minutes)
* @returns The token response containing access_token and metadata
* @throws {ConfigError} When SENTRY_CLIENT_ID is not configured
* @throws {ApiError} When unable to connect to Sentry or API returns an error
* @throws {DeviceFlowError} When authorization fails, is denied, or times out
*/
Expand Down Expand Up @@ -393,13 +396,6 @@ export function refreshAccessToken(
refreshToken: string
): Promise<TokenResponse> {
const clientId = getClientId();
if (!clientId) {
throw new ConfigError(
"SENTRY_CLIENT_ID is required for token refresh",
"Set SENTRY_CLIENT_ID environment variable or use a pre-built binary"
);
}

assertRefreshHostTrusted();

return withHttpSpan("POST", "/oauth/token/", async () => {
Expand Down
Loading