/// 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 { PopupMessage, 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; } /// Popup-only messages the setup tab is also allowed to send. The setup page /// (setup.html) is an extension-internal page — same trust level as the popup — /// so this set mirrors exactly what the wizard sends, no more. /// - save_setup: wires vault config + image into chrome.storage.local at end of init. /// - rate_passphrase: drives the zxcvbn strength meter during passphrase entry. /// - is_unlocked: setup step-4 pings the extension to detect "save config to extension" availability. /// - create_vault / attach_vault: the "name this device" step performs the full vault create/attach. /// - generate_recovery_qr: the done-step recovery-QR banner generates a QR before the user leaves. const SETUP_ALLOWED: ReadonlySet = new Set([ 'save_setup', 'rate_passphrase', 'is_unlocked', 'create_vault', 'attach_vault', 'generate_recovery_qr', ]); export async function route( msg: Request, state: RouterState, sender: chrome.runtime.MessageSender, ): Promise { const popupUrl = chrome.runtime.getURL('popup.html'); const vaultUrl = chrome.runtime.getURL('vault.html'); const setupUrl = chrome.runtime.getURL('setup.html'); const senderUrl = sender.url ?? ''; const isPopup = senderUrl.startsWith(popupUrl) || senderUrl.startsWith(vaultUrl); 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)) { if (!(isPopup || (isSetup && SETUP_ALLOWED.has(msg.type as PopupMessage['type'])))) { // eslint-disable-next-line no-console console.warn('[relicario router] rejected popup-only message from wrong sender', { type: msg.type, senderUrl, isPopup, isSetup, isContent, }); 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) { // eslint-disable-next-line no-console console.warn('[relicario router] rejected content-only message from wrong sender', { type: msg.type, senderUrl, isPopup, isSetup, isContent, frameId: sender.frameId, senderId: sender.id, }); return { ok: false, error: 'unauthorized_sender' }; } return contentCallable.handle(msg as never, state, sender); } // eslint-disable-next-line no-console console.warn('[relicario router] unknown message type', { type: (msg as { type: string }).type }); return { ok: false, error: 'unknown_message_type' }; }