Dashboard › cli › Distillation
1edb616f-074f-491d-b2b0-13d92604eae3["lore_tm_v1_iGmQt76DRTljDRO-JUePw2msV0cWGrokhQUzH9x64RI","lore_tm_v1_RtL1_sYaeXXlAqCBlpXiv5t-p360kZ-2xwYf35Ldy2U","lore_tm_v1_2_4qH58AWo2p3tt0INKK9eN6GGNtg4I3vQdOvlt3R08","lore_tm_v1_sb3wOgOUOet__QLGA06mFEKK4ivUbngdOucW07Z0awU","lore_tm_v1_XznGcyNGLiY3xK3KRTY5muKo-NwP3OxT1VRJ9UHRqQY","lore_tm_v1_K9oGkOar_u0TeqCN2vXON40QQhg3LaSOImTXjqY_7-k","lore_tm_v1_XdLSPlNeqwUpHCaeL9x3foSkQYcWoUz3C3MhLiLKXaI","lore_tm_v1_93QdTW8KCL7Xz4gXWTQuTzHdvDpnYHCadptI5w2Cfbw","lore_tm_v1_0IBX51hOlzlk8OPENr41E_ZPMEH9oiGWPWgP3qGpOeM","lore_tm_v1_tdw8FLXj6AHa8glO2DXBOrxZTtldFd3zDzX9Z8Q5Iko","lore_tm_v1_TObcJsae0HbTBALCPXGyYtLOA18Dp4UuTvksGtb5QsE"]
Date: Sep 10, 2026
getSdkConfig() must set throwOnError: false always because errors are handled by the CLI itself.getControlSdkConfig().setEnv(); getEnv() returns process.env in CLI mode, while library mode calls setEnv() with a merged environment copy and never mutates the consumerβs process.env.http://sentry.io is never legitimate for SaaS trust, and a crafted plaintext URL must not inherit SaaS trust..sentryclirc files are never consulted for env-token host snapshots because they have weaker integrity than environment variables or embedded token claims.packages/cli/src/lib/sentry-client.ts: getSdkConfig(regionUrl) strips one trailing /, returns baseUrl without /api/0/, shared getAuthenticatedFetch(), and throwOnError: false as const; SDK endpoint paths already include /api/0/.getApiBaseUrl() and getControlSiloUrl() both resolve getConfiguredSentryUrl() ?? DEFAULT_SENTRY_URL lazily, so URL parsing that sets SENTRY_URL after import is respected; SaaS control silo is sentry.io, while self-hosted control silo is its custom URL.getDefaultSdkConfig() calls getSdkConfig(getApiBaseUrl()); getControlSdkConfig() calls getSdkConfig(getControlSiloUrl()); resetAuthenticatedFetch() clears singleton cachedFetch.packages/cli/src/lib/env.ts initializes _env to process.env; getEnv() returns _env, and setEnv(env) replaces only _env.packages/cli/src/lib/sentry-urls.ts: getSentryBaseUrl() uses getConfiguredSentryUrl() ?? DEFAULT_SENTRY_URL; getOrgBaseUrl(orgSlug) returns the self-hosted base unchanged, but for SaaS changes the hostname to ${orgSlug}.${parsed.hostname}.isSentrySaasUrl(url) is a hostname-only routing/UX predicate accepting sentry.io and any *.sentry.io, including non-production schemes and ports such as http://sentry.io and https://sentry.io:8443; it must not be used for credential trust.isSaaSTrustOrigin(url) is the credential-trust predicate: it requires https:, default port (parsed.port === ""), and isSentrySaasUrl(url).normalizeOrigin(input) returns canonical scheme://host[:port] for string | URL | Request, returns undefined for nullish/unparseable input, and does not accept bare hostnames; normalizeUserInputToOrigin(input) first uses normalizeUrl() to accept bare hostnames by adding https://.packages/cli/src/lib/sentry-urls.ts: buildOrgUrl() uses https://<org>.sentry.io/; project URLs use /settings/projects/<project>/; issue URLs use /issues/<issueId>/; event search uses /issues/?query=event.id:<eventId>; self-hosted equivalents use /organizations/<org>/, /settings/<org>/projects/<project>/, /organizations/<org>/issues/<issueId>/, and /organizations/<org>/issues/?query=event.id:<eventId>.buildOrgSettingsUrl(orgSlug, hash?) uses SaaS /settings/ versus self-hosted /settings/<org>/ and appends #<hash> when provided; buildSeerSettingsUrl() uses /settings/seer/ versus /settings/<org>/seer/; buildBillingUrl(orgSlug, product?) uses /settings/billing/overview/ versus /settings/<org>/billing/overview/ and appends ?product=<product> when supplied.packages/cli/src/lib/env-token-host.ts pins pinnedHost synchronously before post-boot code can alter SENTRY_HOST/SENTRY_URL, specifically before applySentryCliRcEnvShim can write .sentryclirc values.src/cli.ts::preloadProjectContext: 1. captureEnvTokenHost(); 2. findProjectRoot populates .sentryclirc cache; 3. applySentryCliRcEnvShim may write env.SENTRY_URL; 4. getDefaultUrl() fallback may write env.SENTRY_URL.captureEnvTokenHost() is idempotent and resolves in order: normalized parseSntrysClaim(getRawEnvToken())?.url; then normalized trimmed SENTRY_HOST or SENTRY_URL; then DEFAULT_SENTRY_URL. sntrys_ claim URL wins over environment routing to resist layered-CI environment-variable injection; non-sntrys_ tokens rely on env as their only host signal.getEnvTokenHost() auto-captures if needed and falls back to DEFAULT_SENTRY_URL; resetEnvTokenHostForTesting() sets pinnedHost to undefined.packages/cli/src/lib/token-host.ts host equivalence is either exact normalized origin match (scheme, host, and port) or strict SaaS equivalence: a token for https://sentry.io may access valid *.sentry.io origins. Non-SaaS tokens have no suffix/subdomain matching, so sentry.acme.com cannot authorize sentry.acme.evil.com.isHostTrusted(candidate, trusted) returns false for absent/unparseable hosts, true for exact origins, otherwise true only when both origins pass isSaaSTrustOrigin().getActiveTokenHost() mirrors auth precedence: stored OAuth host wins over an env token unless an env token is present and trimmed SENTRY_FORCE_ENV_TOKEN is set; otherwise an env token uses getEnvTokenHost().registerLoginTrustAnchor(url) stores a normalized process-local URL only when supplied through explicit --url/applyLoginUrl or the boot-time env snapshot; the .sentryclirc shim cannot register one. isLoginTrustAnchorFor(host) requires actual host equivalence, preventing a stale library/test-mode anchor for one host from authorizing another.isOriginTrustedFor(requestInput, anchorHost) authorizes either host-equivalence or a dynamically registered isTrustedRegionOrigin(requestOrigin). isRequestOriginTrusted() returns true with no active token, otherwise requires this scope; isHostTrustedForClaim() applies the same logic anchored to a sntrys_ claim URL.isRequestOriginTrustedForCustomHeaders() authorizes SENTRY_CUSTOM_HEADERS only under active-token request trust, or under a matching explicit login trust anchor during the no-token OAuth bootstrap window; without either it fails closed.packages/cli/src/lib/db/auth.ts: getRawEnvToken() reads trimmed SENTRY_AUTH_TOKEN before SENTRY_TOKEN; empty/whitespace values are unset. AuthSource is "env:SENTRY_AUTH_TOKEN" | "env:SENTRY_TOKEN" | "oauth", and getActiveEnvVarName() follows the same priority, defaulting to "SENTRY_AUTH_TOKEN".getAuthConfig() uses forced env-token priority only when SENTRY_FORCE_ENV_TOKEN is set; otherwise stored OAuth takes priority over environment tokens, fixing issue #646 so wizard-generated build tokens do not silently override interactive login.setAuthToken(token, expiresIn?, newRefreshToken?, options?) persists a normalized token host with precedence: explicit options.host; existing auth-row host during refresh; getConfiguredSentryUrl(); then DEFAULT_SENTRY_URL. It stores expires_at/issued_at from expiresIn, and resets identity-fingerprint, auth-token, auth-row, and stored-credentials caches after write.clearAuth() deletes auth, user_info, org_regions, pagination_cursors, and issue-ID-to-org cache; resets auth-related caches; clears in-process trusted-host state; then dynamically imports and attempts clearResponseCache(), treating a missing cache directory as non-fatal.getIdentityFingerprint() memoizes the active identity cache namespace; ANON_IDENTITY = "<anon>". Its precedence is forced env token, stored OAuth (preferring stable refresh token and falling through expired access-only rows), env token, then anonymous; mutation points reset the cache.packages/cli/test/lib/constants.test.ts documents normalizeUrl() unit coverage: undefined, empty, and whitespace inputs return undefined; bare hosts and host-with-port gain https://; existing http:///https:// protocols and protocol casing are preserved; whitespace is trimmed; and paths/trailing slashes are preserved.getCliEnvironment() test contract: no injected version or "0.0.0-dev" returns "development"; versions "0.24.0-dev.1740000000" and "1.0.0-dev.1700000000" return "nightly"; stable "0.20.0", "1.0.0", and "0.23.0" return "production".getConfiguredSentryUrl() test contract: no configured URL returns undefined; SENTRY_HOST takes precedence over SENTRY_URL; bare configured hosts normalize to HTTPS; without explicit host/URL a valid sntrys_ claim URL routes requests (including http://localhost:8000); explicit host configuration overrides the claim for routing.packages/cli/test/lib/security/sntrys-claim-mismatch.test.ts: a sntrys_ token whose embedded claim says https://sentry.firsthost.com must be rejected before fetch when routing targets https://sentry.secondhost.com; no bearer token may reach the wire.sntrys_ claim/request origins attach bearer authorization; without explicit URL configuration, claim URL http://localhost:8000 becomes API base URL and requests target http://localhost:8000/api/0/organizations/.isHostTrusted, which rejected self-hosted regional fan-out. Fix uses isHostTrustedForClaim(), permitting registered regional URLs such as https://us.sentry.acme.com when the claim/control silo is https://sentry.acme.com; bearer authorization attaches successfully.sntrys_ tokens do not undergo embedded-claim validation; existing host-scoping remains their only enforcement.sntrys_ token without SENTRY_HOST/SENTRY_URL pins its claim host; a sntrys_ claim remains authoritative over conflicting SENTRY_HOST; a non-sntrys_ token without host configuration pins https://sentry.io.sntrys_ claim URLs are intentionally captured as written and are not a security primitive: the CLI protects against other routing inputs redirecting credentials away from the tokenβs host, rather than second-guessing a token source the user chose to trust.