[aidan] chore/affiliate-attribution: align private names and trim comments

This commit is contained in:
abccodes
2026-07-10 19:31:35 -07:00
parent 1e1d0d3c60
commit fb89324462
6 changed files with 30 additions and 113 deletions
-4
View File
@@ -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,
+1 -5
View File
@@ -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
-3
View File
@@ -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"
+12 -77
View File
@@ -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 `<userData>/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 <projectRoot>/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,
};
+14 -15
View File
@@ -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, {});
});
+3 -9
View File
@@ -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 {};
}