diff --git a/src/oauth/routes.ts b/src/oauth/routes.ts index dd30b7e..04a966b 100644 --- a/src/oauth/routes.ts +++ b/src/oauth/routes.ts @@ -76,71 +76,91 @@ export function registerOAuthRoutes( const { session } = await oauthClient.callback(params); const did = session.did; - // Enforce DID match if node already has an identity - if (config.DID && did !== config.DID) { - return c.html(errorPage( - "DID Mismatch", - `Authenticated as ${did} but this node is configured for ${config.DID}. Please log in with the correct account.`, - ), 403); - } - - // Safety net: purge any leftover data from a previous session - // (e.g. if a disconnect failed partway through) - if (replicationManager) { - try { replicationManager.getSyncStorage().purgeAllData(); } - catch (err) { console.warn("[oauth] Failed to purge leftover sync data:", err instanceof Error ? err.message : String(err)); } - try { replicationManager.getChallengeStorage().purgeAll(); } - catch (err) { console.warn("[oauth] Failed to purge leftover challenge data:", err instanceof Error ? err.message : String(err)); } - try { replicationManager.getPolicyEngine()?.clear(); } - catch (err) { console.warn("[oauth] Failed to purge leftover policies:", err instanceof Error ? err.message : String(err)); } - } - if (ipfsService) { - try { ipfsService.clearBlockstore(); } - catch (err) { console.warn("[oauth] Failed to purge leftover IPFS blocks:", err instanceof Error ? err.message : String(err)); } - } - try { rmSync(join(config.DATA_DIR, "blobs"), { recursive: true, force: true }); } - catch (err) { console.warn("[oauth] Failed to purge leftover blobs:", err instanceof Error ? err.message : String(err)); } - - // First login: establish node identity + // Detect whether this is the first OAuth login on this node, + // a re-login as the currently-active DID, or the addition of an + // extra account. const isFirstLogin = !config.DID; + const isAddingAccount = !!config.DID && did !== config.DID; + const isReLogin = !!config.DID && did === config.DID; + + // Wholesale data purge only on first login (clean slate). For + // re-login of the current DID we keep state — re-OAuth shouldn't + // blow away replication. For adding a different account we + // definitely keep state — the existing account's data must + // survive a multi-account add. if (isFirstLogin) { - config.DID = did; + if (replicationManager) { + try { replicationManager.getSyncStorage().purgeAllData(); } + catch (err) { console.warn("[oauth] Failed to purge leftover sync data:", err instanceof Error ? err.message : String(err)); } + try { replicationManager.getChallengeStorage().purgeAll(); } + catch (err) { console.warn("[oauth] Failed to purge leftover challenge data:", err instanceof Error ? err.message : String(err)); } + try { replicationManager.getPolicyEngine()?.clear(); } + catch (err) { console.warn("[oauth] Failed to purge leftover policies:", err instanceof Error ? err.message : String(err)); } + } + if (ipfsService) { + try { ipfsService.clearBlockstore(); } + catch (err) { console.warn("[oauth] Failed to purge leftover IPFS blocks:", err instanceof Error ? err.message : String(err)); } + } + try { rmSync(join(config.DATA_DIR, "blobs"), { recursive: true, force: true }); } + catch (err) { console.warn("[oauth] Failed to purge leftover blobs:", err instanceof Error ? err.message : String(err)); } + } - // Resolve handle for display - let handle: string | undefined; - try { - const res = await fetch( - `https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile?actor=${encodeURIComponent(did)}`, - ); - if (res.ok) { - const data = await res.json() as Record; - if (typeof data.handle === "string") { - handle = data.handle; - config.HANDLE = handle; - } + // Re-bind config.DID to the freshly-authenticated identity. For an + // account-add this hot-swaps the active DID; the previous account's + // data stays on disk and the registry remembers it as inactive. + // Some in-process singletons (RepoManager, PdsClient, replication + // state) still close over the previous DID — the UI's + // account-switcher surface flags `restartRequired: true` so the + // user knows to relaunch to fully load the new identity. + void isReLogin; + + // Resolve handle for display (best-effort; AppView profile lookup). + let handle: string | undefined; + try { + const res = await fetch( + `https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile?actor=${encodeURIComponent(did)}`, + ); + if (res.ok) { + const data = await res.json() as Record; + if (typeof data.handle === "string") { + handle = data.handle; } - } catch { - // Handle resolution is best-effort } - - // Persist identity so it survives restarts. setActive: true makes - // this the default account, demoting any previously active one. - // The other identities stay in the registry so an account switcher - // can offer them. - upsertIdentity(db, did, handle ?? null, { setActive: true }); - - console.log(`[oauth] Node identity established: ${did}${handle ? ` (@${handle})` : ""}`); + } catch { + // Handle resolution is best-effort } - // Create PdsClient if we don't have one yet - if (!pdsClientRef.current) { - pdsClientRef.current = new PdsClient(oauthClient, did); - // Late-bind into ReplicationManager so OfferManager can be created - if (replicationManager) { - replicationManager.setPdsClient(pdsClientRef.current, did); - } + // Persist identity to the multi-account registry. setActive: true + // makes this the default account; any previously active DID is + // demoted but its data stays on disk so the account-switcher + // can offer it later. + upsertIdentity(db, did, handle ?? null, { setActive: true }); + + // Hot-swap the running config to the freshly-authenticated DID. + // On first login this is the only place config.DID gets set. + // On account-add this swaps the active context so subsequent + // requests see the new identity. (RepoManager binding still + // closes over the old DID — see the restartRequired guidance in + // the UI; resolving that is a follow-up.) + config.DID = did; + if (handle) config.HANDLE = handle; + + // Bind / re-bind the PdsClient to the freshly-authenticated DID. + // Re-bind on account-add so subsequent PDS-mediated calls use + // the new session; on re-login this rebuilds the agent with the + // fresh OAuth tokens. + pdsClientRef.current?.clearAgent(); + pdsClientRef.current = new PdsClient(oauthClient, did); + if (replicationManager) { + replicationManager.setPdsClient(pdsClientRef.current, did); } + console.log( + `[oauth] Node identity ${ + isFirstLogin ? "established" : isAddingAccount ? "added (active)" : "refreshed" + }: ${did}${handle ? ` (@${handle})` : ""}`, + ); + // Publish peer record on successful auth try { await publishPeerRecord(pdsClientRef.current, networkService, config.PUBLIC_URL);