diff --git a/extension/src/service-worker/router/index.ts b/extension/src/service-worker/router/index.ts new file mode 100644 index 0000000..65229c3 --- /dev/null +++ b/extension/src/service-worker/router/index.ts @@ -0,0 +1,48 @@ +/// Single chrome.runtime.onMessage entry. Classifies the sender and dispatches +/// to popup-only or content-callable handlers. Unauthorized senders are +/// rejected with { ok: false, error: 'unauthorized_sender' }. + +import type { Request, Response } from '../../shared/messages'; +import { POPUP_ONLY_TYPES, CONTENT_CALLABLE_TYPES } from '../../shared/messages'; +import type { Manifest } from '../../shared/types'; +import type { GitHost } from '../git-host'; +import * as popupOnly from './popup-only'; +import * as contentCallable from './content-callable'; + +export interface RouterState { + manifest: Manifest | null; + gitHost: GitHost | null; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + wasm: any; +} + +export async function route( + msg: Request, + state: RouterState, + sender: chrome.runtime.MessageSender, +): Promise { + const popupUrl = chrome.runtime.getURL('popup.html'); + const setupUrl = chrome.runtime.getURL('setup.html'); + const senderUrl = sender.url ?? ''; + + const isPopup = senderUrl === popupUrl; + const isSetup = senderUrl.startsWith(setupUrl); + const isContent = sender.tab !== undefined + && sender.frameId === 0 + && sender.id === chrome.runtime.id; + + if (POPUP_ONLY_TYPES.has(msg.type as never)) { + // save_setup gets one exception: allowed from the setup tab too. + if (!(isPopup || (msg.type === 'save_setup' && isSetup))) { + return { ok: false, error: 'unauthorized_sender' }; + } + return popupOnly.handle(msg as never, state, sender); + } + + if (CONTENT_CALLABLE_TYPES.has(msg.type as never)) { + if (!isContent) return { ok: false, error: 'unauthorized_sender' }; + return contentCallable.handle(msg as never, state, sender); + } + + return { ok: false, error: 'unknown_message_type' }; +}