(Alleged) Leaked source of Claude Code
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 592 lines 18 kB view raw
1import isEqual from 'lodash-es/isEqual.js' 2import { toError } from '../errors.js' 3import { logError } from '../log.js' 4import { getSettingsForSource } from '../settings/settings.js' 5import { plural } from '../stringUtils.js' 6import { checkGitAvailable } from './gitAvailability.js' 7import { getMarketplace } from './marketplaceManager.js' 8import type { KnownMarketplace, MarketplaceSource } from './schemas.js' 9 10/** 11 * Format plugin failure details for user display 12 * @param failures - Array of failures with names and reasons 13 * @param includeReasons - Whether to include failure reasons (true for full errors, false for summaries) 14 * @returns Formatted string like "plugin-a (reason); plugin-b (reason)" or "plugin-a, plugin-b" 15 */ 16export function formatFailureDetails( 17 failures: Array<{ name: string; reason?: string; error?: string }>, 18 includeReasons: boolean, 19): string { 20 const maxShow = 2 21 const details = failures 22 .slice(0, maxShow) 23 .map(f => { 24 const reason = f.reason || f.error || 'unknown error' 25 return includeReasons ? `${f.name} (${reason})` : f.name 26 }) 27 .join(includeReasons ? '; ' : ', ') 28 29 const remaining = failures.length - maxShow 30 const moreText = remaining > 0 ? ` and ${remaining} more` : '' 31 32 return `${details}${moreText}` 33} 34 35/** 36 * Extract source display string from marketplace configuration 37 */ 38export function getMarketplaceSourceDisplay(source: MarketplaceSource): string { 39 switch (source.source) { 40 case 'github': 41 return source.repo 42 case 'url': 43 return source.url 44 case 'git': 45 return source.url 46 case 'directory': 47 return source.path 48 case 'file': 49 return source.path 50 case 'settings': 51 return `settings:${source.name}` 52 default: 53 return 'Unknown source' 54 } 55} 56 57/** 58 * Create a plugin ID from plugin name and marketplace name 59 */ 60export function createPluginId( 61 pluginName: string, 62 marketplaceName: string, 63): string { 64 return `${pluginName}@${marketplaceName}` 65} 66 67/** 68 * Load marketplaces with graceful degradation for individual failures. 69 * Blocked marketplaces (per enterprise policy) are excluded from the results. 70 */ 71export async function loadMarketplacesWithGracefulDegradation( 72 config: Record<string, KnownMarketplace>, 73): Promise<{ 74 marketplaces: Array<{ 75 name: string 76 config: KnownMarketplace 77 data: Awaited<ReturnType<typeof getMarketplace>> | null 78 }> 79 failures: Array<{ name: string; error: string }> 80}> { 81 const marketplaces: Array<{ 82 name: string 83 config: KnownMarketplace 84 data: Awaited<ReturnType<typeof getMarketplace>> | null 85 }> = [] 86 const failures: Array<{ name: string; error: string }> = [] 87 88 for (const [name, marketplaceConfig] of Object.entries(config)) { 89 // Skip marketplaces blocked by enterprise policy 90 if (!isSourceAllowedByPolicy(marketplaceConfig.source)) { 91 continue 92 } 93 94 let data = null 95 try { 96 data = await getMarketplace(name) 97 } catch (err) { 98 // Track individual marketplace failures but continue loading others 99 const errorMessage = err instanceof Error ? err.message : String(err) 100 failures.push({ name, error: errorMessage }) 101 102 // Log for monitoring 103 logError(toError(err)) 104 } 105 106 marketplaces.push({ 107 name, 108 config: marketplaceConfig, 109 data, 110 }) 111 } 112 113 return { marketplaces, failures } 114} 115 116/** 117 * Format marketplace loading failures into appropriate user messages 118 */ 119export function formatMarketplaceLoadingErrors( 120 failures: Array<{ name: string; error: string }>, 121 successCount: number, 122): { type: 'warning' | 'error'; message: string } | null { 123 if (failures.length === 0) { 124 return null 125 } 126 127 // If some marketplaces succeeded, show warning 128 if (successCount > 0) { 129 const message = 130 failures.length === 1 131 ? `Warning: Failed to load marketplace '${failures[0]!.name}': ${failures[0]!.error}` 132 : `Warning: Failed to load ${failures.length} marketplaces: ${formatFailureNames(failures)}` 133 return { type: 'warning', message } 134 } 135 136 // All marketplaces failed - this is a critical error 137 return { 138 type: 'error', 139 message: `Failed to load all marketplaces. Errors: ${formatFailureErrors(failures)}`, 140 } 141} 142 143function formatFailureNames( 144 failures: Array<{ name: string; error: string }>, 145): string { 146 return failures.map(f => f.name).join(', ') 147} 148 149function formatFailureErrors( 150 failures: Array<{ name: string; error: string }>, 151): string { 152 return failures.map(f => `${f.name}: ${f.error}`).join('; ') 153} 154 155/** 156 * Get the strict marketplace source allowlist from policy settings. 157 * Returns null if no restriction is in place, or an array of allowed sources. 158 */ 159export function getStrictKnownMarketplaces(): MarketplaceSource[] | null { 160 const policySettings = getSettingsForSource('policySettings') 161 if (!policySettings?.strictKnownMarketplaces) { 162 return null // No restrictions 163 } 164 return policySettings.strictKnownMarketplaces 165} 166 167/** 168 * Get the marketplace source blocklist from policy settings. 169 * Returns null if no blocklist is in place, or an array of blocked sources. 170 */ 171export function getBlockedMarketplaces(): MarketplaceSource[] | null { 172 const policySettings = getSettingsForSource('policySettings') 173 if (!policySettings?.blockedMarketplaces) { 174 return null // No blocklist 175 } 176 return policySettings.blockedMarketplaces 177} 178 179/** 180 * Get the custom plugin trust message from policy settings. 181 * Returns undefined if not configured. 182 */ 183export function getPluginTrustMessage(): string | undefined { 184 return getSettingsForSource('policySettings')?.pluginTrustMessage 185} 186 187/** 188 * Compare two MarketplaceSource objects for equality. 189 * Sources are equal if they have the same type and all relevant fields match. 190 */ 191function areSourcesEqual(a: MarketplaceSource, b: MarketplaceSource): boolean { 192 if (a.source !== b.source) return false 193 194 switch (a.source) { 195 case 'url': 196 return a.url === (b as typeof a).url 197 case 'github': 198 return ( 199 a.repo === (b as typeof a).repo && 200 (a.ref || undefined) === ((b as typeof a).ref || undefined) && 201 (a.path || undefined) === ((b as typeof a).path || undefined) 202 ) 203 case 'git': 204 return ( 205 a.url === (b as typeof a).url && 206 (a.ref || undefined) === ((b as typeof a).ref || undefined) && 207 (a.path || undefined) === ((b as typeof a).path || undefined) 208 ) 209 case 'npm': 210 return a.package === (b as typeof a).package 211 case 'file': 212 return a.path === (b as typeof a).path 213 case 'directory': 214 return a.path === (b as typeof a).path 215 case 'settings': 216 return ( 217 a.name === (b as typeof a).name && 218 isEqual(a.plugins, (b as typeof a).plugins) 219 ) 220 default: 221 return false 222 } 223} 224 225/** 226 * Extract the host/domain from a marketplace source. 227 * Used for hostPattern matching in strictKnownMarketplaces. 228 * 229 * Currently only supports github, git, and url sources. 230 * npm, file, and directory sources are not supported for hostPattern matching. 231 * 232 * @param source - The marketplace source to extract host from 233 * @returns The hostname string, or null if extraction fails or source type not supported 234 */ 235export function extractHostFromSource( 236 source: MarketplaceSource, 237): string | null { 238 switch (source.source) { 239 case 'github': 240 // GitHub shorthand always means github.com 241 return 'github.com' 242 243 case 'git': { 244 // SSH format: user@HOST:path (e.g., git@github.com:owner/repo.git) 245 const sshMatch = source.url.match(/^[^@]+@([^:]+):/) 246 if (sshMatch?.[1]) { 247 return sshMatch[1] 248 } 249 // HTTPS format: extract hostname from URL 250 try { 251 return new URL(source.url).hostname 252 } catch { 253 return null 254 } 255 } 256 257 case 'url': 258 try { 259 return new URL(source.url).hostname 260 } catch { 261 return null 262 } 263 264 // npm, file, directory, hostPattern, pathPattern sources are not supported for hostPattern matching 265 default: 266 return null 267 } 268} 269 270/** 271 * Check if a source matches a hostPattern entry. 272 * Extracts the host from the source and tests it against the regex pattern. 273 * 274 * @param source - The marketplace source to check 275 * @param pattern - The hostPattern entry from strictKnownMarketplaces 276 * @returns true if the source's host matches the pattern 277 */ 278function doesSourceMatchHostPattern( 279 source: MarketplaceSource, 280 pattern: MarketplaceSource & { source: 'hostPattern' }, 281): boolean { 282 const host = extractHostFromSource(source) 283 if (!host) { 284 return false 285 } 286 287 try { 288 const regex = new RegExp(pattern.hostPattern) 289 return regex.test(host) 290 } catch { 291 // Invalid regex - log and return false 292 logError(new Error(`Invalid hostPattern regex: ${pattern.hostPattern}`)) 293 return false 294 } 295} 296 297/** 298 * Check if a source matches a pathPattern entry. 299 * Tests the source's .path (file and directory sources only) against the regex pattern. 300 * 301 * @param source - The marketplace source to check 302 * @param pattern - The pathPattern entry from strictKnownMarketplaces 303 * @returns true if the source's path matches the pattern 304 */ 305function doesSourceMatchPathPattern( 306 source: MarketplaceSource, 307 pattern: MarketplaceSource & { source: 'pathPattern' }, 308): boolean { 309 // Only file and directory sources have a .path to match against 310 if (source.source !== 'file' && source.source !== 'directory') { 311 return false 312 } 313 314 try { 315 const regex = new RegExp(pattern.pathPattern) 316 return regex.test(source.path) 317 } catch { 318 logError(new Error(`Invalid pathPattern regex: ${pattern.pathPattern}`)) 319 return false 320 } 321} 322 323/** 324 * Get hosts from hostPattern entries in the allowlist. 325 * Used to provide helpful error messages. 326 */ 327export function getHostPatternsFromAllowlist(): string[] { 328 const allowlist = getStrictKnownMarketplaces() 329 if (!allowlist) return [] 330 331 return allowlist 332 .filter( 333 (entry): entry is MarketplaceSource & { source: 'hostPattern' } => 334 entry.source === 'hostPattern', 335 ) 336 .map(entry => entry.hostPattern) 337} 338 339/** 340 * Extract GitHub owner/repo from a git URL if it's a GitHub URL. 341 * Returns null if not a GitHub URL. 342 * 343 * Handles: 344 * - git@github.com:owner/repo.git 345 * - https://github.com/owner/repo.git 346 * - https://github.com/owner/repo 347 */ 348function extractGitHubRepoFromGitUrl(url: string): string | null { 349 // SSH format: git@github.com:owner/repo.git 350 const sshMatch = url.match(/^git@github\.com:([^/]+\/[^/]+?)(?:\.git)?$/) 351 if (sshMatch && sshMatch[1]) { 352 return sshMatch[1] 353 } 354 355 // HTTPS format: https://github.com/owner/repo.git or https://github.com/owner/repo 356 const httpsMatch = url.match( 357 /^https?:\/\/github\.com\/([^/]+\/[^/]+?)(?:\.git)?$/, 358 ) 359 if (httpsMatch && httpsMatch[1]) { 360 return httpsMatch[1] 361 } 362 363 return null 364} 365 366/** 367 * Check if a blocked ref/path constraint matches a source. 368 * If the blocklist entry has no ref/path, it matches ALL refs/paths (wildcard). 369 * If the blocklist entry has a specific ref/path, it only matches that exact value. 370 */ 371function blockedConstraintMatches( 372 blockedValue: string | undefined, 373 sourceValue: string | undefined, 374): boolean { 375 // If blocklist doesn't specify a constraint, it's a wildcard - matches anything 376 if (!blockedValue) { 377 return true 378 } 379 // If blocklist specifies a constraint, source must match exactly 380 return (blockedValue || undefined) === (sourceValue || undefined) 381} 382 383/** 384 * Check if two sources refer to the same GitHub repository, even if using 385 * different source types (github vs git with GitHub URL). 386 * 387 * Blocklist matching is asymmetric: 388 * - If blocklist entry has no ref/path, it blocks ALL refs/paths (wildcard) 389 * - If blocklist entry has a specific ref/path, only that exact value is blocked 390 */ 391function areSourcesEquivalentForBlocklist( 392 source: MarketplaceSource, 393 blocked: MarketplaceSource, 394): boolean { 395 // Check exact same source type 396 if (source.source === blocked.source) { 397 switch (source.source) { 398 case 'github': { 399 const b = blocked as typeof source 400 if (source.repo !== b.repo) return false 401 return ( 402 blockedConstraintMatches(b.ref, source.ref) && 403 blockedConstraintMatches(b.path, source.path) 404 ) 405 } 406 case 'git': { 407 const b = blocked as typeof source 408 if (source.url !== b.url) return false 409 return ( 410 blockedConstraintMatches(b.ref, source.ref) && 411 blockedConstraintMatches(b.path, source.path) 412 ) 413 } 414 case 'url': 415 return source.url === (blocked as typeof source).url 416 case 'npm': 417 return source.package === (blocked as typeof source).package 418 case 'file': 419 return source.path === (blocked as typeof source).path 420 case 'directory': 421 return source.path === (blocked as typeof source).path 422 case 'settings': 423 return source.name === (blocked as typeof source).name 424 default: 425 return false 426 } 427 } 428 429 // Check if a git source matches a github blocklist entry 430 if (source.source === 'git' && blocked.source === 'github') { 431 const extractedRepo = extractGitHubRepoFromGitUrl(source.url) 432 if (extractedRepo === blocked.repo) { 433 return ( 434 blockedConstraintMatches(blocked.ref, source.ref) && 435 blockedConstraintMatches(blocked.path, source.path) 436 ) 437 } 438 } 439 440 // Check if a github source matches a git blocklist entry (GitHub URL) 441 if (source.source === 'github' && blocked.source === 'git') { 442 const extractedRepo = extractGitHubRepoFromGitUrl(blocked.url) 443 if (extractedRepo === source.repo) { 444 return ( 445 blockedConstraintMatches(blocked.ref, source.ref) && 446 blockedConstraintMatches(blocked.path, source.path) 447 ) 448 } 449 } 450 451 return false 452} 453 454/** 455 * Check if a marketplace source is explicitly in the blocklist. 456 * Used for error message differentiation. 457 * 458 * This also catches attempts to bypass a github blocklist entry by using 459 * git URLs (e.g., git@github.com:owner/repo.git or https://github.com/owner/repo.git). 460 */ 461export function isSourceInBlocklist(source: MarketplaceSource): boolean { 462 const blocklist = getBlockedMarketplaces() 463 if (blocklist === null) { 464 return false 465 } 466 return blocklist.some(blocked => 467 areSourcesEquivalentForBlocklist(source, blocked), 468 ) 469} 470 471/** 472 * Check if a marketplace source is allowed by enterprise policy. 473 * Returns true if allowed (or no policy), false if blocked. 474 * This check happens BEFORE downloading, so blocked sources never touch the filesystem. 475 * 476 * Policy precedence: 477 * 1. blockedMarketplaces (blocklist) - if source matches, it's blocked 478 * 2. strictKnownMarketplaces (allowlist) - if set, source must be in the list 479 */ 480export function isSourceAllowedByPolicy(source: MarketplaceSource): boolean { 481 // Check blocklist first (takes precedence) 482 if (isSourceInBlocklist(source)) { 483 return false 484 } 485 486 // Then check allowlist 487 const allowlist = getStrictKnownMarketplaces() 488 if (allowlist === null) { 489 return true // No restrictions 490 } 491 492 // Check each entry in the allowlist 493 return allowlist.some(allowed => { 494 // Handle hostPattern entries - match by extracted host 495 if (allowed.source === 'hostPattern') { 496 return doesSourceMatchHostPattern(source, allowed) 497 } 498 // Handle pathPattern entries - match file/directory .path by regex 499 if (allowed.source === 'pathPattern') { 500 return doesSourceMatchPathPattern(source, allowed) 501 } 502 // Handle regular source entries - exact match 503 return areSourcesEqual(source, allowed) 504 }) 505} 506 507/** 508 * Format a MarketplaceSource for display in error messages 509 */ 510export function formatSourceForDisplay(source: MarketplaceSource): string { 511 switch (source.source) { 512 case 'github': 513 return `github:${source.repo}${source.ref ? `@${source.ref}` : ''}` 514 case 'url': 515 return source.url 516 case 'git': 517 return `git:${source.url}${source.ref ? `@${source.ref}` : ''}` 518 case 'npm': 519 return `npm:${source.package}` 520 case 'file': 521 return `file:${source.path}` 522 case 'directory': 523 return `dir:${source.path}` 524 case 'hostPattern': 525 return `hostPattern:${source.hostPattern}` 526 case 'pathPattern': 527 return `pathPattern:${source.pathPattern}` 528 case 'settings': 529 return `settings:${source.name} (${source.plugins.length} ${plural(source.plugins.length, 'plugin')})` 530 default: 531 return 'unknown source' 532 } 533} 534 535/** 536 * Reasons why no marketplaces are available in the Discover screen 537 */ 538export type EmptyMarketplaceReason = 539 | 'git-not-installed' 540 | 'all-blocked-by-policy' 541 | 'policy-restricts-sources' 542 | 'all-marketplaces-failed' 543 | 'no-marketplaces-configured' 544 | 'all-plugins-installed' 545 546/** 547 * Detect why no marketplaces are available. 548 * Checks in order of priority: git availability → policy restrictions → config state → failures 549 */ 550export async function detectEmptyMarketplaceReason({ 551 configuredMarketplaceCount, 552 failedMarketplaceCount, 553}: { 554 configuredMarketplaceCount: number 555 failedMarketplaceCount: number 556}): Promise<EmptyMarketplaceReason> { 557 // Check if git is installed (required for most marketplace sources) 558 const gitAvailable = await checkGitAvailable() 559 if (!gitAvailable) { 560 return 'git-not-installed' 561 } 562 563 // Check policy restrictions 564 const allowlist = getStrictKnownMarketplaces() 565 if (allowlist !== null) { 566 if (allowlist.length === 0) { 567 // Policy explicitly blocks all marketplaces 568 return 'all-blocked-by-policy' 569 } 570 // Policy restricts which sources can be used 571 if (configuredMarketplaceCount === 0) { 572 return 'policy-restricts-sources' 573 } 574 } 575 576 // Check if any marketplaces are configured 577 if (configuredMarketplaceCount === 0) { 578 return 'no-marketplaces-configured' 579 } 580 581 // Check if all configured marketplaces failed to load 582 if ( 583 failedMarketplaceCount > 0 && 584 failedMarketplaceCount === configuredMarketplaceCount 585 ) { 586 return 'all-marketplaces-failed' 587 } 588 589 // Marketplaces are configured and loaded, but no plugins available 590 // This typically means all plugins are already installed 591 return 'all-plugins-installed' 592}