diff --git a/bun.lock b/bun.lock index cd0aa169b..776480324 100644 --- a/bun.lock +++ b/bun.lock @@ -129,6 +129,7 @@ "lodash": "*", "micromatch": "^4.0.8", "nanoid": "5.0.7", + "node-machine-id": "^1.1.12", "onetime": "5.1.2", "picocolors": "1.1.0", "pino": "9.4.0", @@ -1653,7 +1654,7 @@ "devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="], - "devtools-protocol": ["devtools-protocol@0.0.1452169", "", {}, "sha512-FOFDVMGrAUNp0dDKsAU1TorWJUx2JOU1k9xdgBKKJF3IBh/Uhl2yswG5r3TEAOrCiGY2QRp1e6LVDQrCsTKO4g=="], + "devtools-protocol": ["devtools-protocol@0.0.1464554", "", {}, "sha512-CAoP3lYfwAGQTaAXYvA6JZR0fjGUb7qec1qf4mToyoH2TZgUFeIqYcjh6f9jNuhHfuZiEdH+PONHYrLhRQX6aw=="], "didyoumean": ["didyoumean@1.2.2", "", {}, "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw=="], @@ -2865,7 +2866,7 @@ "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], - "puppeteer-core": ["puppeteer-core@24.10.1", "", { "dependencies": { "@puppeteer/browsers": "2.10.5", "chromium-bidi": "5.1.0", "debug": "^4.4.1", "devtools-protocol": "0.0.1452169", "typed-query-selector": "^2.12.0", "ws": "^8.18.2" } }, "sha512-AE6doA9znmEEps/pC5lc9p0zejCdNLR6UBp3EZ49/15Nbvh+uklXxGox7Qh8/lFGqGVwxInl0TXmsOmIuIMwiQ=="], + "puppeteer-core": ["puppeteer-core@24.12.0", "", { "dependencies": { "@puppeteer/browsers": "2.10.5", "chromium-bidi": "5.1.0", "debug": "^4.4.1", "devtools-protocol": "0.0.1464554", "typed-query-selector": "^2.12.0", "ws": "^8.18.3" } }, "sha512-VrPXPho5Q90Ao86FwJVb+JeAF2Tf41wOTGg8k2SyQJePiJ6hJ5iujYpmP+bmhlb6o+J26bQYRDPOYXP7ALWcxQ=="], "pure-rand": ["pure-rand@6.1.0", "", {}, "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA=="], @@ -4061,7 +4062,7 @@ "puppeteer-core/debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], - "puppeteer-core/ws": ["ws@8.18.2", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ=="], + "puppeteer-core/ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="], "react-devtools-core/ws": ["ws@7.5.10", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ=="], diff --git a/npm-app/package.json b/npm-app/package.json index 403e47293..f64abb63d 100644 --- a/npm-app/package.json +++ b/npm-app/package.json @@ -55,6 +55,7 @@ "lodash": "*", "micromatch": "^4.0.8", "nanoid": "5.0.7", + "node-machine-id": "^1.1.12", "onetime": "5.1.2", "picocolors": "1.1.0", "pino": "9.4.0", diff --git a/npm-app/src/__tests__/fingerprint-integration.test.ts b/npm-app/src/__tests__/fingerprint-integration.test.ts new file mode 100644 index 000000000..0ab22a17a --- /dev/null +++ b/npm-app/src/__tests__/fingerprint-integration.test.ts @@ -0,0 +1,63 @@ +import { calculateFingerprint } from '../fingerprint' + +describe('Fingerprint Integration Test', () => { + it('should generate fingerprints and test both enhanced CLI and legacy modes', async () => { + console.log('šŸ” Testing enhanced CLI fingerprinting implementation...') + + // Test multiple fingerprint generations + const results = [] + for (let i = 0; i < 3; i++) { + const start = Date.now() + const fingerprint = await calculateFingerprint() + const duration = Date.now() - start + + results.push({ + fingerprint, + duration, + isEnhanced: fingerprint.startsWith('enhanced-') || fingerprint.startsWith('fp-'), + isLegacy: fingerprint.startsWith('legacy-') + }) + + console.log(`Attempt ${i + 1}: ${fingerprint} (${duration}ms)`) + } + + // Verify all fingerprints are valid + results.forEach((result, index) => { + expect(result.fingerprint).toBeDefined() + expect(typeof result.fingerprint).toBe('string') + expect(result.fingerprint.length).toBeGreaterThan(20) + expect(result.isEnhanced || result.isLegacy).toBe(true) + }) + + // Check uniqueness patterns + // Enhanced fingerprints should be deterministic (same each time) + // Legacy fingerprints should be unique (due to random suffix) + const enhancedResults = results.filter(r => r.isEnhanced) + const legacyResults = results.filter(r => r.isLegacy) + + if (enhancedResults.length > 1) { + // Enhanced fingerprints should be identical (deterministic) + const uniqueEnhanced = new Set(enhancedResults.map(r => r.fingerprint)) + expect(uniqueEnhanced.size).toBe(1) + } + + if (legacyResults.length > 1) { + // Legacy fingerprints should be unique (random suffix) + const uniqueLegacy = new Set(legacyResults.map(r => r.fingerprint)) + expect(uniqueLegacy.size).toBe(legacyResults.length) + } + + // Log summary + const enhancedCount = results.filter(r => r.isEnhanced).length + const legacyCount = results.filter(r => r.isLegacy).length + const avgDuration = results.reduce((sum, r) => sum + r.duration, 0) / results.length + + console.log(`\nšŸ“Š Results Summary:`) + console.log(` Enhanced: ${enhancedCount}/${results.length}`) + console.log(` Legacy: ${legacyCount}/${results.length}`) + console.log(` Avg Duration: ${avgDuration.toFixed(0)}ms`) + + // At least one should succeed + expect(results.length).toBeGreaterThan(0) + }, 10000) // 10 second timeout for CLI operations +}) diff --git a/npm-app/src/browser-runner.ts b/npm-app/src/browser-runner.ts index 72d1d3fa2..c32bdf81a 100644 --- a/npm-app/src/browser-runner.ts +++ b/npm-app/src/browser-runner.ts @@ -20,6 +20,18 @@ import { logger } from './utils/logger' type NonOptional = T & { [P in K]-?: T[P] } +// Define helper to find Chrome in standard locations +export const findChrome = () => { + switch (process.platform) { + case 'win32': + return 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe' + case 'darwin': + return '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome' + default: + return '/usr/bin/google-chrome' + } +} + export class BrowserRunner { // Add getter methods for diagnostic loop getLogs(): BrowserResponse['logs'] { @@ -328,18 +340,6 @@ export class BrowserRunner { } catch (error) {} try { - // Define helper to find Chrome in standard locations - const findChrome = () => { - switch (process.platform) { - case 'win32': - return 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe' - case 'darwin': - return '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome' - default: - return '/usr/bin/google-chrome' - } - } - this.browser = await puppeteer.launch({ defaultViewport: { width: BROWSER_DEFAULTS.viewportWidth, diff --git a/npm-app/src/client.ts b/npm-app/src/client.ts index 1a45a8fe1..d5047ec53 100644 --- a/npm-app/src/client.ts +++ b/npm-app/src/client.ts @@ -81,7 +81,7 @@ import { import { logAndHandleStartup } from './startup-process-handler' import { handleToolCall } from './tool-handlers' import { GitCommand, MakeNullable } from './types' -import { identifyUser, trackEvent } from './utils/analytics' +import { identifyUser, identifyUserWithFingerprint, trackEvent } from './utils/analytics' import { getRepoMetrics, gitCommandIsAvailable } from './utils/git' import { logger, loggerContext } from './utils/logger' import { Spinner } from './utils/spinner' @@ -289,7 +289,8 @@ export class Client { const credentialsFile = readFileSync(CREDENTIALS_PATH, 'utf8') const user = userFromJson(credentialsFile) if (user) { - identifyUser(user.id, { + // Use enhanced fingerprint identification for better analytics + identifyUserWithFingerprint(user.id, { email: user.email, name: user.name, fingerprintId: this.fingerprintId, @@ -593,7 +594,8 @@ export class Client { shouldRequestLogin = false this.user = user - identifyUser(user.id, { + // Use enhanced fingerprint identification for login + identifyUserWithFingerprint(user.id, { email: user.email, name: user.name, fingerprintId: fingerprintId, diff --git a/npm-app/src/fingerprint.ts b/npm-app/src/fingerprint.ts index cba7c4e96..748b3b5e8 100644 --- a/npm-app/src/fingerprint.ts +++ b/npm-app/src/fingerprint.ts @@ -1,59 +1,162 @@ +// Enhanced fingerprinting with CLI-only approach and backward compatibility // Modified from: https://github.com/andsmedeiros/hw-fingerprint import { createHash, randomBytes } from 'node:crypto' -import { EOL, endianness } from 'node:os' +import { networkInterfaces } from 'node:os' import { - system, bios, - baseboard, cpu, + graphics, + mem, osInfo, + system, // @ts-ignore } from 'systeminformation' +import { machineId } from 'node-machine-id' + +import { detectShell } from './utils/detect-shell' +import { getSystemInfo } from './utils/system-info' +import { logger } from './utils/logger' + +// Enhanced CLI fingerprint implementation using multiple Node.js data sources +const getEnhancedFingerprintInfo = async () => { + // Get essential system information efficiently + const [ + systemInfo, + cpuInfo, + osInfo_, + machineIdValue, + systemInfoBasic, + shell, + networkInfo + ] = await Promise.all([ + system(), + cpu(), + osInfo(), + machineId().catch(() => 'unknown'), + getSystemInfo(), + detectShell(), + Promise.resolve(networkInterfaces()) + ]) -const getFingerprintInfo = async () => { + // Extract MAC addresses for additional uniqueness + const macAddresses = Object.values(networkInfo) + .flat() + .filter(iface => iface && !iface.internal && iface.mac && iface.mac !== '00:00:00:00:00:00') + .map(iface => iface!.mac) + .sort() + + return { + // Hardware identifiers + system: { + manufacturer: systemInfo.manufacturer, + model: systemInfo.model, + serial: systemInfo.serial, + uuid: systemInfo.uuid, + }, + cpu: { + manufacturer: cpuInfo.manufacturer, + brand: cpuInfo.brand, + cores: cpuInfo.cores, + physicalCores: cpuInfo.physicalCores, + }, + os: { + platform: osInfo_.platform, + distro: osInfo_.distro, + arch: osInfo_.arch, + hostname: osInfo_.hostname, + }, + // CLI-specific identifiers + runtime: { + nodeVersion: systemInfoBasic.nodeVersion, + platform: systemInfoBasic.platform, + arch: systemInfoBasic.arch, + shell, + cpuCount: systemInfoBasic.cpus, + }, + // Network identifiers + network: { + macAddresses, + interfaceCount: Object.keys(networkInfo).length, + }, + // Machine ID (OS-specific unique identifier) + machineId: machineIdValue, + // Timestamp for version tracking + fingerprintVersion: '2.0', + } as Record +} + +// Legacy fingerprint implementation (for backward compatibility) +const getLegacyFingerprintInfo = async () => { const { manufacturer, model, serial, uuid } = await system() const { vendor, version: biosVersion, releaseDate } = await bios() - const { - manufacturer: boardManufacturer, - model: boardModel, - serial: boardSerial, - } = await baseboard() - const { - manufacturer: cpuManufacturer, - brand, - speedMax, - cores, - physicalCores, - socket, - } = await cpu() + const { manufacturer: cpuManufacturer, brand, speed, cores } = await cpu() + const { total: totalMemory } = await mem() + const { controllers } = await graphics() const { platform, arch } = await osInfo() return { - EOL, - endianness: endianness(), - manufacturer, - model, - serial, - uuid, - vendor, - biosVersion, - releaseDate, - boardManufacturer, - boardModel, - boardSerial, - cpuManufacturer, - brand, - speedMax: speedMax.toFixed(2), - cores, - physicalCores, - socket, - platform, - arch, + system: { + manufacturer, + model, + serial, + uuid, + }, + bios: { + vendor, + version: biosVersion, + releaseDate, + }, + cpu: { + manufacturer: cpuManufacturer, + brand, + speed, + cores, + }, + memory: { + total: totalMemory, + }, + graphics: { + controllers: controllers?.map((c) => ({ + vendor: c.vendor, + model: c.model, + vram: c.vram, + })), + }, + os: { + platform, + arch, + }, } as Record } -export async function calculateFingerprint() { - const fingerprintInfo = await getFingerprintInfo() + +// Enhanced CLI-only fingerprint (deterministic, no browser required) +async function calculateEnhancedFingerprint(): Promise { + try { + const fingerprintInfo = await getEnhancedFingerprintInfo() + const fingerprintString = JSON.stringify(fingerprintInfo) + const fingerprintHash = createHash('sha256') + .update(fingerprintString) + .digest() + .toString('base64url') + + // No random suffix needed - comprehensive system data provides sufficient uniqueness + return `enhanced-${fingerprintHash}` + } catch (error) { + logger.warn( + { + errorMessage: error instanceof Error ? error.message : String(error), + fingerprintType: 'enhanced_failed', + }, + 'Enhanced CLI fingerprinting failed, falling back to legacy' + ) + throw error + } +} + +// Legacy implementation with random suffix (still needed for collision avoidance) +async function calculateLegacyFingerprint() { + const fingerprintInfo = await getLegacyFingerprintInfo() const fingerprintString = JSON.stringify(fingerprintInfo) const fingerprintHash = createHash('sha256') .update(fingerprintString) @@ -63,5 +166,50 @@ export async function calculateFingerprint() { // Add 8 random characters to make the fingerprint unique even on identical hardware const randomSuffix = randomBytes(6).toString('base64url').substring(0, 8) - return `${fingerprintHash}-${randomSuffix}` + return `legacy-${fingerprintHash}-${randomSuffix}` +} + +// Main fingerprint function with CLI-only approach +export async function calculateFingerprint(): Promise { + try { + // Try enhanced CLI fingerprinting first + const fingerprint = await calculateEnhancedFingerprint() + logger.info( + { + fingerprintType: 'enhanced_cli', + fingerprintId: fingerprint, + }, + 'Enhanced CLI fingerprint generated successfully' + ) + return fingerprint + } catch (enhancedError) { + logger.info( + { + errorMessage: enhancedError instanceof Error ? enhancedError.message : String(enhancedError), + fingerprintType: 'enhanced_failed_fallback', + }, + 'Enhanced CLI fingerprinting failed, using legacy fallback' + ) + + try { + const fingerprint = await calculateLegacyFingerprint() + logger.info( + { + fingerprintType: 'legacy_fallback', + fingerprintId: fingerprint, + }, + 'Legacy fingerprint generated successfully as fallback' + ) + return fingerprint + } catch (legacyError) { + logger.error( + { + errorMessage: legacyError instanceof Error ? legacyError.message : String(legacyError), + fingerprintType: 'failed', + }, + 'Both enhanced and legacy fingerprint generation failed' + ) + throw new Error('Fingerprint generation failed') + } + } } diff --git a/npm-app/src/utils/analytics.ts b/npm-app/src/utils/analytics.ts index cf4b87df1..00d38ea56 100644 --- a/npm-app/src/utils/analytics.ts +++ b/npm-app/src/utils/analytics.ts @@ -4,6 +4,7 @@ import { PostHog } from 'posthog-node' import { logger } from './logger' import { suppressConsoleOutput } from './suppress-console' + // Prints the events to console // It's very noisy, so recommended you set this to true // only when you're actively adding new analytics @@ -113,6 +114,44 @@ export function identifyUser(userId: string, properties?: Record) { }) } +// User identification with enhanced fingerprint data +export function identifyUserWithFingerprint( + userId: string, + properties?: Record +) { + // Store the user ID for future events + currentUserId = userId + + if (!client) { + throw new Error('Analytics client not initialized') + } + + // Enhanced properties with fingerprint metadata + const enhancedProperties = { + ...properties, + fingerprintType: properties?.fingerprintId?.startsWith('enhanced-') ? 'enhanced_cli' : + properties?.fingerprintId?.startsWith('fp-') ? 'enhanced_browser' : + properties?.fingerprintId?.startsWith('legacy-') ? 'legacy' : 'unknown', + hasEnhancedFingerprint: properties?.fingerprintId?.startsWith('enhanced-') || + properties?.fingerprintId?.startsWith('fp-') || false, + } + + if (process.env.NEXT_PUBLIC_CB_ENVIRONMENT !== 'prod') { + if (DEBUG_DEV_EVENTS) { + console.log('Enhanced identify event sent', { + userId, + properties: enhancedProperties, + }) + } + return + } + + client.identify({ + distinctId: userId, + properties: enhancedProperties, + }) +} + export function logError( error: any, userId?: string,