mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-08-17 18:25:42 +02:00
[aidan] chore/affiliate-attribution: align private names and trim comments
This commit is contained in:
@@ -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
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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
@@ -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 {};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user