feat(chrome-ext): native chrome.runtime.sendMessage cross-origin polyfill
Replaces the patch-the-extension-files approach (reverted in @-) with a
proper chrome-api-polyfill module that bridges page → main → BG service
worker for cross-origin chrome.runtime.sendMessage(extId, msg) calls
issued by pages whose origin matches an extension's
externally_connectable.matches.
Electron 40 partially implements chrome.runtime for externally-connectable
origins — the page sees chrome.runtime exist, but cross-origin sendMessage
does not actually round-trip to onMessageExternal listeners in the BG SW.
Pages like account.proton.me/auth-ext (Proton Pass OAuth fork callback)
rely on this round-trip to deliver auth tokens to the extension and
silently fail without a polyfill.
Architecture (page → BG):
page MAIN world
│ chrome.runtime.sendMessage(extId, msg, cb) ◄── polyfilled by preload
│ via window.postMessage
▼
webview preload (runtime-external-preload.cjs)
│ ipcRenderer.invoke('peek:runtime-external:send', payload)
▼
main process (runtime-external.js setupMainProcess)
│ session.serviceWorkers.send(versionId, channel, payload)
▼
BG service worker (getBgInjection() prepended to background.js)
│ fan-fires registered chrome.runtime.onMessageExternal listeners
│ with synthetic sender; routes sendResponse → globalThis.postMessage
▼ ('ipc-message' event on session.serviceWorkers)
main resolves the pending Promise → preload → page polyfill
Wiring:
- chrome-api-polyfills/runtime-external.js: setupMainProcess (IPC handler
+ serviceWorkers IPC), getBgInjection (SW shim source), getPreloadPath,
getPagePolyfillSource(extId) — extId-templated MAIN-world polyfill.
- chrome-api-polyfills/runtime-external-preload.cjs: webview preload that
injects the MAIN-world chrome.runtime polyfill via webFrame.executeJavaScript
and bridges window.postMessage ↔ ipcRenderer.invoke. Resolves the matching
extension ID at preload time from --peek-runtime-external-ext-id=
passed in webPreferences.additionalArguments.
- chrome-api-polyfills/index.js: registered runtimeExternal in registerAll.
- chrome-extensions.ts: findExtensionByExternallyConnectableUrl returns the
Electron-assigned Chrome runtime ID hash (loaded.electronExtension.id),
not the unpacked-extension directory name — Proton's bundle calls
sendMessage(chrome.runtime.id, msg) and expects the runtime hash.
- entry.ts will-attach-webview: for http(s) URLs that match a loaded
extension's externally_connectable.matches, sets webPreferences.preload
to the runtime-external preload, sandbox=false (works around an Electron
sandbox-bundle init crash on subsequent navigations within Proton's auth
flow), and additionalArguments with --peek-runtime-external-ext-id=<hash>.
- entry.ts did-navigate + did-navigate-in-page: re-inject the MAIN-world
polyfill via contents.executeJavaScript on every full and SPA-style
same-document navigation. Webview preloads run only once per webContents,
but Proton's auth flow uses history-API navigations between /authorize,
/reauth, /pass, /auth-ext within the same webContents — without
re-injection the polyfill is only active on the first page.
- runtime-external page polyfill exposes chrome.runtime.id, .getURL,
.getManifest in addition to .sendMessage so the Mozilla
webextension-polyfill (used inside Proton's bundle) sees a complete shape.
- scripts/patch-chrome-extensions.js: imports getBgInjection() and appends
it to background.js alongside the other BG-side shims (permissions,
storage.session, runtime.getBackgroundPage). Source of truth lives in
the polyfill module; the patcher just delivers it.
Verification:
- Build passes (yarn build). Patcher runs idempotently.
- Smoke test (tests/desktop/runtime-external-polyfill.spec.ts): negative
test asserts non-matching URLs don't get the polyfill installed.
- End-to-end Proton auth flow exercised in dev (DEBUG=1 PROFILE=isolated
yarn start): the polyfill installs on /authorize → /reauth → /pass →
/auth-ext (verified via console-message forwarding); chrome.runtime.id,
.sendMessage, etc. read correctly by the page (verified via diagnostic
Proxy reverted before commit). Proton's auth-ext page does NOT invoke
the polyfilled sendMessage in this flow — its content_script orchestrator
fails earlier on chrome.runtime communication to its BG service worker
("Could not establish connection. Receiving end does not exist."). That
is a separate Electron extension-messaging issue, not a polyfill bug.
What's kept from the patcher (not yet replaced by native polyfills):
- PERMISSIONS_SHIM (BG-side chrome.permissions + browser.permissions
install + chrome accessor-lock)
- STORAGE_SESSION_SHIM (chrome.storage.session in-memory polyfill)
- GET_BACKGROUND_PAGE_SHIM
- peek-permissions.js (popup-side chrome.permissions + chrome.tabs.create
polyfills, runs via <script src> from each extension HTML entry)
These cover gaps the existing chrome-api-polyfills/ module doesn't yet
have native equivalents for; replacing them is the larger task tracked
separately (see Peek MCP item: "native chrome ext APIs: replace
patch-extension hacks with electron-side polyfills").