From fb89324462c7e94e349910047e4500c9e9ea977a Mon Sep 17 00:00:00 2001 From: abccodes Date: Fri, 10 Jul 2026 19:31:35 -0700 Subject: [PATCH] [aidan] chore/affiliate-attribution: align private names and trim comments --- backend/apps/auth/router.py | 4 -- backend/main.py | 6 +- backend/tests/test_auth_router.py | 3 - electron/affiliateTracking.js | 89 ++++-------------------------- electron/affiliateTracking.test.js | 29 +++++----- electron/main.js | 12 +--- 6 files changed, 30 insertions(+), 113 deletions(-) diff --git a/backend/apps/auth/router.py b/backend/apps/auth/router.py index 706995ff..dd813efd 100644 --- a/backend/apps/auth/router.py +++ b/backend/apps/auth/router.py @@ -106,10 +106,6 @@ async def signin_activate(body: SigninActivateRequest): raise HTTPException(status_code=400, detail="Invalid token") proxy = p_proxy_url() - # settings.installation_id doubles as the affiliate app_install_id (one - # unified id, resolved by Electron before the backend spawns). Forwarding - # it lets the cloud record affiliate attribution the moment sign-in - # identifies the user, instead of waiting for a Stripe checkout. payload = { "token": body.token, "signin_method": body.signin_method, diff --git a/backend/main.py b/backend/main.py index cfac7651..5827f78e 100644 --- a/backend/main.py +++ b/backend/main.py @@ -70,11 +70,7 @@ try: from backend.apps.settings.store import load_settings as p_load_boot_settings, save_settings as p_save_boot_settings p_boot_settings = p_load_boot_settings() if not getattr(p_boot_settings, "installation_id", None): - # Prefer the unified install id Electron resolved before spawning us - # (shared with the affiliate install.json app_install_id), so the - # analytics install_id and the affiliate id are the same value and - # affiliate refs join directly to telemetry. Falls back to a fresh - # uuid for bare `uvicorn backend.main:app` runs. + # Electron resolves this before startup so analytics and affiliate attribution share one install id. p_env_iid = os.environ.get("OPENSWARM_INSTALLATION_ID", "") p_boot_settings.installation_id = ( p_env_iid if p_re.fullmatch(r"[A-Za-z0-9_-]{8,128}", p_env_iid) else p_uuid.uuid4().hex diff --git a/backend/tests/test_auth_router.py b/backend/tests/test_auth_router.py index ec351619..8690f21d 100644 --- a/backend/tests/test_auth_router.py +++ b/backend/tests/test_auth_router.py @@ -39,9 +39,6 @@ def reset_settings(): # --------------------------------------------------------------------------- /api/auth/signin-activate --------------------------------------------------------------------------- def test_signin_activate_persists_user_id(client, reset_settings): - # Give settings a known installation_id so we can assert the router - # forwards it as install_id (the unified id the cloud uses for both - # identity stitching and affiliate attribution). from backend.apps.settings.settings import load_settings, save_settings s = load_settings() s.installation_id = "unified-install-id-123" diff --git a/electron/affiliateTracking.js b/electron/affiliateTracking.js index cb97c4dc..650e6736 100644 --- a/electron/affiliateTracking.js +++ b/electron/affiliateTracking.js @@ -1,25 +1,3 @@ -// Affiliate / referral install tracking on the desktop side. -// -// On first launch the app opens https://openswarm.com/welcome?app_install_id=… -// in the user's default browser and polls the cloud's /api/install/lookup -// endpoint until a referral binding shows up (or we time out). The browser -// page is what actually performs the bind: it reads the install_token that -// the landing page stashed in localStorage / cookie when the user clicked -// Download, and POSTs it to the cloud paired with our app_install_id. -// -// State lives in `/install.json`. The shape: -// { -// app_install_id: "uuid", // unified install id, see resolveInstallId() -// first_launch_at: 1700000000000, // unix ms; presence = "this isn't first launch" -// ref: "haik" | null, // populated once lookup succeeds -// ref_bound_at: 1700000000000 | null, -// ref_bind_method: "affiliate_filename_hash" | null, -// attempts: 0 // last polling attempt count, for debugging -// } -// -// Skipped entirely in dev unless OPENSWARM_AFFILIATE_FORCE=1 is set, so -// `bash run.sh` doesn't pop a browser tab on every restart. - const fs = require("fs"); const path = require("path"); const crypto = require("crypto"); @@ -28,22 +6,13 @@ const os = require("os"); const DEFAULT_LANDING_URL = "https://openswarm.com"; const DEFAULT_CLOUD_URL = "https://api.openswarm.com"; -// Polling: 12 attempts, 5s apart = 60s window. Generous enough for the user -// to actually click through the welcome page; small enough that a stuck -// poll doesn't sit around all day. The page itself is fast (single POST) -// so most binds land in the first one or two ticks. -// -// Both knobs are overridable via env so tests can drive a 200ms × 5 -// poll window instead of 60s. const POLL_INTERVAL_MS = Number(process.env.OPENSWARM_AFFILIATE_POLL_INTERVAL_MS) || 5000; const POLL_MAX_ATTEMPTS = Number(process.env.OPENSWARM_AFFILIATE_POLL_MAX_ATTEMPTS) || 12; 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. +// Allow clock skew so a fresh installer is not discarded after an NTP adjustment. const FUTURE_MTIME_TOLERANCE_MS = 60 * 1000; function getStateFilePath(userDataDir) { @@ -56,7 +25,7 @@ function readState(userDataDir) { const raw = fs.readFileSync(p, "utf8"); const parsed = JSON.parse(raw); if (parsed && typeof parsed === "object") return parsed; - } catch (_) {} + } catch {} return {}; } @@ -64,9 +33,7 @@ function writeState(userDataDir, state) { const p = getStateFilePath(userDataDir); try { fs.mkdirSync(path.dirname(p), { recursive: true }); - // Atomic-ish write: temp file + rename. Avoids leaving a half-written - // install.json if the process is killed mid-write (which would brick - // first-launch detection on the next start). + // Rename a complete temp file so termination cannot leave invalid JSON. const tmp = p + ".tmp"; fs.writeFileSync(tmp, JSON.stringify(state, null, 2), "utf8"); fs.renameSync(tmp, p); @@ -75,31 +42,8 @@ function writeState(userDataDir, state) { } } -// -------------------------------------------------------------------------- -// Unified install identity. -// -// The desktop historically had TWO per-install ids that never met: this -// module's app_install_id (install.json, affiliate handshake) and the Python -// backend's settings.installation_id (analytics envelope). Affiliate data -// could therefore only join to analytics through a signed-in user — and -// sign-in is optional. resolveInstallId collapses them into one value: -// main.js calls it BEFORE spawning the backend and exports the result as -// OPENSWARM_INSTALLATION_ID, so install_tokens.app_install_id in the cloud -// and the analytics install_id carry the same id and affiliate refs join -// directly to telemetry with no sign-in required. -// -// Resolution order (first hit wins): -// 1. install.json app_install_id — continuity for installs that already -// ran the affiliate handshake -// 2. python settings.json installation_id — upgrades adopt the existing -// analytics identity instead of minting a second one -// 3. fresh crypto.randomUUID(), persisted to install.json immediately so -// every later reader (handshake, renderer, next boot) agrees on it - const INSTALL_ID_RE = /^[A-Za-z0-9_-]{8,128}$/; -// Mirrors backend/config/paths.py: packaged data root is per-OS app support; -// dev is /backend/data. function pythonSettingsFile({ isPackaged, projectRoot, platform, env, homeDir }) { if (!isPackaged) { return path.join(projectRoot, "backend", "data", "settings", "settings.json"); @@ -135,7 +79,7 @@ function resolveInstallId({ writeState(userDataDir, { ...state, app_install_id: iid }); return iid; } - } catch (_) {} + } catch {} const freshId = crypto.randomUUID(); writeState(userDataDir, { ...state, app_install_id: freshId }); @@ -151,8 +95,6 @@ function urlsFromEnv() { async function pollLookupOnce(cloudUrl, appInstallId) { const url = `${cloudUrl}/api/install/lookup?app_install_id=${encodeURIComponent(appInstallId)}`; - // Node 18+ ships global fetch; Electron 40 is on a Chromium that has it. - // Defensive timeout via AbortSignal.timeout (Node 17+). const controller = new AbortController(); const t = setTimeout(() => controller.abort(), 5000); try { @@ -161,7 +103,7 @@ async function pollLookupOnce(cloudUrl, appInstallId) { const body = await res.json(); if (body && typeof body.ref === "string" && body.ref) return body.ref; return null; - } catch (_) { + } catch { return null; } finally { clearTimeout(t); @@ -183,7 +125,7 @@ async function bindAffiliateHashOnce(cloudUrl, appInstallId, affiliateHash) { const body = await res.json(); if (body && typeof body.ref === "string" && body.ref) return body.ref; return null; - } catch (_) { + } catch { return null; } finally { clearTimeout(t); @@ -206,7 +148,7 @@ function recentInstallerHashesInDir(dir, nowMs) { let entries = []; try { entries = fs.readdirSync(dir, { withFileTypes: true }); - } catch (_) { + } catch { return out; } @@ -220,7 +162,7 @@ function recentInstallerHashesInDir(dir, nowMs) { const ageMs = nowMs - st.mtimeMs; if (ageMs < -FUTURE_MTIME_TOLERANCE_MS || ageMs > FILENAME_ATTRIBUTION_WINDOW_MS) continue; out.push({ hash, path: fullPath, mtimeMs: st.mtimeMs }); - } catch (_) {} + } catch {} } return out; } @@ -317,9 +259,6 @@ async function maybeRunFirstLaunchHandshake({ return; } - // First launch. Reuse the id resolveInstallId persisted before the backend - // spawned (the unified install id); only generate here if main.js never - // resolved one (e.g. direct module use in tests). const appInstallId = typeof state.app_install_id === "string" && INSTALL_ID_RE.test(state.app_install_id) ? state.app_install_id @@ -373,12 +312,8 @@ async function maybeRunFirstLaunchHandshake({ module.exports = { maybeRunFirstLaunchHandshake, resolveInstallId, - // Exported for tests + IPC handlers. - _readState: readState, - _writeState: writeState, - _getStateFilePath: getStateFilePath, - _pollLookupOnce: pollLookupOnce, - _bindAffiliateHashOnce: bindAffiliateHashOnce, - _hashFromInstallerBasename: hashFromInstallerBasename, - _findAffiliateHashFromInstaller: findAffiliateHashFromInstaller, + p_readState: readState, + p_writeState: writeState, + p_hashFromInstallerBasename: hashFromInstallerBasename, + p_findAffiliateHashFromInstaller: findAffiliateHashFromInstaller, }; diff --git a/electron/affiliateTracking.test.js b/electron/affiliateTracking.test.js index c81b8949..3a6fb6b6 100644 --- a/electron/affiliateTracking.test.js +++ b/electron/affiliateTracking.test.js @@ -285,14 +285,14 @@ test("first launch: stamped AppImage hash binds before opening welcome URL", asy test("filename parser accepts browser duplicate suffix", () => { assert.equal( - affiliateTracking._hashFromInstallerBasename("OpenSwarm-arm64-abcDEF1234567890_hash (1).dmg"), + affiliateTracking.p_hashFromInstallerBasename("OpenSwarm-arm64-abcDEF1234567890_hash (1).dmg"), "abcDEF1234567890_hash", ); }); test("filename parser keeps hyphens inside base64url affiliate hash", () => { assert.equal( - affiliateTracking._hashFromInstallerBasename("OpenSwarm-arm64-abcDEF1234567890-hash.dmg"), + affiliateTracking.p_hashFromInstallerBasename("OpenSwarm-arm64-abcDEF1234567890-hash.dmg"), "abcDEF1234567890-hash", ); }); @@ -300,17 +300,16 @@ 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 + `OpenSwarm-arm64-${h}.dmg`, + `OpenSwarm-x64-${h}.dmg`, + `OpenSwarm-Setup-x64-${h}.exe`, + `OpenSwarm-x64-${h}.AppImage`, + `OpenSwarm-arm64-${h}.AppImage`, ]) { - assert.equal(affiliateTracking._hashFromInstallerBasename(name), h, name); + assert.equal(affiliateTracking.p_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); + assert.equal(affiliateTracking.p_hashFromInstallerBasename(name), null, name); } }); @@ -354,7 +353,7 @@ test("download scan refuses ambiguous stamped installers", () => { fs.writeFileSync(path.join(downloads, "OpenSwarm-arm64-abcDEF1234567890_a.dmg"), ""); fs.writeFileSync(path.join(downloads, "OpenSwarm-arm64-abcDEF1234567890_b.dmg"), ""); - const hash = affiliateTracking._findAffiliateHashFromInstaller({ + const hash = affiliateTracking.p_findAffiliateHashFromInstaller({ platform: "darwin", homeDir: userDataDir, nowMs: Date.now(), @@ -364,7 +363,7 @@ test("download scan refuses ambiguous stamped installers", () => { test("resolveInstallId: reuses install.json app_install_id", () => { const userDataDir = makeTempUserDataDir(); - affiliateTracking._writeState(userDataDir, { app_install_id: "existing-id-12345" }); + affiliateTracking.p_writeState(userDataDir, { app_install_id: "existing-id-12345" }); const id = affiliateTracking.resolveInstallId({ userDataDir, isPackaged: true, projectRoot: userDataDir, homeDir: userDataDir, }); @@ -596,7 +595,7 @@ test("dev mode: skipped unless OPENSWARM_AFFILIATE_FORCE=1", async () => { test("install.json write is atomic-ish (temp + rename)", async () => { const userDataDir = makeTempUserDataDir(); - affiliateTracking._writeState(userDataDir, { app_install_id: "atomic-test-1234567890", ref: "x" }); + affiliateTracking.p_writeState(userDataDir, { app_install_id: "atomic-test-1234567890", ref: "x" }); // After write, the temp file shouldn't be left behind. const files = fs.readdirSync(userDataDir); assert.ok(files.includes("install.json")); @@ -605,14 +604,14 @@ test("install.json write is atomic-ish (temp + rename)", async () => { test("readState returns {} when no install.json exists", () => { const userDataDir = makeTempUserDataDir(); - const state = affiliateTracking._readState(userDataDir); + const state = affiliateTracking.p_readState(userDataDir); assert.deepEqual(state, {}); }); test("readState returns {} when install.json is corrupt", () => { const userDataDir = makeTempUserDataDir(); fs.writeFileSync(path.join(userDataDir, "install.json"), "{ not json"); - const state = affiliateTracking._readState(userDataDir); + const state = affiliateTracking.p_readState(userDataDir); assert.deepEqual(state, {}); }); diff --git a/electron/main.js b/electron/main.js index fa6c63bb..03a5cfac 100644 --- a/electron/main.js +++ b/electron/main.js @@ -944,12 +944,6 @@ async function startBackend() { PYTHONUTF8: '1', }; - // Unified install identity: resolve the affiliate app_install_id (adopting - // the backend's existing settings.installation_id on upgrades) BEFORE the - // backend spawns, and hand it down so the analytics install_id and the - // affiliate id are the same value. One id means affiliate refs join - // directly to telemetry without requiring a sign-in. See - // affiliateTracking.resolveInstallId(). try { env.OPENSWARM_INSTALLATION_ID = affiliateTracking.resolveInstallId({ userDataDir: app.getPath('userData'), @@ -990,8 +984,8 @@ async function startBackend() { // so a user-submitted backend.log instantly says what shipped. Emitted here // (not in whenReady) because openBackendLog() above just installed the console // tee; logging earlier would miss the persistent file. - const _bi = getBuildInfo(); - console.log(`[provenance] OpenSwarm ${app.getVersion()} sha=${_bi.shortSha} channel=${_bi.channel} builtAt=${_bi.builtAt || 'n/a'}`); + const p_buildInfo = getBuildInfo(); + console.log(`[provenance] OpenSwarm ${app.getVersion()} sha=${p_buildInfo.shortSha} channel=${p_buildInfo.channel} builtAt=${p_buildInfo.builtAt || 'n/a'}`); logPreflight(backendPort); runComprehensivePreflight(); // Record what we're about to launch and whether the interpreter is even @@ -2603,7 +2597,7 @@ ipcMain.handle('open-external', (_event, url) => { // (Stripe checkout, sign-in events) for downstream attribution. ipcMain.handle('get-install-state', () => { try { - return affiliateTracking._readState(app.getPath('userData')); + return affiliateTracking.p_readState(app.getPath('userData')); } catch (_) { return {}; }