mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-11 12:17:45 +02:00
[eric] seamless subscription connect — no 9Router GUI needed
OAuth flow now stays entirely within OpenSwarm:
1. Click "Connect Claude" → Anthropic sign-in opens as popup
2. User signs in → 9Router's /callback catches redirect
3. postMessage sends code back to OpenSwarm window
4. OpenSwarm exchanges code via /subscriptions/exchange proxy
5. Shows "Connected ✓" — done in 3 steps, 1 app
Changes:
- nine_router.py: return real authUrl for auth_code providers
instead of redirecting to 9Router dashboard
- nine_router.py: add exchange_oauth() proxy function
- agents.py: add POST /subscriptions/exchange endpoint
- Settings.tsx: popup + window.addEventListener('message') for
OAuth callback, with polling fallback
Before: 8 steps across 2 apps
After: 3 steps in 1 app (click → sign in → done)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
7430eae88a
commit
24ad784cd0
@@ -336,6 +336,26 @@ async def subscriptions_poll(body: dict):
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@agents.router.post("/subscriptions/exchange")
|
||||
async def subscriptions_exchange(body: dict):
|
||||
"""Exchange OAuth code for tokens via 9Router."""
|
||||
from backend.apps.nine_router import exchange_oauth
|
||||
provider = body.get("provider", "")
|
||||
code = body.get("code", "")
|
||||
redirect_uri = body.get("redirect_uri", "")
|
||||
code_verifier = body.get("code_verifier", "")
|
||||
state = body.get("state", "")
|
||||
|
||||
if not provider or not code:
|
||||
raise HTTPException(status_code=400, detail="provider and code required")
|
||||
|
||||
try:
|
||||
result = await exchange_oauth(provider, code, redirect_uri, code_verifier, state)
|
||||
return result
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@agents.router.get("/subscriptions/models")
|
||||
async def subscriptions_models():
|
||||
"""List all models available through connected subscriptions."""
|
||||
|
||||
@@ -123,12 +123,21 @@ async def start_oauth(provider: str) -> dict:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# For authorization_code providers (Claude, Codex, Gemini),
|
||||
# open 9Router's own dashboard page where the user can connect.
|
||||
# 9Router handles the full OAuth flow (popup + callback) internally.
|
||||
# Authorization code flow — get the real auth URL from 9Router
|
||||
# The callback goes to 9Router's /callback page which sends postMessage back
|
||||
callback_url = f"http://localhost:{NINE_ROUTER_PORT}/callback"
|
||||
r = await client.get(
|
||||
f"{NINE_ROUTER_API}/oauth/{provider}/authorize",
|
||||
params={"redirect_uri": callback_url},
|
||||
)
|
||||
r.raise_for_status()
|
||||
data = r.json()
|
||||
return {
|
||||
"flow": "dashboard_redirect",
|
||||
"dashboard_url": f"http://localhost:{NINE_ROUTER_PORT}/dashboard/providers",
|
||||
"flow": "authorization_code",
|
||||
"auth_url": data.get("authUrl", ""),
|
||||
"code_verifier": data.get("codeVerifier", ""),
|
||||
"state": data.get("state", ""),
|
||||
"redirect_uri": callback_url,
|
||||
}
|
||||
|
||||
|
||||
@@ -152,6 +161,22 @@ async def poll_oauth(provider: str, device_code: str, code_verifier: str | None
|
||||
return r.json()
|
||||
|
||||
|
||||
async def exchange_oauth(provider: str, code: str, redirect_uri: str, code_verifier: str, state: str = "") -> dict:
|
||||
"""Exchange OAuth code for tokens via 9Router."""
|
||||
async with httpx.AsyncClient(timeout=15.0) as client:
|
||||
r = await client.post(
|
||||
f"{NINE_ROUTER_API}/oauth/{provider}/exchange",
|
||||
json={
|
||||
"code": code,
|
||||
"redirectUri": redirect_uri,
|
||||
"codeVerifier": code_verifier,
|
||||
"state": state,
|
||||
},
|
||||
)
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
|
||||
|
||||
async def get_models() -> list[dict]:
|
||||
"""Get all available models from 9Router."""
|
||||
try:
|
||||
|
||||
@@ -251,27 +251,54 @@ const SubscriptionCards: React.FC = () => {
|
||||
setPollTimer(timer);
|
||||
setTimeout(() => { clearInterval(timer); setConnecting(null); setUserCode(''); }, 300000);
|
||||
|
||||
} else if (data.flow === 'authorization_code' || data.flow === 'dashboard_redirect') {
|
||||
// Auth code flow (Claude, Codex, Gemini) — open 9Router dashboard, poll for connection
|
||||
const url = data.dashboard_url || data.auth_url;
|
||||
if (url) window.open(url, '_blank');
|
||||
} else if (data.flow === 'authorization_code') {
|
||||
// Auth code flow (Claude, Codex, Gemini) — open popup, listen for postMessage callback
|
||||
const popup = window.open(data.auth_url, 'oauth', 'popup,width=600,height=700');
|
||||
|
||||
// Poll the providers list until this provider appears as connected
|
||||
// Listen for OAuth callback from 9Router's /callback page
|
||||
const handler = async (event: MessageEvent) => {
|
||||
// 9Router's callback page sends: { type: 'oauth-callback', code, state } or just { code, state }
|
||||
const d = event.data;
|
||||
if (d?.code || d?.type === 'oauth-callback') {
|
||||
window.removeEventListener('message', handler);
|
||||
try {
|
||||
// Exchange the code for tokens via our backend proxy
|
||||
await fetch(`${API_BASE}/agents/subscriptions/exchange`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
provider: providerId,
|
||||
code: d.code,
|
||||
redirect_uri: data.redirect_uri,
|
||||
code_verifier: data.code_verifier,
|
||||
state: d.state || data.state,
|
||||
}),
|
||||
});
|
||||
} catch {}
|
||||
if (popup && !popup.closed) popup.close();
|
||||
setConnecting(null);
|
||||
fetchStatus();
|
||||
}
|
||||
};
|
||||
window.addEventListener('message', handler);
|
||||
|
||||
// Also poll as fallback in case postMessage doesn't work
|
||||
const timer = setInterval(async () => {
|
||||
try {
|
||||
const sr = await fetch(`${API_BASE}/agents/subscriptions/status`);
|
||||
const sd = await sr.json();
|
||||
const providers = Array.isArray(sd.providers?.connections) ? sd.providers.connections : Array.isArray(sd.providers) ? sd.providers : [];
|
||||
const found = providers.some((p: any) => p.provider === providerId && p.isActive);
|
||||
const connections = sd.providers?.connections || (Array.isArray(sd.providers) ? sd.providers : []);
|
||||
const found = connections.some((p: any) => p.provider === providerId && p.isActive);
|
||||
if (found) {
|
||||
clearInterval(timer);
|
||||
window.removeEventListener('message', handler);
|
||||
setConnecting(null);
|
||||
fetchStatus();
|
||||
}
|
||||
} catch {}
|
||||
}, 3000);
|
||||
setPollTimer(timer);
|
||||
setTimeout(() => { clearInterval(timer); setConnecting(null); }, 300000);
|
||||
setTimeout(() => { clearInterval(timer); window.removeEventListener('message', handler); setConnecting(null); }, 300000);
|
||||
|
||||
} else {
|
||||
setConnecting(null);
|
||||
|
||||
Reference in New Issue
Block a user