// Worker side of interactive sign-in. The OAuth client cannot run in the // service worker, so it lives in an offscreen document; this module owns what // that document cannot touch: opening the consent window, spotting the // redirect back, and cleaning up. // // Consent can take minutes and the worker can be reaped meanwhile, so no flow // state lives in worker memory: the pending window id sits in // storage.session, and the tab/window listeners are registered at the top // level so their events revive the worker. import { AUTH_ERROR_KEY, callbackFromUpdate, oauthRedirectUri, replacedSession } from './lib/authflow' import { invalidateAccount } from './lib/cache' import { ensureOffscreenDocument } from './lib/offscreen' import { getStoredSession, saveSessionMirror } from './lib/session-store' import type { OffscreenMsg, SessionInfo } from './lib/types' const PENDING_KEY = 'pendingAuth' // Enough for every PDS consent page we know of; Chrome clamps to the screen. const CONSENT_WIDTH = 480 const CONSENT_HEIGHT = 720 interface PendingAuth { windowId: number } async function getPending(): Promise { return (await chrome.storage.session.get(PENDING_KEY))[PENDING_KEY] as PendingAuth | undefined } function ensureOffscreen(): Promise { return ensureOffscreenDocument({ url: 'offscreen.html', reasons: [chrome.offscreen.Reason.LOCAL_STORAGE], justification: 'The AT Protocol OAuth client keeps its sign-in state in DOM storage', }) } async function closeOffscreen(): Promise { await chrome.offscreen.closeDocument().catch(() => { // already gone }) } async function toOffscreen(msg: OffscreenMsg): Promise { const res = await chrome.runtime.sendMessage(msg) if (res && typeof res === 'object' && '__error' in res) { throw new Error(String((res as { __error: unknown }).__error)) } return res as T } /** * Open a consent window for `handle`. Resolves once the window is up; the * rest of the flow continues in the listeners below. Rejects (into the * popup's form) if the handle can't be resolved or the PDS refuses the * authorization request. */ export async function startSignIn(handle: string): Promise { // A new attempt supersedes any window still waiting for consent. const prev = await getPending() if (prev) { await chrome.storage.session.remove(PENDING_KEY) await chrome.windows.remove(prev.windowId).catch(() => {}) } await chrome.storage.session.remove(AUTH_ERROR_KEY) await ensureOffscreen() const url = await toOffscreen({ target: 'offscreen', type: 'oauth-authorize', handle }) const win = await chrome.windows.create({ url, type: 'popup', width: CONSENT_WIDTH, height: CONSENT_HEIGHT, }) if (win.id === undefined) throw new Error('Could not open the consent window') console.debug('[substandard] consent window opened', win.id) await chrome.storage.session.set({ [PENDING_KEY]: { windowId: win.id } satisfies PendingAuth }) } chrome.tabs.onUpdated.addListener((_tabId, changeInfo, tab) => { void onConsentTabUpdated(changeInfo, tab) }) /** * Windows whose redirect is already being handled by this worker instance. * tabs.onUpdated delivers the redirect URL more than once (commit, complete); * the dedupe check must stay synchronous — an await before it lets a second * event start a second exchange, which fails on the consumed state and closes * the offscreen document under the first one. */ const claimedWindows = new Set() async function onConsentTabUpdated( changeInfo: chrome.tabs.TabChangeInfo, tab: chrome.tabs.Tab, ): Promise { const callbackUrl = callbackFromUpdate(oauthRedirectUri(chrome.runtime.id), changeInfo, tab) if (!callbackUrl || claimedWindows.has(tab.windowId)) return claimedWindows.add(tab.windowId) try { const pending = await getPending() if (!pending || tab.windowId !== pending.windowId) return // Claim the flow in storage too, so the onRemoved listener below doesn't // read the window close as a cancellation (and a restarted worker doesn't // see a stale flow). await chrome.storage.session.remove(PENDING_KEY) await chrome.windows.remove(pending.windowId).catch(() => {}) try { await ensureOffscreen() const info = await toOffscreen({ target: 'offscreen', type: 'oauth-callback', url: callbackUrl, }) // An account switch keeps the old login usable until the new one has // landed — which is now, so retire it, and drop everything cached from // its repo: the next answers must be the new account's. const replaced = replacedSession(await getStoredSession(), info) if (replaced) { await toOffscreen({ target: 'offscreen', type: 'oauth-revoke', sub: replaced }) await invalidateAccount(replaced) console.debug('[substandard] revoked replaced session', replaced) } // The offscreen document cannot persist this itself (no chrome.storage // there); the mirror write is ours. await saveSessionMirror(info) console.debug('[substandard] signed in as', info.did) } catch (err) { console.debug('[substandard] token exchange failed', err) await chrome.storage.session.set({ [AUTH_ERROR_KEY]: err instanceof Error ? err.message : String(err), }) } await closeOffscreen() } finally { claimedWindows.delete(tab.windowId) } } chrome.windows.onRemoved.addListener((windowId) => { void onConsentWindowClosed(windowId) }) async function onConsentWindowClosed(windowId: number): Promise { const pending = await getPending() if (!pending || pending.windowId !== windowId) return // User closed the window without consenting; not an error worth surfacing. console.debug('[substandard] consent window closed before redirect') await chrome.storage.session.remove(PENDING_KEY) await closeOffscreen() }