mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-10-01 05:54:56 +02:00
[eric] fix: exempt /api/health/* (prefix) so packaged boot doesn't deadlock on 401, onboarding: mirror Settings' provider connect flow so Gemini works, fix packaged-mode boot + sync onboarding Gemini connect flow
This commit is contained in:
+6
-4
@@ -88,15 +88,17 @@ _AUTH_EXEMPT_EXACT = {
|
||||
# Google/Anthropic/etc). The `state` query param is already a
|
||||
# one-time nonce validated against `_pending_oauth`.
|
||||
"/api/subscriptions/callback",
|
||||
# Electron's boot handshake pings these before it has a token —
|
||||
# the HTTP port is up before the token file is readable in some
|
||||
# races. Safe to expose since they don't return any session data.
|
||||
"/api/health",
|
||||
"/api/version",
|
||||
}
|
||||
|
||||
# Path prefixes that never require auth. Trailing slash optional.
|
||||
_AUTH_EXEMPT_PREFIX = (
|
||||
# Electron's boot handshake polls /api/health/check before it has a
|
||||
# token (the HTTP port is up before main.js calls loadAuthToken()).
|
||||
# Use a prefix so /api/health/check — and any future sub-route — is
|
||||
# covered without re-introducing the bootstrap deadlock that an
|
||||
# exact "/api/health" match caused.
|
||||
"/api/health",
|
||||
# FastAPI's default health/docs/schema surface (packaged app never
|
||||
# ships /docs, but be defensive).
|
||||
"/docs",
|
||||
|
||||
@@ -3943,7 +3943,7 @@ var require_core = __commonJS({
|
||||
constructor(opts = {}) {
|
||||
this.schemas = {};
|
||||
this.refs = {};
|
||||
this.formats = {};
|
||||
this.formats = /* @__PURE__ */ Object.create(null);
|
||||
this._compilations = /* @__PURE__ */ new Set();
|
||||
this._loading = {};
|
||||
this._cache = /* @__PURE__ */ new Map();
|
||||
|
||||
+28
-13
@@ -404,9 +404,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ajv": {
|
||||
"version": "8.18.0",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz",
|
||||
"integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==",
|
||||
"version": "8.20.0",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz",
|
||||
"integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
@@ -497,9 +497,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/body-parser": {
|
||||
"version": "1.20.4",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz",
|
||||
"integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==",
|
||||
"version": "1.20.5",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz",
|
||||
"integrity": "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bytes": "~3.1.2",
|
||||
@@ -510,7 +510,7 @@
|
||||
"http-errors": "~2.0.1",
|
||||
"iconv-lite": "~0.4.24",
|
||||
"on-finished": "~2.4.1",
|
||||
"qs": "~6.14.0",
|
||||
"qs": "~6.15.1",
|
||||
"raw-body": "~2.5.3",
|
||||
"type-is": "~1.6.18",
|
||||
"unpipe": "~1.0.0"
|
||||
@@ -520,6 +520,21 @@
|
||||
"npm": "1.2.8000 || >= 1.4.16"
|
||||
}
|
||||
},
|
||||
"node_modules/body-parser/node_modules/qs": {
|
||||
"version": "6.15.1",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz",
|
||||
"integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"side-channel": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/body-parser/node_modules/raw-body": {
|
||||
"version": "2.5.3",
|
||||
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz",
|
||||
@@ -929,9 +944,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/express-rate-limit": {
|
||||
"version": "8.3.2",
|
||||
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.3.2.tgz",
|
||||
"integrity": "sha512-77VmFeJkO0/rvimEDuUC5H30oqUC4EyOhyGccfqoLebB0oiEYfM7nwPrsDsBL1gsTpwfzX8SFy2MT3TDyRq+bg==",
|
||||
"version": "8.4.1",
|
||||
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.4.1.tgz",
|
||||
"integrity": "sha512-NGVYwQSAyEQgzxX1iCM978PP9AdO/hW93gMcF6ZwQCm+rFvLsBH6w4xcXWTcliS8La5EPRN3p9wzItqBwJrfNw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ip-address": "10.1.0"
|
||||
@@ -1182,9 +1197,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/hono": {
|
||||
"version": "4.12.14",
|
||||
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.14.tgz",
|
||||
"integrity": "sha512-am5zfg3yu6sqn5yjKBNqhnTX7Cv+m00ox+7jbaKkrLMRJ4rAdldd1xPd/JzbBWspqaQv6RSTrgFN95EsfhC+7w==",
|
||||
"version": "4.12.15",
|
||||
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.15.tgz",
|
||||
"integrity": "sha512-qM0jDhFEaCBb4TxoW7f53Qrpv9RBiayUHo0S52JudprkhvpjIrGoU1mnnr29Fvd1U335ZFPZQY1wlkqgfGXyLg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=16.9.0"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"name": "notionhq-notion-mcp-server",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
@@ -11,6 +12,5 @@
|
||||
"type": "commonjs",
|
||||
"dependencies": {
|
||||
"@notionhq/notion-mcp-server": "^2.2.1"
|
||||
},
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
|
||||
+24
-24
@@ -9,7 +9,7 @@
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@softeria/ms-365-mcp-server": "^0.85.0"
|
||||
"@softeria/ms-365-mcp-server": "^0.85.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@azure-rest/core-client": {
|
||||
@@ -177,13 +177,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/identity/node_modules/@azure/msal-node": {
|
||||
"version": "5.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-5.1.3.tgz",
|
||||
"integrity": "sha512-LqT8mRZpEils9zGR9eW+Ljqifh2aMA99UF/X0jxIKDYZeHr6onlHwhVP4xHCeLhh55BI63JCbdf1iWJbMh1mPw==",
|
||||
"version": "5.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-5.1.4.tgz",
|
||||
"integrity": "sha512-G4LXGGggok1QC48uKu64/SV2DPRDlddmV8EieK8pflsNYMj9/Zz+Y9OHoEBhT15h+zpdwXXLYA/7PJCR/yZ8aw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@azure/msal-common": "16.5.0",
|
||||
"@azure/msal-common": "16.5.1",
|
||||
"jsonwebtoken": "^9.0.0",
|
||||
"uuid": "^8.3.0"
|
||||
},
|
||||
@@ -284,22 +284,22 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/msal-browser": {
|
||||
"version": "5.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-5.7.0.tgz",
|
||||
"integrity": "sha512-uYbJ0YarxkVGWEq814BysJry/IPvpDNkVKmc2bMZp4G+igUQkJ5nlFirycwPGUeA9ICLQqCxqExCA1Z1E07bPA==",
|
||||
"version": "5.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-5.8.0.tgz",
|
||||
"integrity": "sha512-X7IZV77bN56l7sbLjkcbQJX1t3U4tgxqztDr/XFbUcUfKk+z2FavcLgKP+OYUNj0wl/pEEtV9lldW9siY8BuHQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@azure/msal-common": "16.5.0"
|
||||
"@azure/msal-common": "16.5.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/msal-common": {
|
||||
"version": "16.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-16.5.0.tgz",
|
||||
"integrity": "sha512-i3eS/5pmxDbIU/mLMENs88Qg3k6XxqJytJy6PpB7L1tCBjdXHJDadCD3Hu1TyTooe7iQo7CYqbocgL/l/8u90g==",
|
||||
"version": "16.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-16.5.1.tgz",
|
||||
"integrity": "sha512-WS9w9SfI8SEYO7mTnxGeZ3UwQfhAVYCWglYF2/7GNx3ioHiAs2gPkl9eSwVs8cPrmiGh+zi9ai/OOKoq4cyzDw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
@@ -412,9 +412,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@softeria/ms-365-mcp-server": {
|
||||
"version": "0.85.0",
|
||||
"resolved": "https://registry.npmjs.org/@softeria/ms-365-mcp-server/-/ms-365-mcp-server-0.85.0.tgz",
|
||||
"integrity": "sha512-QKzuaCFQEApBWLXbMAwoM+ptTvwxggCCPkFGWhKn1OlY9CO+IPxX9rTRIkOvsTUg6L3KpnKnzyeiAhet5gI4uA==",
|
||||
"version": "0.85.1",
|
||||
"resolved": "https://registry.npmjs.org/@softeria/ms-365-mcp-server/-/ms-365-mcp-server-0.85.1.tgz",
|
||||
"integrity": "sha512-G7tF8rQ6D8yu5H935wLLTANGIHVk7f9zU7iWMLqozthPoW4jRaKjCi9ZYaLtNx4HXv+zI3i7neJwvh6RuMkyew==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@azure/msal-node": "^3.8.0",
|
||||
@@ -492,9 +492,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ajv": {
|
||||
"version": "8.18.0",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz",
|
||||
"integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==",
|
||||
"version": "8.20.0",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz",
|
||||
"integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
@@ -1098,9 +1098,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/express-rate-limit": {
|
||||
"version": "8.3.2",
|
||||
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.3.2.tgz",
|
||||
"integrity": "sha512-77VmFeJkO0/rvimEDuUC5H30oqUC4EyOhyGccfqoLebB0oiEYfM7nwPrsDsBL1gsTpwfzX8SFy2MT3TDyRq+bg==",
|
||||
"version": "8.4.1",
|
||||
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.4.1.tgz",
|
||||
"integrity": "sha512-NGVYwQSAyEQgzxX1iCM978PP9AdO/hW93gMcF6ZwQCm+rFvLsBH6w4xcXWTcliS8La5EPRN3p9wzItqBwJrfNw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ip-address": "10.1.0"
|
||||
@@ -1285,9 +1285,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/hono": {
|
||||
"version": "4.12.14",
|
||||
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.14.tgz",
|
||||
"integrity": "sha512-am5zfg3yu6sqn5yjKBNqhnTX7Cv+m00ox+7jbaKkrLMRJ4rAdldd1xPd/JzbBWspqaQv6RSTrgFN95EsfhC+7w==",
|
||||
"version": "4.12.15",
|
||||
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.15.tgz",
|
||||
"integrity": "sha512-qM0jDhFEaCBb4TxoW7f53Qrpv9RBiayUHo0S52JudprkhvpjIrGoU1mnnr29Fvd1U335ZFPZQY1wlkqgfGXyLg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=16.9.0"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"name": "softeria-ms-365-mcp-server",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
@@ -10,6 +11,6 @@
|
||||
"license": "ISC",
|
||||
"type": "commonjs",
|
||||
"dependencies": {
|
||||
"@softeria/ms-365-mcp-server": "^0.85.0"
|
||||
"@softeria/ms-365-mcp-server": "^0.85.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ function isValidEmail(email: string): boolean {
|
||||
const SUBSCRIPTION_PROVIDERS = [
|
||||
{ id: 'openswarm-pro', name: 'OpenSwarm Pro', desc: 'One subscription — no setup, no Claude account needed', color: '#6366F1', preview: false, recommended: true },
|
||||
{ id: 'claude', name: 'Claude', desc: 'Use your own Claude Pro/Max subscription', color: '#E8927A', preview: false },
|
||||
{ id: 'antigravity', name: 'Gemini', desc: 'Gemini 3 Pro, 3 Flash, 2.5 Pro & Flash', color: '#4285F4', preview: false },
|
||||
{ id: 'antigravity', name: 'Gemini Advanced', desc: 'Gemini 3 Pro, 3 Flash, 2.5 Pro, 2.5 Flash', color: '#4285F4', preview: false },
|
||||
{ id: 'codex', name: 'ChatGPT', desc: 'GPT-5.4, GPT-5.4 Mini, GPT-5.3 Codex', color: '#74AA9C', preview: false },
|
||||
];
|
||||
|
||||
@@ -373,7 +373,16 @@ const OnboardingModal: React.FC = () => {
|
||||
trackEvent('onboarding.email_suggestion_applied');
|
||||
};
|
||||
|
||||
// Same connect logic as Settings/SubscriptionCards
|
||||
// Mirrors Settings/SubscriptionCards `handleConnect` so the Gemini
|
||||
// (and every other provider) connect popup behaves identically in
|
||||
// onboarding and settings. The only onboarding-specific differences:
|
||||
// - OpenSwarm Pro routes to the pricing step instead of OAuth.
|
||||
// - On success we `dismiss()` the modal to advance the flow.
|
||||
// Anything else — popup vs. system-browser dispatch, dual
|
||||
// device-code+status polling, postMessage / Electron IPC code
|
||||
// delivery, focus-based "user closed the popup" reset, 5-minute
|
||||
// hard timeout — must stay byte-for-byte aligned with Settings or
|
||||
// Gemini's anti-embedded-browser policy will break it again.
|
||||
const handleConnect = async (providerId: string) => {
|
||||
// Cancel any previous attempt
|
||||
if (pollTimerRef.current) { clearInterval(pollTimerRef.current); pollTimerRef.current = null; }
|
||||
@@ -391,58 +400,111 @@ const OnboardingModal: React.FC = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
// Delay before calling connect — avoids Claude OAuth rate limit on retries
|
||||
await new Promise(r => setTimeout(r, 1000));
|
||||
// Small delay if retrying — avoids hitting Claude's rate limit
|
||||
await new Promise(r => setTimeout(r, 500));
|
||||
|
||||
try {
|
||||
const r = await fetch(`${API_BASE}/agents/subscriptions/connect`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ provider: providerId }),
|
||||
});
|
||||
if (!r.ok) {
|
||||
setConnecting(null);
|
||||
return;
|
||||
}
|
||||
if (!r.ok) { setConnecting(null); return; }
|
||||
const data = await r.json();
|
||||
|
||||
if (data.flow === 'device_code') {
|
||||
if (data.verification_uri) window.open(data.verification_uri, '_blank');
|
||||
// Use a named window with features so Electron's
|
||||
// setWindowOpenHandler routes it as a `new-window` popup, not a
|
||||
// dashboard webview tab. Keep the reference so we can auto-close
|
||||
// it once the backend poll detects success.
|
||||
let devicePopup: Window | null = null;
|
||||
if (data.verification_uri) {
|
||||
devicePopup = window.open(data.verification_uri, 'oauth_connect', 'width=600,height=720');
|
||||
}
|
||||
|
||||
const timer = setInterval(async () => {
|
||||
let stopped = false;
|
||||
const onDeviceSuccess = () => {
|
||||
if (stopped) return;
|
||||
stopped = true;
|
||||
clearInterval(devicePollTimer);
|
||||
clearInterval(statusPollTimer);
|
||||
pollTimerRef.current = null;
|
||||
trackEvent('onboarding.provider_connected', { provider: providerId });
|
||||
// Auto-close the popup 2s after success so the user briefly
|
||||
// sees the "Connected!" page then it goes away on its own.
|
||||
setTimeout(() => {
|
||||
if (devicePopup && !devicePopup.closed) {
|
||||
try { devicePopup.close(); } catch {}
|
||||
}
|
||||
}, 2000);
|
||||
dismiss();
|
||||
};
|
||||
|
||||
// Path 1: device-code poll — primary path when 9Router is happy.
|
||||
const pollOnce = async () => {
|
||||
if (stopped) return;
|
||||
try {
|
||||
const pr = await fetch(`${API_BASE}/agents/subscriptions/poll`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
provider: providerId,
|
||||
device_code: data.device_code,
|
||||
code_verifier: data.code_verifier,
|
||||
extra_data: data.extra_data,
|
||||
}),
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ provider: providerId, device_code: data.device_code, code_verifier: data.code_verifier, extra_data: data.extra_data }),
|
||||
});
|
||||
if (!pr.ok) return;
|
||||
const pd = await pr.json();
|
||||
if (pd.success) {
|
||||
clearInterval(timer);
|
||||
pollTimerRef.current = null;
|
||||
trackEvent('onboarding.provider_connected', { provider: providerId });
|
||||
dismiss();
|
||||
if (pd.success) onDeviceSuccess();
|
||||
} catch {}
|
||||
};
|
||||
pollOnce();
|
||||
const devicePollTimer = setInterval(pollOnce, 5000);
|
||||
|
||||
// Path 2: status poller — checks 9Router's connection list every
|
||||
// 2s. Catches the connection even if the device-code poll silently
|
||||
// errors (e.g. 9Router 500 from postExchange).
|
||||
const statusPollTimer = setInterval(async () => {
|
||||
if (stopped) return;
|
||||
try {
|
||||
const sr = await fetch(`${API_BASE}/agents/subscriptions/status`);
|
||||
const sd = await sr.json();
|
||||
const connections = sd.providers?.connections || [];
|
||||
if (connections.some((p: any) => p.provider === providerId && (p.isActive || p.testStatus === 'active'))) {
|
||||
onDeviceSuccess();
|
||||
}
|
||||
} catch {}
|
||||
}, 5000);
|
||||
pollTimerRef.current = timer;
|
||||
// 3-minute popup timeout (was 30s). OAuth with 2FA on Windows can
|
||||
// easily take >30s; the connectedProviders poller above still picks
|
||||
// up the connection after timeout, but a longer in-flow window
|
||||
// keeps the "Connecting…" indicator accurate instead of flipping
|
||||
// back to "Connect →" mid-auth.
|
||||
setTimeout(() => { clearInterval(timer); pollTimerRef.current = null; setConnecting(null); }, 180000);
|
||||
}, 2000);
|
||||
|
||||
pollTimerRef.current = devicePollTimer;
|
||||
|
||||
// 5-minute hard timeout — clean up everything if the user
|
||||
// walks away mid-flow.
|
||||
setTimeout(() => {
|
||||
if (stopped) return;
|
||||
stopped = true;
|
||||
clearInterval(devicePollTimer);
|
||||
clearInterval(statusPollTimer);
|
||||
pollTimerRef.current = null;
|
||||
setConnecting(null);
|
||||
if (devicePopup && !devicePopup.closed) {
|
||||
try { devicePopup.close(); } catch {}
|
||||
}
|
||||
}, 300000);
|
||||
|
||||
} else if (data.flow === 'authorization_code') {
|
||||
const popup = window.open(data.auth_url, 'oauth_connect', 'width=600,height=700');
|
||||
// Some providers (currently Gemini/Google) enforce an
|
||||
// anti-embedded-browser policy on their OAuth consent page that
|
||||
// no amount of user-agent spoofing defeats. For those, the
|
||||
// backend sets `use_external_browser: true` and we open the
|
||||
// auth URL in the user's default browser via shell.openExternal.
|
||||
// The callback then lands on the backend's own
|
||||
// /api/subscriptions/callback endpoint, which performs the
|
||||
// exchange itself. Detection happens via the status poller —
|
||||
// no postMessage handoff is possible because the system browser
|
||||
// has no window.opener relationship back to us.
|
||||
const useExternal = !!data.use_external_browser;
|
||||
let popup: Window | null = null;
|
||||
if (useExternal && (window as any).openswarm?.openExternal) {
|
||||
(window as any).openswarm.openExternal(data.auth_url);
|
||||
} else {
|
||||
popup = window.open(data.auth_url, 'oauth_connect', 'width=600,height=700');
|
||||
}
|
||||
|
||||
// Centralized exchange + cleanup so all three detection paths
|
||||
// (postMessage, Electron IPC, status polling) can trigger it.
|
||||
let exchanged = false;
|
||||
const runExchange = async (code: string, state?: string) => {
|
||||
if (exchanged) return;
|
||||
@@ -451,41 +513,33 @@ const OnboardingModal: React.FC = () => {
|
||||
window.removeEventListener('message', msgHandlerRef.current);
|
||||
msgHandlerRef.current = null;
|
||||
}
|
||||
if (ipcUnsub) { ipcUnsub(); ipcUnsub = null; }
|
||||
if (pollTimerRef.current) { clearInterval(pollTimerRef.current); pollTimerRef.current = null; }
|
||||
if (ipcUnsub) ipcUnsub();
|
||||
clearInterval(statusPoller);
|
||||
pollTimerRef.current = null;
|
||||
if (popup && !popup.closed) popup.close();
|
||||
try {
|
||||
await fetch(`${API_BASE}/agents/subscriptions/exchange`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
provider: providerId,
|
||||
code,
|
||||
redirect_uri: data.redirect_uri,
|
||||
code_verifier: data.code_verifier,
|
||||
state: state || data.state,
|
||||
}),
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ provider: providerId, code, redirect_uri: data.redirect_uri, code_verifier: data.code_verifier, state: state || data.state }),
|
||||
});
|
||||
} catch {}
|
||||
trackEvent('onboarding.provider_connected', { provider: providerId });
|
||||
dismiss();
|
||||
};
|
||||
|
||||
// Poll status as one detection path (works when the connection
|
||||
// gets created server-side without a client-side callback).
|
||||
const statusPoller = setInterval(async () => {
|
||||
try {
|
||||
const sr = await fetch(`${API_BASE}/agents/subscriptions/status`);
|
||||
const sd = await sr.json();
|
||||
const connections = sd.providers?.connections || [];
|
||||
if (connections.some((p: any) => p.provider === providerId && p.isActive)) {
|
||||
if (connections.some((p: any) => p.provider === providerId && (p.isActive || p.testStatus === 'active'))) {
|
||||
if (!exchanged) {
|
||||
exchanged = true;
|
||||
if (msgHandlerRef.current) {
|
||||
window.removeEventListener('message', msgHandlerRef.current);
|
||||
msgHandlerRef.current = null;
|
||||
}
|
||||
if (ipcUnsub) { ipcUnsub(); ipcUnsub = null; }
|
||||
if (ipcUnsub) ipcUnsub();
|
||||
clearInterval(statusPoller);
|
||||
pollTimerRef.current = null;
|
||||
trackEvent('onboarding.provider_connected', { provider: providerId });
|
||||
@@ -496,24 +550,23 @@ const OnboardingModal: React.FC = () => {
|
||||
}, 2000);
|
||||
pollTimerRef.current = statusPoller;
|
||||
|
||||
// postMessage from the popup's /callback page (faster when the
|
||||
// popup isn't cross-origin).
|
||||
// postMessage listener — only meaningful for the in-app popup
|
||||
// path. The system-browser path can't reach window.opener.
|
||||
const msgHandler = async (event: MessageEvent) => {
|
||||
const d = event.data;
|
||||
const callbackData = d?.type === 'oauth_callback' ? d.data : d;
|
||||
if (callbackData?.code) await runExchange(callbackData.code, callbackData.state);
|
||||
};
|
||||
window.addEventListener('message', msgHandler);
|
||||
msgHandlerRef.current = msgHandler;
|
||||
if (!useExternal) {
|
||||
window.addEventListener('message', msgHandler);
|
||||
msgHandlerRef.current = msgHandler;
|
||||
}
|
||||
|
||||
// Electron IPC fallback — main.js captures child webContents
|
||||
// Electron IPC fallback — main.js captures any child webContents
|
||||
// navigating to localhost:20128/callback?code=... and forwards
|
||||
// the parsed params here. This is the REQUIRED path for Claude
|
||||
// OAuth in Electron (cross-origin redirects sever opener chain,
|
||||
// so postMessage can't fire). Without this listener, the
|
||||
// onboarding flow would only see the connection via the
|
||||
// 2-second status poll — but the code never gets exchanged
|
||||
// because the CLI callback page in the popup never reaches us.
|
||||
// the parsed params here. Required for Claude OAuth in Electron
|
||||
// (cross-origin redirects sever the opener chain, so
|
||||
// postMessage can't fire).
|
||||
let ipcUnsub: (() => void) | null = null;
|
||||
const ow = (window as any).openswarm;
|
||||
if (ow && typeof ow.onOauthCallback === 'function') {
|
||||
@@ -522,19 +575,26 @@ const OnboardingModal: React.FC = () => {
|
||||
});
|
||||
}
|
||||
|
||||
// Timeout: 3 min for popup flow (was 30s — too short for 2FA /
|
||||
// slow Windows OAuth where postMessage can silently fail), 5
|
||||
// minutes for external-browser flow (user has to tab-switch,
|
||||
// log in, consent — takes much longer in practice).
|
||||
const timeoutMs = useExternal ? 300_000 : 180_000;
|
||||
setTimeout(() => {
|
||||
if (pollTimerRef.current) { clearInterval(pollTimerRef.current); pollTimerRef.current = null; }
|
||||
if (msgHandlerRef.current) { window.removeEventListener('message', msgHandlerRef.current); msgHandlerRef.current = null; }
|
||||
if (ipcUnsub) { ipcUnsub(); ipcUnsub = null; }
|
||||
clearInterval(statusPoller);
|
||||
pollTimerRef.current = null;
|
||||
if (!useExternal && msgHandlerRef.current) {
|
||||
window.removeEventListener('message', msgHandlerRef.current);
|
||||
msgHandlerRef.current = null;
|
||||
}
|
||||
if (ipcUnsub) ipcUnsub();
|
||||
setConnecting(null);
|
||||
}, 180000);
|
||||
}, timeoutMs);
|
||||
|
||||
} else {
|
||||
setConnecting(null);
|
||||
}
|
||||
} catch {
|
||||
setConnecting(null);
|
||||
}
|
||||
} catch { setConnecting(null); }
|
||||
};
|
||||
|
||||
const handleApiKey = () => { trackEvent('onboarding.api_key_chosen'); dismiss(); };
|
||||
|
||||
@@ -26,6 +26,19 @@ elif [[ "${1:-}" == "--sign" ]]; then
|
||||
SIGN_MODE=true
|
||||
fi
|
||||
|
||||
# Defensive: detach any leftover OpenSwarm DMG volumes from prior failed builds.
|
||||
# hdiutil's "Resource busy" / volume-name-collision errors almost always trace
|
||||
# back to a stale mount in /Volumes (e.g. after a build crash or a still-open
|
||||
# Finder window from the last run).
|
||||
shopt -s nullglob
|
||||
for vol in /Volumes/OpenSwarm*; do
|
||||
if [[ -d "$vol" ]]; then
|
||||
echo "Detaching leftover DMG mount: $vol"
|
||||
hdiutil detach -force "$vol" 2>/dev/null || hdiutil detach "$vol" 2>/dev/null || true
|
||||
fi
|
||||
done
|
||||
shopt -u nullglob
|
||||
|
||||
echo "========================================"
|
||||
echo " OpenSwarm Desktop App Builder"
|
||||
if $PUBLISH_MODE; then
|
||||
|
||||
Reference in New Issue
Block a user