import type { MessageType } from "../shared/types"; export type MessageContext = | "extension-page-only" | "content-script-allowed" | "background-internal" | "page-relay"; export interface MessageSenderShape { id?: string; url?: string; } /** * Every message type has an explicit trust boundary. Keep this record * exhaustive: adding a MessageType without choosing its sender context must * fail type-checking instead of silently becoming trusted. */ export const MESSAGE_CONTEXT_POLICY = { // Popup/options operations and sensitive reads/writes. GET_SYNC_ALERTS: "extension-page-only", GET_PENDING_VOUCHED_DEVICES: "extension-page-only", CONFIRM_VOUCHED_DEVICE: "extension-page-only", REJECT_VOUCHED_DEVICE: "extension-page-only", ROTATE_SYNC_IDENTITY: "extension-page-only", UNLOCK: "extension-page-only", UNLOCK_WITH_PIN: "extension-page-only", GET_PIN_STATUS: "extension-page-only", LOCK: "extension-page-only", IS_UNLOCKED: "extension-page-only", HAS_VAULT: "extension-page-only", GET_ENTRIES: "extension-page-only", BATCH_ADD_ENTRIES: "extension-page-only", RESTORE_ENTRIES: "extension-page-only", CLEAR_VAULT_ENTRIES: "extension-page-only", DELETE_ENTRY: "extension-page-only", CREATE_VAULT: "extension-page-only", CREATE_VAULT_FROM_ROOT_KEY: "extension-page-only", CHECK_ALL_BREACHES: "extension-page-only", UPDATE_SETTINGS: "extension-page-only", SETUP_PIN: "extension-page-only", DISABLE_PIN: "extension-page-only", ACTIVATE_LICENSE: "extension-page-only", GET_LICENSE: "extension-page-only", GET_LICENSE_KEY: "extension-page-only", IS_DURESS_ENABLED: "extension-page-only", SETUP_DURESS: "extension-page-only", DISABLE_DURESS: "extension-page-only", VERIFY_MAIN_PASSWORD: "extension-page-only", CHANGE_PASSWORD: "extension-page-only", GET_LOCAL_IDENTITY: "extension-page-only", LIST_PAIRED_DEVICES: "extension-page-only", RENAME_PAIRED_DEVICE: "extension-page-only", UNPAIR_DEVICE: "extension-page-only", RENAME_LOCAL_DEVICE: "extension-page-only", START_ADOPTION_SOURCE: "extension-page-only", START_ADOPTION_RECEIVER: "extension-page-only", CANCEL_ADOPTION_SOURCE: "extension-page-only", SOURCE_SAS_DECISION: "extension-page-only", TRIGGER_SYNC: "extension-page-only", GET_SYNC_STATE: "extension-page-only", GET_SYNC_SETTINGS: "extension-page-only", SET_SYNC_SETTINGS: "extension-page-only", GET_PENDING_PASSKEY: "extension-page-only", APPROVE_PASSKEY: "extension-page-only", DENY_PASSKEY: "extension-page-only", GET_PASSKEY_PROVIDER_STATE: "extension-page-only", SET_PASSKEY_PROVIDER_ENABLED: "extension-page-only", GET_DELETED_ENTRIES: "extension-page-only", RESTORE_ENTRY: "extension-page-only", PURGE_ENTRY: "extension-page-only", PURGE_EXPIRED_TRASH: "extension-page-only", GET_VAULT_HISTORY: "extension-page-only", RESTORE_VAULT_SNAPSHOT: "extension-page-only", COUNT_DUPLICATES: "extension-page-only", DEDUPLICATE_ENTRIES: "extension-page-only", GET_VAULT_STATUS: "extension-page-only", GET_ADVANCED_HEALTH_REPORT: "extension-page-only", DEACTIVATE_LICENSE: "extension-page-only", GET_IDENTITIES: "content-script-allowed", DESTROY_VAULT: "extension-page-only", COMPLETE_FIREFOX_PASSKEY_CREATE: "extension-page-only", VALIDATE_RECOVERY_WORDS: "extension-page-only", RECOVER_WITH_WORDS: "extension-page-only", GET_RECOVERY_WORDS: "extension-page-only", GET_RECOVERY_CONFIRM_PENDING: "extension-page-only", SET_RECOVERY_CONFIRM_PENDING: "extension-page-only", CLEAR_RECOVERY_CONFIRM_PENDING: "extension-page-only", GET_IDENTITY_SUGGESTIONS: "extension-page-only", SAVE_NOTE: "extension-page-only", DELETE_NOTE: "extension-page-only", GET_NOTES: "extension-page-only", SEARCH_VAULT: "extension-page-only", GET_CODEX_VAULT_DATA: "extension-page-only", ESTIMATE_CODEX_PAGES: "extension-page-only", GENERATE_CODEX: "extension-page-only", RESTORE_FROM_CODEX: "extension-page-only", OPEN_DETACHED_POPUP: "extension-page-only", // Content-script requests that are intentionally narrow or origin-bound in // their handlers. These are not bulk vault export or device-authority APIs. ADD_ENTRY: "content-script-allowed", UPDATE_ENTRY: "content-script-allowed", ADD_ADDITIONAL_URL: "content-script-allowed", AUTOFILL_QUERY: "content-script-allowed", COPY: "content-script-allowed", FIRE_CANARY_PING: "content-script-allowed", GET_ENTRY_FOR_FILL: "content-script-allowed", GET_ENTRY_FOR_EDIT: "content-script-allowed", GET_SETTINGS: "content-script-allowed", GET_SITE_PASSWORD_POLICY: "content-script-allowed", RECORD_MANUAL_SUMMON: "content-script-allowed", DECLINE_CARD_SAVE: "content-script-allowed", IS_CARD_DECLINED: "content-script-allowed", PASSKEY_AUTH_RESPONSE: "content-script-allowed", ML_INIT: "content-script-allowed", ML_PREDICT_BATCH: "content-script-allowed", OPEN_PRO_GATE: "content-script-allowed", OPEN_POPUP: "content-script-allowed", TOUCH_AUTO_LOCK: "content-script-allowed", // These seven are the explicit Firefox MAIN-world passkey relay surface. INITIATE_FIREFOX_PASSKEY_CREATE: "page-relay", PASSKEY_OFFER_DECLINE: "page-relay", GET_PASSKEY_OFFER_DECLINED: "page-relay", GET_PASSKEY_FOR_DOMAIN: "page-relay", PASSKEY_HAS_CREDENTIALS: "page-relay", PASSKEY_CREATE: "page-relay", PASSKEY_GET: "page-relay", // Messages produced by the background or handled outside messages.ts. CLEAR_CLIPBOARD: "background-internal", VOUCHED_DEVICE_PENDING: "background-internal", DEVICE_KEY_ROTATED: "background-internal", ADOPTION_SOURCE_SAS: "background-internal", ADOPTION_SOURCE_RESULT: "background-internal", PING: "background-internal", SUMMON_AUTOFILL: "background-internal", VAULT_LOCKED: "background-internal", VAULT_UNLOCKED: "background-internal", PRO_GATE_TRIGGERED: "background-internal", PASSKEY_PRO_GATE: "background-internal", SHOW_PASSKEY_TOAST: "background-internal", DISMISS_PASSKEY_TOAST: "background-internal", SHOULD_PLAY_CELEBRATION: "extension-page-only", MARK_CELEBRATION_PLAYED: "extension-page-only", } satisfies Record; export function getMessageContext(type: unknown): MessageContext | undefined { if (typeof type !== "string") return undefined; return Object.hasOwn(MESSAGE_CONTEXT_POLICY, type) ? MESSAGE_CONTEXT_POLICY[type as MessageType] : undefined; } export function isMessageSenderAllowed( type: unknown, sender: MessageSenderShape, extensionId: string | undefined, extensionOrigin: string, ): boolean { if (sender.id !== undefined && sender.id !== extensionId) return false; const context = getMessageContext(type); if (context === "background-internal" || context === undefined) return false; if (context === "extension-page-only") { if (!sender.url) return true; try { return new URL(sender.url).origin === new URL(extensionOrigin).origin; } catch { return false; } } if (context === "page-relay") { if (!sender.url) return false; try { return new URL(sender.url).origin !== new URL(extensionOrigin).origin; } catch { return false; } } return true; }