diff --git a/backend/apps/subscription/router.py b/backend/apps/subscription/router.py index c3640a08..38be56f2 100644 --- a/backend/apps/subscription/router.py +++ b/backend/apps/subscription/router.py @@ -239,11 +239,16 @@ async def sync(): No-op when not in openswarm-pro mode. Best-effort: network failures are swallowed — the caller still gets a 200 with whatever local state we already had.""" + # Lazy-import the PostHog helper so subscription/router doesn't pay the + # cost when analytics are disabled. + from backend.apps.analytics.collector import record as _record + settings_obj = load_settings() bearer = getattr(settings_obj, "openswarm_bearer_token", None) mode = getattr(settings_obj, "connection_mode", "own_key") if mode != "openswarm-pro" or not bearer: + _record("subscription.sync_ran", {"reason": "no_bearer"}) return {"ok": True, "synced": False, "connection_mode": mode} try: @@ -254,6 +259,7 @@ async def sync(): ) except httpx.HTTPError as e: logger.debug("subscription/sync live fetch failed: %s", e) + _record("subscription.sync_ran", {"reason": "network"}) return {"ok": True, "synced": False, "reason": "network"} # Same 401/402 handling as /status: if Stripe-side reconciliation proves @@ -261,15 +267,18 @@ async def sync(): # reverts to own_key instead of hammering a useless token. if r.status_code in (401, 402): _clear_subscription(settings_obj) + reason = "revoked" if r.status_code == 401 else "expired" + _record("subscription.sync_ran", {"reason": reason}) return { "ok": True, "synced": False, "connected": False, - "reason": "revoked" if r.status_code == 401 else "expired", + "reason": reason, } if r.status_code != 200: logger.debug("subscription/sync got %s from cloud: %s", r.status_code, r.text[:200]) + _record("subscription.sync_ran", {"reason": "upstream", "status_code": r.status_code}) return {"ok": True, "synced": False, "reason": "upstream"} data = r.json() @@ -287,6 +296,11 @@ async def sync(): ) _write_settings(settings_obj) _sync_subscription_identity(settings_obj) + _record("subscription.sync_ran", { + "reason": "ok", + "synced": bool(data.get("synced")), + "plan": cloud_plan, + }) return { "ok": True, "synced": bool(data.get("synced")), diff --git a/electron/main.js b/electron/main.js index f313658c..f280aac9 100644 --- a/electron/main.js +++ b/electron/main.js @@ -283,9 +283,21 @@ function createWindow() { mainWindow.loadFile(frontendPath); } - mainWindow.webContents.on('will-attach-webview', (_event, webPreferences, _params) => { + mainWindow.webContents.on('will-attach-webview', (_event, webPreferences, params) => { webPreferences.plugins = true; webPreferences.enableBlinkFeatures = 'EncryptedMedia'; + // Force our webview preload to attach for every , unconditionally. + // The alternative (reading window.openswarm.getWebviewPreloadPath() in + // BrowserCard's React code at module-eval time) raced against the + // preload's async contextBridge exposure — the resulting attribute on + // the element ended up empty, so no preload ran and our + // passkey shim never loaded. Setting webPreferences.preload here runs + // on every attach and can't be out-raced. Absolute path (not file://) + // is what webPreferences expects. + webPreferences.preload = path.join(__dirname, 'webview-preload.js'); + try { + console.log('[openswarm:attach-webview] forced preload=', webPreferences.preload, 'src=', params.src); + } catch (_) {} }); mainWindow.webContents.on('will-navigate', (event, url) => { @@ -528,7 +540,9 @@ app.on('web-contents-created', (_event, contents) => { contents.on('console-message', (_e, level, message, line, sourceId) => { if (message.includes('widevine') || message.includes('drm') || message.includes('license') || message.includes('MediaKeySession') || - message.includes('EME') || message.includes('[drm-diag]') || level >= 2) { + message.includes('EME') || message.includes('[drm-diag]') || + message.includes('openswarm') || + level >= 2) { const tag = ['LOG', 'INFO', 'WARN', 'ERROR'][level] || 'LOG'; const src = sourceId ? sourceId.split('/').pop() : ''; console.log(`[webview:${tag}] ${message}${src ? ` (${src}:${line})` : ''}`); @@ -564,7 +578,59 @@ app.on('web-contents-created', (_event, contents) => { cdpQueueByWcId.delete(contents.id); }); + // WebAuthn/passkey shim. Injected on every dom-ready in the main world + // via executeJavaScript (which uses V8's direct evaluation path and + // bypasses Trusted Types CSP — inline