From aecfd7ae3491a2f77ad0703e9433777e11f4e62c Mon Sep 17 00:00:00 2001 From: brandonkachen Date: Mon, 7 Jul 2025 16:57:18 -0700 Subject: [PATCH 1/2] Enhance fingerprinting with FingerprintJS and remove random suffix for enhanced fingerprints MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add @fingerprintjs/fingerprintjs dependency for browser-based fingerprinting - Enhanced fingerprints now use deterministic format: fp-{hash} (no random suffix) - Legacy fingerprints keep random suffix for collision avoidance: legacy-{hash}-{suffix} - Update analytics to properly detect both fingerprint formats - Add comprehensive integration tests for both fingerprint types - Enhanced fingerprints provide better uniqueness through browser signals - Maintain backward compatibility with existing legacy fingerprints šŸ¤– Generated with Codebuff Co-Authored-By: Codebuff --- bun.lock | 9 +- npm-app/package.json | 1 + .../__tests__/fingerprint-integration.test.ts | 63 +++++ npm-app/src/browser-runner.ts | 24 +- npm-app/src/client.ts | 8 +- npm-app/src/fingerprint.ts | 219 +++++++++++++++--- npm-app/src/utils/analytics.ts | 37 +++ 7 files changed, 305 insertions(+), 56 deletions(-) create mode 100644 npm-app/src/__tests__/fingerprint-integration.test.ts diff --git a/bun.lock b/bun.lock index cd0aa169b..f636b7986 100644 --- a/bun.lock +++ b/bun.lock @@ -116,6 +116,7 @@ "dependencies": { "@codebuff/code-map": "workspace:*", "@codebuff/common": "workspace:*", + "@fingerprintjs/fingerprintjs": "^4.6.2", "@types/diff": "5.2.1", "@types/micromatch": "^4.0.9", "@vscode/ripgrep": "1.15.9", @@ -603,6 +604,8 @@ "@fal-works/esbuild-plugin-global-externals": ["@fal-works/esbuild-plugin-global-externals@2.1.2", "", {}, "sha512-cEee/Z+I12mZcFJshKcCqC8tuX5hG3s+d+9nZ3LabqKF1vKdF41B92pJVCBggjAGORAeOzyyDDKrZwIkLffeOQ=="], + "@fingerprintjs/fingerprintjs": ["@fingerprintjs/fingerprintjs@4.6.2", "", { "dependencies": { "tslib": "^2.4.1" } }, "sha512-g8mXuqcFKbgH2CZKwPfVtsUJDHyvcgIABQI7Y0tzWEFXpGxJaXuAuzlifT2oTakjDBLTK4Gaa9/5PERDhqUjtw=="], + "@floating-ui/core": ["@floating-ui/core@1.7.1", "", { "dependencies": { "@floating-ui/utils": "^0.2.9" } }, "sha512-azI0DrjMMfIug/ExbBaeDVJXcY0a7EPvPjb2xAJPa4HeimBX+Z18HK8QQR3jb6356SnDDdxx+hinMLcJEDdOjw=="], "@floating-ui/dom": ["@floating-ui/dom@1.7.1", "", { "dependencies": { "@floating-ui/core": "^1.7.1", "@floating-ui/utils": "^0.2.9" } }, "sha512-cwsmW/zyw5ltYTUeeYJ60CnQuPqmGwuGVhG9w0PRaRKkAyi38BT5CKrpIbb+jtahSwUl04cWzSx9ZOIxeS6RsQ=="], @@ -1653,7 +1656,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 +2868,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 +4064,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..15e346d03 100644 --- a/npm-app/package.json +++ b/npm-app/package.json @@ -42,6 +42,7 @@ "dependencies": { "@codebuff/code-map": "workspace:*", "@codebuff/common": "workspace:*", + "@fingerprintjs/fingerprintjs": "^4.6.2", "@types/diff": "5.2.1", "@types/micromatch": "^4.0.9", "@vscode/ripgrep": "1.15.9", 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..1914ebaa8 --- /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 and legacy modes', async () => { + console.log('šŸ” Testing enhanced 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('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) + }, 30000) // 30 second timeout for browser 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..f0ea06212 100644 --- a/npm-app/src/fingerprint.ts +++ b/npm-app/src/fingerprint.ts @@ -1,58 +1,77 @@ +// Enhanced fingerprinting with FingerprintJS and backward compatibility // Modified from: https://github.com/andsmedeiros/hw-fingerprint import { createHash, randomBytes } from 'node:crypto' -import { EOL, endianness } from 'node:os' import { - system, bios, - baseboard, cpu, + graphics, + mem, osInfo, + system, // @ts-ignore } from 'systeminformation' +import { findChrome } from './browser-runner' +import { logger } from './utils/logger' + +// Type declaration for FingerprintJS result +declare global { + interface Window { + fingerprintResult?: { + visitorId: string + confidence: { score: number } + components: Record + } + } +} + +// Legacy fingerprint implementation (for backward compatibility) const getFingerprintInfo = 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() { + +// Legacy implementation with random suffix (still needed for collision avoidance) +async function calculateLegacyFingerprint() { const fingerprintInfo = await getFingerprintInfo() const fingerprintString = JSON.stringify(fingerprintInfo) const fingerprintHash = createHash('sha256') @@ -63,5 +82,129 @@ 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}` +} + +// Enhanced FingerprintJS implementation using headless browser +async function calculateEnhancedFingerprint(): Promise { + try { + const puppeteer = await import('puppeteer-core') + + const browser = await puppeteer.default.launch({ + headless: true, + args: ['--no-sandbox', '--disable-setuid-sandbox'], + executablePath: findChrome(), + }) + + const page = await browser.newPage() + + // Create a minimal HTML page with FingerprintJS + const html = ` + + + + + + + + + + ` + + await page.setContent(html) + + // Wait for FingerprintJS to complete + await page.waitForFunction(() => window.fingerprintResult, { timeout: 10000 }) + + // Extract the fingerprint result + const result = await page.evaluate(() => window.fingerprintResult) + + await browser.close() + + // Combine FingerprintJS result with system info for enhanced uniqueness + const systemInfo = await getFingerprintInfo() + const combinedData = { + fingerprintjs: result!.visitorId, + confidence: result!.confidence, + system: systemInfo, + } + + const combinedString = JSON.stringify(combinedData) + const combinedHash = createHash('sha256') + .update(combinedString) + .digest() + .toString('base64url') + + // No random suffix needed - FingerprintJS provides sufficient uniqueness + return `fp-${combinedHash}` + } catch (error) { + logger.warn( + { + errorMessage: error instanceof Error ? error.message : String(error), + fingerprintType: 'enhanced_failed', + }, + 'Enhanced fingerprinting failed, falling back to legacy' + ) + throw error + } +} + +// Main fingerprint function with hybrid approach +export async function calculateFingerprint(): Promise { + try { + // Try enhanced fingerprinting first + const fingerprint = await calculateEnhancedFingerprint() + logger.info( + { + fingerprintType: 'enhanced', + fingerprintId: fingerprint, + }, + 'Enhanced fingerprint generated successfully' + ) + return fingerprint + } catch (enhancedError) { + logger.info( + { + errorMessage: enhancedError instanceof Error ? enhancedError.message : String(enhancedError), + fingerprintType: 'enhanced_failed_fallback', + }, + 'Enhanced 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..e2b99d155 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,42 @@ 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('fp-') ? 'enhanced' : + properties?.fingerprintId?.startsWith('legacy-') ? 'legacy' : 'unknown', + hasEnhancedFingerprint: 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, From 58efb812dda08c1807e3af14b41900657a286702 Mon Sep 17 00:00:00 2001 From: brandonkachen Date: Mon, 7 Jul 2025 17:04:29 -0700 Subject: [PATCH 2/2] fix: switch to node-machine-id, which is supposedly faster and just as accurate --- bun.lock | 4 +- npm-app/package.json | 2 +- .../__tests__/fingerprint-integration.test.ts | 8 +- npm-app/src/fingerprint.ts | 197 +++++++++--------- npm-app/src/utils/analytics.ts | 6 +- 5 files changed, 111 insertions(+), 106 deletions(-) diff --git a/bun.lock b/bun.lock index f636b7986..776480324 100644 --- a/bun.lock +++ b/bun.lock @@ -116,7 +116,6 @@ "dependencies": { "@codebuff/code-map": "workspace:*", "@codebuff/common": "workspace:*", - "@fingerprintjs/fingerprintjs": "^4.6.2", "@types/diff": "5.2.1", "@types/micromatch": "^4.0.9", "@vscode/ripgrep": "1.15.9", @@ -130,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", @@ -604,8 +604,6 @@ "@fal-works/esbuild-plugin-global-externals": ["@fal-works/esbuild-plugin-global-externals@2.1.2", "", {}, "sha512-cEee/Z+I12mZcFJshKcCqC8tuX5hG3s+d+9nZ3LabqKF1vKdF41B92pJVCBggjAGORAeOzyyDDKrZwIkLffeOQ=="], - "@fingerprintjs/fingerprintjs": ["@fingerprintjs/fingerprintjs@4.6.2", "", { "dependencies": { "tslib": "^2.4.1" } }, "sha512-g8mXuqcFKbgH2CZKwPfVtsUJDHyvcgIABQI7Y0tzWEFXpGxJaXuAuzlifT2oTakjDBLTK4Gaa9/5PERDhqUjtw=="], - "@floating-ui/core": ["@floating-ui/core@1.7.1", "", { "dependencies": { "@floating-ui/utils": "^0.2.9" } }, "sha512-azI0DrjMMfIug/ExbBaeDVJXcY0a7EPvPjb2xAJPa4HeimBX+Z18HK8QQR3jb6356SnDDdxx+hinMLcJEDdOjw=="], "@floating-ui/dom": ["@floating-ui/dom@1.7.1", "", { "dependencies": { "@floating-ui/core": "^1.7.1", "@floating-ui/utils": "^0.2.9" } }, "sha512-cwsmW/zyw5ltYTUeeYJ60CnQuPqmGwuGVhG9w0PRaRKkAyi38BT5CKrpIbb+jtahSwUl04cWzSx9ZOIxeS6RsQ=="], diff --git a/npm-app/package.json b/npm-app/package.json index 15e346d03..f64abb63d 100644 --- a/npm-app/package.json +++ b/npm-app/package.json @@ -42,7 +42,6 @@ "dependencies": { "@codebuff/code-map": "workspace:*", "@codebuff/common": "workspace:*", - "@fingerprintjs/fingerprintjs": "^4.6.2", "@types/diff": "5.2.1", "@types/micromatch": "^4.0.9", "@vscode/ripgrep": "1.15.9", @@ -56,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 index 1914ebaa8..0ab22a17a 100644 --- a/npm-app/src/__tests__/fingerprint-integration.test.ts +++ b/npm-app/src/__tests__/fingerprint-integration.test.ts @@ -1,8 +1,8 @@ import { calculateFingerprint } from '../fingerprint' describe('Fingerprint Integration Test', () => { - it('should generate fingerprints and test both enhanced and legacy modes', async () => { - console.log('šŸ” Testing enhanced fingerprinting implementation...') + 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 = [] @@ -14,7 +14,7 @@ describe('Fingerprint Integration Test', () => { results.push({ fingerprint, duration, - isEnhanced: fingerprint.startsWith('fp-'), + isEnhanced: fingerprint.startsWith('enhanced-') || fingerprint.startsWith('fp-'), isLegacy: fingerprint.startsWith('legacy-') }) @@ -59,5 +59,5 @@ describe('Fingerprint Integration Test', () => { // At least one should succeed expect(results.length).toBeGreaterThan(0) - }, 30000) // 30 second timeout for browser operations + }, 10000) // 10 second timeout for CLI operations }) diff --git a/npm-app/src/fingerprint.ts b/npm-app/src/fingerprint.ts index f0ea06212..748b3b5e8 100644 --- a/npm-app/src/fingerprint.ts +++ b/npm-app/src/fingerprint.ts @@ -1,7 +1,8 @@ -// Enhanced fingerprinting with FingerprintJS and backward compatibility +// Enhanced fingerprinting with CLI-only approach and backward compatibility // Modified from: https://github.com/andsmedeiros/hw-fingerprint import { createHash, randomBytes } from 'node:crypto' +import { networkInterfaces } from 'node:os' import { bios, cpu, @@ -11,23 +12,82 @@ import { system, // @ts-ignore } from 'systeminformation' +import { machineId } from 'node-machine-id' -import { findChrome } from './browser-runner' +import { detectShell } from './utils/detect-shell' +import { getSystemInfo } from './utils/system-info' import { logger } from './utils/logger' -// Type declaration for FingerprintJS result -declare global { - interface Window { - fingerprintResult?: { - visitorId: string - confidence: { score: number } - components: Record - } - } +// 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()) + ]) + + // 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 getFingerprintInfo = async () => { +const getLegacyFingerprintInfo = async () => { const { manufacturer, model, serial, uuid } = await system() const { vendor, version: biosVersion, releaseDate } = await bios() const { manufacturer: cpuManufacturer, brand, speed, cores } = await cpu() @@ -70,111 +130,56 @@ const getFingerprintInfo = async () => { } as Record } -// Legacy implementation with random suffix (still needed for collision avoidance) -async function calculateLegacyFingerprint() { - const fingerprintInfo = await getFingerprintInfo() - const fingerprintString = JSON.stringify(fingerprintInfo) - const fingerprintHash = createHash('sha256') - .update(fingerprintString) - .digest() - .toString('base64url') - - // Add 8 random characters to make the fingerprint unique even on identical hardware - const randomSuffix = randomBytes(6).toString('base64url').substring(0, 8) - - return `legacy-${fingerprintHash}-${randomSuffix}` -} - -// Enhanced FingerprintJS implementation using headless browser +// Enhanced CLI-only fingerprint (deterministic, no browser required) async function calculateEnhancedFingerprint(): Promise { try { - const puppeteer = await import('puppeteer-core') - - const browser = await puppeteer.default.launch({ - headless: true, - args: ['--no-sandbox', '--disable-setuid-sandbox'], - executablePath: findChrome(), - }) - - const page = await browser.newPage() - - // Create a minimal HTML page with FingerprintJS - const html = ` - - - - - - - - - - ` - - await page.setContent(html) - - // Wait for FingerprintJS to complete - await page.waitForFunction(() => window.fingerprintResult, { timeout: 10000 }) - - // Extract the fingerprint result - const result = await page.evaluate(() => window.fingerprintResult) - - await browser.close() - - // Combine FingerprintJS result with system info for enhanced uniqueness - const systemInfo = await getFingerprintInfo() - const combinedData = { - fingerprintjs: result!.visitorId, - confidence: result!.confidence, - system: systemInfo, - } - - const combinedString = JSON.stringify(combinedData) - const combinedHash = createHash('sha256') - .update(combinedString) + const fingerprintInfo = await getEnhancedFingerprintInfo() + const fingerprintString = JSON.stringify(fingerprintInfo) + const fingerprintHash = createHash('sha256') + .update(fingerprintString) .digest() .toString('base64url') - - // No random suffix needed - FingerprintJS provides sufficient uniqueness - return `fp-${combinedHash}` + + // 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 fingerprinting failed, falling back to legacy' + 'Enhanced CLI fingerprinting failed, falling back to legacy' ) throw error } } -// Main fingerprint function with hybrid approach +// 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) + .digest() + .toString('base64url') + + // Add 8 random characters to make the fingerprint unique even on identical hardware + const randomSuffix = randomBytes(6).toString('base64url').substring(0, 8) + + return `legacy-${fingerprintHash}-${randomSuffix}` +} + +// Main fingerprint function with CLI-only approach export async function calculateFingerprint(): Promise { try { - // Try enhanced fingerprinting first + // Try enhanced CLI fingerprinting first const fingerprint = await calculateEnhancedFingerprint() logger.info( { - fingerprintType: 'enhanced', + fingerprintType: 'enhanced_cli', fingerprintId: fingerprint, }, - 'Enhanced fingerprint generated successfully' + 'Enhanced CLI fingerprint generated successfully' ) return fingerprint } catch (enhancedError) { @@ -183,7 +188,7 @@ export async function calculateFingerprint(): Promise { errorMessage: enhancedError instanceof Error ? enhancedError.message : String(enhancedError), fingerprintType: 'enhanced_failed_fallback', }, - 'Enhanced fingerprinting failed, using legacy fallback' + 'Enhanced CLI fingerprinting failed, using legacy fallback' ) try { diff --git a/npm-app/src/utils/analytics.ts b/npm-app/src/utils/analytics.ts index e2b99d155..00d38ea56 100644 --- a/npm-app/src/utils/analytics.ts +++ b/npm-app/src/utils/analytics.ts @@ -129,9 +129,11 @@ export function identifyUserWithFingerprint( // Enhanced properties with fingerprint metadata const enhancedProperties = { ...properties, - fingerprintType: properties?.fingerprintId?.startsWith('fp-') ? 'enhanced' : + fingerprintType: properties?.fingerprintId?.startsWith('enhanced-') ? 'enhanced_cli' : + properties?.fingerprintId?.startsWith('fp-') ? 'enhanced_browser' : properties?.fingerprintId?.startsWith('legacy-') ? 'legacy' : 'unknown', - hasEnhancedFingerprint: properties?.fingerprintId?.startsWith('fp-') || false, + hasEnhancedFingerprint: properties?.fingerprintId?.startsWith('enhanced-') || + properties?.fingerprintId?.startsWith('fp-') || false, } if (process.env.NEXT_PUBLIC_CB_ENVIRONMENT !== 'prod') {