Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 | 205x 205x 205x 11x 3x 8x 8x 8x 8x 8x 1173x 1476x 1476x 1476x 11x 11x 429x 429x 26x 26x 205x 294x 294x 294x 294x 13x 13x 13x 5x 8x 926x 926x 926x 926x 254x 24x 230x 230x 230x 230x 528x 528x 77x 528x 96x 528x 528x 528x 160x 160x 160x 260x 260x 316x 205x 158x 158x 158x 158x 158x 3x 3x 3x 3x | /**
* Organization region cache for multi-region support.
*
* Sentry has multiple regions (US, EU, etc.) and organizations are bound
* to a specific region. This module caches the organization-to-region
* mapping to avoid repeated lookups.
*
* The `org_id` column (added in schema v8) enables offline resolution
* of numeric org IDs extracted from DSN hosts (e.g., `o1081365` →
* look up by `org_id = '1081365'` → get the slug).
*/
import { normalizeOrigin } from "../sentry-urls.js";
import { recordCacheHit } from "../telemetry.js";
import { getDatabase } from "./index.js";
import { runUpsert } from "./utils.js";
const TABLE = "org_regions";
/**
* Process-local trust extension: origins that were vouched for by the
* active token's issuing host (via `/users/me/regions/` responses or
* `org_regions` table entries from prior invocations). Used by the
* fetch-layer trust check in `token-host.ts` to admit requests to
* regional silos that share the token's trust class.
*
* Lazy-seeded from the persisted table on first read so that on cold
* start (with cached orgs from a previous CLI invocation) we don't
* re-fetch regions just to extend trust.
*/
const trustedRegionOrigins = new Set<string>();
let trustedRegionOriginsSeeded = false;
function seedTrustedRegionOriginsIfNeeded(): void {
if (trustedRegionOriginsSeeded) {
return;
}
trustedRegionOriginsSeeded = true;
try {
const db = getDatabase();
const rows = db
.query(`SELECT DISTINCT region_url FROM ${TABLE}`)
.all() as Pick<OrgRegionRow, "region_url">[];
for (const row of rows) {
const origin = normalizeOrigin(row.region_url);
if (origin) {
trustedRegionOrigins.add(origin);
}
}
} catch {
// No DB / no table yet — first-run callers will populate via
// setOrgRegion(s) or registerTrustedRegionUrls.
}
}
/**
* Register region URLs the control silo just told us about. Used to
* extend the trust scope BEFORE region URLs are persisted (the fan-out
* step needs trust to be admitted before we have results to write).
*
* Also called automatically by {@link setOrgRegion} and
* {@link setOrgRegions} so persistent and in-process state stay in sync.
*/
export function registerTrustedRegionUrls(urls: readonly string[]): void {
for (const url of urls) {
const origin = normalizeOrigin(url);
Eif (origin) {
trustedRegionOrigins.add(origin);
}
}
}
/**
* Whether `origin` was vouched for by the active token's issuing host.
* Lazy-seeds from `org_regions` on first call.
*/
export function isTrustedRegionOrigin(origin: string): boolean {
seedTrustedRegionOriginsIfNeeded();
return trustedRegionOrigins.has(origin);
}
/**
* Clear the in-process trust extension. Called from `clearAuth()` to
* evict region extensions tied to the now-cleared identity.
*
* Does NOT clear the login trust anchor in `token-host.ts` — that
* represents the current `auth login` command's intent and is needed
* after clearAuth runs during re-auth.
*/
export function clearTrustedHostState(): void {
trustedRegionOrigins.clear();
// Force re-seed on next read in case the caller deletes table rows
// (clearOrgRegions) between this clear and the next access.
trustedRegionOriginsSeeded = false;
}
/** @internal exported for testing */
export function resetTrustedRegionUrlsForTesting(): void {
trustedRegionOrigins.clear();
trustedRegionOriginsSeeded = false;
}
/** When true, getCachedOrganizations() returns empty (forces API fetch). */
let orgCacheDisabled = false;
/** Disable the org listing cache for this invocation (e.g., `--fresh` flag). */
export function disableOrgCache(): void {
orgCacheDisabled = true;
}
/** Re-enable the org listing cache. Exported for testing. */
export function enableOrgCache(): void {
orgCacheDisabled = false;
}
type OrgRegionRow = {
org_slug: string;
org_id: string | null;
org_name: string | null;
org_role: string | null;
region_url: string;
updated_at: number;
};
/** Entry for batch-caching org regions with optional metadata. */
export type OrgRegionEntry = {
slug: string;
regionUrl: string;
orgId?: string;
orgName?: string;
/** The authenticated user's role in this organization (e.g., "member", "admin", "owner"). */
orgRole?: string;
};
/**
* Get the cached region URL for an organization.
*
* @param orgSlug - The organization slug
* @returns The region URL if cached, undefined otherwise
*/
export function getOrgRegion(orgSlug: string): string | undefined {
const db = getDatabase();
const row = db
.query(`SELECT region_url FROM ${TABLE} WHERE org_slug = ?`)
.get(orgSlug) as Pick<OrgRegionRow, "region_url"> | undefined;
recordCacheHit("region", !!row);
return row?.region_url;
}
/**
* Look up an organization slug by its numeric ID.
*
* Used to resolve DSN-style org identifiers (e.g., `o1081365` → strip
* prefix → look up `1081365` → get the slug `my-org`).
*
* @param numericId - The bare numeric org ID (without "o" prefix)
* @returns The org slug and region URL if found, undefined otherwise
*/
export function getOrgByNumericId(
numericId: string
): { slug: string; regionUrl: string } | undefined {
const db = getDatabase();
const row = db
.query(`SELECT org_slug, region_url FROM ${TABLE} WHERE org_id = ?`)
.get(numericId) as
| Pick<OrgRegionRow, "org_slug" | "region_url">
| undefined;
if (!row) {
return;
}
return { slug: row.org_slug, regionUrl: row.region_url };
}
/**
* Cache the region URL for an organization.
*
* @param orgSlug - The organization slug
* @param regionUrl - The region URL (e.g., https://us.sentry.io)
*/
export function setOrgRegion(orgSlug: string, regionUrl: string): void {
const db = getDatabase();
const now = Date.now();
runUpsert(
db,
TABLE,
{ org_slug: orgSlug, region_url: regionUrl, updated_at: now },
["org_slug"]
);
registerTrustedRegionUrls([regionUrl]);
}
/**
* Cache region URLs for multiple organizations in a single transaction.
* More efficient than calling setOrgRegion() multiple times.
*
* Each entry includes the org slug, region URL, and optionally the
* numeric org ID for offline ID→slug lookups.
*
* @param entries - Array of org region entries
*/
export function setOrgRegions(entries: OrgRegionEntry[]): void {
if (entries.length === 0) {
return;
}
const db = getDatabase();
const now = Date.now();
db.transaction(() => {
for (const entry of entries) {
const row: Record<string, string | number | null> = {
org_slug: entry.slug,
region_url: entry.regionUrl,
updated_at: now,
};
if (entry.orgId) {
row.org_id = entry.orgId;
}
if (entry.orgName) {
row.org_name = entry.orgName;
}
Iif (entry.orgRole) {
row.org_role = entry.orgRole;
}
runUpsert(db, TABLE, row, ["org_slug"]);
}
})();
registerTrustedRegionUrls(entries.map((e) => e.regionUrl));
}
/**
* Clear all cached organization regions.
* Should be called when the user logs out.
*/
export function clearOrgRegions(): void {
const db = getDatabase();
db.query(`DELETE FROM ${TABLE}`).run();
clearTrustedHostState();
}
/**
* Get all cached organization regions.
* Used for determining if user has orgs in multiple regions.
*
* @returns Map of org slug to region URL
*/
export function getAllOrgRegions(): Map<string, string> {
const db = getDatabase();
const rows = db
.query(`SELECT org_slug, region_url FROM ${TABLE}`)
.all() as Pick<OrgRegionRow, "org_slug" | "region_url">[];
return new Map(rows.map((row) => [row.org_slug, row.region_url]));
}
/** Cached org entry with the fields needed to reconstruct a SentryOrganization. */
export type CachedOrg = {
slug: string;
id: string;
name: string;
/** The authenticated user's role in this organization, if available. */
orgRole?: string;
};
/**
* Maximum age (ms) for cached organization entries.
* Entries older than this are considered stale and ignored, forcing a
* fresh API fetch. 7 days balances offline usability with picking up
* new org memberships.
*/
const ORG_CACHE_TTL_MS = 7 * 24 * 60 * 60 * 1000;
/**
* Get all cached organizations with id, slug, and name.
*
* Returns organizations that have all three fields populated and were
* updated within the TTL window. Rows with missing `org_id` or `org_name`
* (from before schema v9) or stale `updated_at` are excluded — callers
* should fall back to the API when the result is empty.
*
* Returns empty when the cache is disabled via {@link disableOrgCache}
* (e.g., `--fresh` flag).
*
* @returns Array of cached org entries, or empty if cache is cold/stale/disabled/incomplete
*/
export function getCachedOrganizations(): CachedOrg[] {
Iif (orgCacheDisabled) {
return [];
}
const db = getDatabase();
const cutoff = Date.now() - ORG_CACHE_TTL_MS;
const rows = db
.query(
`SELECT org_slug, org_id, org_name, org_role FROM ${TABLE} WHERE org_id IS NOT NULL AND org_name IS NOT NULL AND updated_at > ?`
)
.all(cutoff) as Pick<
OrgRegionRow,
"org_slug" | "org_id" | "org_name" | "org_role"
>[];
return rows.map((row) => ({
slug: row.org_slug,
// org_id and org_name are guaranteed non-null by the WHERE clause
id: row.org_id as string,
name: row.org_name as string,
...(row.org_role ? { orgRole: row.org_role } : {}),
}));
}
/**
* Get the cached org role for a single organization.
*
* Returns the user's role from the org cache without an API call.
* The role is populated when `listOrganizations()` fetches from the API.
*
* @param orgSlug - The organization slug
* @returns The user's role (e.g., "member", "admin", "owner"), or undefined if not cached
*/
export function getCachedOrgRole(orgSlug: string): string | undefined {
const db = getDatabase();
const cutoff = Date.now() - ORG_CACHE_TTL_MS;
const row = db
.query(
`SELECT org_role FROM ${TABLE} WHERE org_slug = ? AND org_role IS NOT NULL AND updated_at > ?`
)
.get(orgSlug, cutoff) as Pick<OrgRegionRow, "org_role"> | undefined;
return row?.org_role ?? undefined;
}
|