Skip to content

Give each provider scope its own message channel - #3606

Draft
reneaaron wants to merge 1 commit into
masterfrom
fix/provider-message-transport
Draft

Give each provider scope its own message channel#3606
reneaaron wants to merge 1 commit into
masterfrom
fix/provider-message-transport

Conversation

@reneaaron

Copy link
Copy Markdown
Member

Describe the changes you have made in this PR

The inpage providers and their content scripts communicated over window.postMessage on the page window. Everything sent that way — the correlation id, the arguments and the reply — is delivered to every other message listener in the frame, and the first reply carrying a matching id settles the call.

Each scope now gets its own MessageChannel. The content script keeps one end and transfers the other to the inpage world; requests, replies and events travel over that channel rather than the page window.

What changed

  • content-script/messagePortServer.js (new) creates the channel and hands the port over. It is called at module top, ahead of the asynchronous should-inject/blocklist decision, which now only gates whether requests get serviced. Requests that arrive before the scope registers a handler are buffered and flushed.
  • providers/postMessage.ts requests its port and retries until it arrives — either world can initialise first — then routes replies to the waiting caller by id.
  • A reply for an id that has already been settled is logged and dropped rather than applied to the result.
  • accountChanged now reaches the providers over the channel instead of being read off the page window.
  • The five content scripts (webln, nostr, webbtc, liquid, alby) move to the shared helper, which removes a fair amount of duplicated listener code.

Scope of the change, and what it does not do

The port handover is negotiated with window messages, so the channel is private only from scripts that were not already running when the port was transferred. On MV2 the inpage script is injected inline at document_start and that ordering holds. On MV3 the main-world script is registered separately and the ordering is not guaranteed, so a script that is already running in the frame can still take part in the handover.

Treat this as a narrowing of the shared page-window surface, not as a guarantee that provider results are authenticated to the page. The security boundary remains the extension-rendered confirmation UI, which the page cannot draw. Worth deciding separately (not in this PR):

  • whether to make provider results self-verifying where the cryptography allows it — a sendPayment preimage can be checked against the BOLT11 payment hash by the integrator
  • whether to pin the provider objects on window. Object.freeze() on the instances is not viable as-is: providerBase assigns this._isEnabled after construction and these are ES modules, so freezing would make enable() / isEnabled() throw. A non-configurable window.webln / window.nostr would prevent replacement but risks breaking coexistence with other WebLN/NIP-07 extensions.

Tests

  • providers/__tests__/postMessage.test.ts — a request goes over the port and its reply resolves the call; a reply of the old shape replayed on the page window does not resolve it and the genuine reply still does; a second reply for a settled id does not overwrite the result; errors reject; events arrive over the port and are not picked up from the window.
  • content-script/__tests__/messagePortServer.test.ts — a request over the port is serviced and answered on it, requests before a handler are buffered, the port is transferred once, another scope's handshake is ignored, events go over the port.
  • JSDOM has no MessageChannel. tests/unit/helpers/fakeMessageChannel.ts supplies a minimal stand-in, installed per test file rather than globally, because msw depends on the real one.

yarn lint, yarn tsc:compile and yarn test:unit pass (77 suites, 179 passed, 2 pre-existing skips), and yarn build:chrome succeeds.

The inpage providers and their content scripts talked to each other with
`window.postMessage` on the page window. Every message that went over it —
request ids, arguments and replies alike — was delivered to all the other
message listeners in the frame, and the first reply carrying a matching id
settled the call.

Each scope now gets a `MessageChannel`. The content script keeps one end and
transfers the other to the inpage world, and provider requests, replies and
events travel over that channel instead of the page window.

- `messagePortServer.js` creates the channel and hands over the port. It runs
  at module top, ahead of the asynchronous should-inject decision, and buffers
  requests that arrive before the scope registers its handler.
- `postMessage.ts` asks for its port, retries until it arrives (either world may
  start first) and routes replies back to the caller by id.
- replies for an id that is already settled are reported rather than applied.
- `accountChanged` is delivered over the channel instead of the page window.

The handover is negotiated with window messages, so the channel is private only
from scripts that are not yet running when the port is transferred. On MV2 the
inpage script is injected inline at document_start and that ordering holds; on
MV3 the main-world script is registered separately and it is not guaranteed.

Tests cover both sides of the transport. JSDOM has no MessageChannel, so
`tests/unit/helpers/fakeMessageChannel.ts` provides a stand-in that is installed
per test file — msw relies on the real one, so it is not replaced globally.
@coderabbitai

coderabbitai Bot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant