diff --git a/electron/affiliateTracking.js b/electron/affiliateTracking.js index 6f060dd0..cb97c4dc 100644 --- a/electron/affiliateTracking.js +++ b/electron/affiliateTracking.js @@ -41,6 +41,10 @@ const FILENAME_ATTRIBUTION_WINDOW_MS = Number(process.env.OPENSWARM_AFFILIATE_FILENAME_WINDOW_MS) || 30 * 24 * 60 * 60 * 1000; const INSTALLER_HASH_RE = /^OpenSwarm(?:-Setup)?-(?:arm64|x64)-([A-Za-z0-9_-]{16,32})(?: \([0-9]+\))?\.(dmg|exe|AppImage)$/i; +// A file whose mtime is slightly in the future is NOT suspicious: APFS/NTFS +// keep sub-ms timestamps, and NTP can step the clock backwards between the +// download and first launch. Only skip files more than a minute ahead. +const FUTURE_MTIME_TOLERANCE_MS = 60 * 1000; function getStateFilePath(userDataDir) { return path.join(userDataDir, "install.json"); @@ -214,7 +218,7 @@ function recentInstallerHashesInDir(dir, nowMs) { try { const st = fs.statSync(fullPath); const ageMs = nowMs - st.mtimeMs; - if (ageMs < 0 || ageMs > FILENAME_ATTRIBUTION_WINDOW_MS) continue; + if (ageMs < -FUTURE_MTIME_TOLERANCE_MS || ageMs > FILENAME_ATTRIBUTION_WINDOW_MS) continue; out.push({ hash, path: fullPath, mtimeMs: st.mtimeMs }); } catch (_) {} } diff --git a/electron/affiliateTracking.test.js b/electron/affiliateTracking.test.js index 8ae98705..c81b8949 100644 --- a/electron/affiliateTracking.test.js +++ b/electron/affiliateTracking.test.js @@ -297,6 +297,56 @@ test("filename parser keeps hyphens inside base64url affiliate hash", () => { ); }); +test("filename parser covers every stamped artifact shape (mac/win/linux)", () => { + const h = "abcDEF1234567890_hash"; + for (const name of [ + `OpenSwarm-arm64-${h}.dmg`, // mac Apple Silicon + `OpenSwarm-x64-${h}.dmg`, // mac Intel + `OpenSwarm-Setup-x64-${h}.exe`, // windows squirrel setup + `OpenSwarm-x64-${h}.AppImage`, // linux x64 + `OpenSwarm-arm64-${h}.AppImage`, // linux arm64 + ]) { + assert.equal(affiliateTracking._hashFromInstallerBasename(name), h, name); + } + // Unstamped artifacts must NOT parse as carrying a hash. + for (const name of ["OpenSwarm-arm64.dmg", "OpenSwarm-Setup-x64.exe", "OpenSwarm-x64.AppImage"]) { + assert.equal(affiliateTracking._hashFromInstallerBasename(name), null, name); + } +}); + +test("first launch (win32): stamped setup exe in Downloads binds before welcome URL", async () => { + const cloud = await makeMockCloud(); + try { + const userDataDir = makeTempUserDataDir(); + const shell = makeFakeShell(); + const hash = "abcDEF1234567890_hash"; + cloud.filenameHashes.set(hash, "windows-affiliate"); + + const downloads = path.join(userDataDir, "Downloads"); + fs.mkdirSync(downloads, { recursive: true }); + fs.writeFileSync(path.join(downloads, `OpenSwarm-Setup-x64-${hash}.exe`), ""); + + process.env.OPENSWARM_AFFILIATE_LANDING_URL = "https://landing.test"; + process.env.OPENSWARM_AFFILIATE_CLOUD_URL = cloud.url; + + await affiliateTracking.maybeRunFirstLaunchHandshake({ + shell, + userDataDir, + isDev: false, + isPackaged: true, + platform: "win32", + homeDir: userDataDir, + }); + + assert.equal(shell.opened.length, 0, "filename hash bind skips welcome URL"); + const state = readJson(path.join(userDataDir, "install.json")); + assert.equal(state.ref, "windows-affiliate"); + assert.equal(state.ref_bind_method, "affiliate_filename_hash"); + } finally { + await cloud.close(); + } +}); + test("download scan refuses ambiguous stamped installers", () => { const userDataDir = makeTempUserDataDir(); const downloads = path.join(userDataDir, "Downloads");