[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:
ciregenz
2026-04-25 02:27:23 -07:00
parent e14c3c57cc
commit 801268d6b7
8 changed files with 205 additions and 114 deletions
+6 -4
View File
@@ -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",
+1 -1
View File
@@ -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();
@@ -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": ""
}
}
@@ -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"
}
}
+129 -69
View File
@@ -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(); };
+13
View File
@@ -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