Skip to content

feat(web-extension-bridge): adds @webex/web-extension-bridge - #5162

Open
rarajes2 wants to merge 2 commits into
webex:nextfrom
rarajes2:web-extension-bridge
Open

feat(web-extension-bridge): adds @webex/web-extension-bridge#5162
rarajes2 wants to merge 2 commits into
webex:nextfrom
rarajes2:web-extension-bridge

Conversation

@rarajes2

@rarajes2 rarajes2 commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

COMPLETES #< CAI-8448 >

This pull request addresses

Adds @webex/web-extension-bridge, a new TypeScript workspace package that lets a web
application and a Chrome Manifest V3 extension exchange typed, validated messages over a
hardened window.postMessage + chrome.runtime path. Implements M1–M4 of the intake spec
(added here as WEB-EXTENSION-BRIDGE-INTAKE-SPEC.md).

by making the following changes

What it provides

Four subpath exports, one per execution context, so a web build never pulls in extension code:

Import Context Surface
@webex/web-extension-bridge/web page createWebBridge — publish, request handlers, connection events
@webex/web-extension-bridge/extension/content content script relay only, no product logic
@webex/web-extension-bridge/extension/background MV3 service worker createExtensionBridge — subscribe, request, connections, buffer
@webex/web-extension-bridge/extension/client popup / options / side panel createExtensionClient, proxies to the worker

Core capabilities: fire-and-forget push from page to extension (buffered while the popup is
closed), on-demand pull from extension to page with timeouts and abort, connection lifecycle
events, per-topic rate limiting, and counters for observability.

Security posture

  • Strict origin allow-listing on both sides; sender verification in the worker
    (sender.id, sender.tab, sender.origin) rather than relying on manifest matches.
  • Envelope validation with a fixed schema, size caps, replay protection (LRU + TTL) and
    session binding; prototype-pollution-safe parsing.
  • Coded, redacted errors — no stack traces, handler messages or payloads cross the boundary,
    and no log field can carry a payload or session token.
  • Least-privilege manifest (storage only), no web_accessible_resources, content script
    pre-bundled into a single classic script.
  • Threat model T1–T14 has one-to-one regression tests in test/unit/spec/security/threats.ts.

Samples

docs/samples/web-extension-bridge (web app) and docs/samples/web-extension-bridge-extension
(MV3 extension), built with esbuild into git-ignored vendor/ directories. The extension
manifest is generated from a template so the allowed origin is configurable and carries a
"local testing only" banner. See the README for the click-path.

Tests

575 unit tests (mocha/sinon/chai via legacy-tools) covering core protocol, web and extension
adapters, an integration suite that wires both halves through fake browser seams, and the
security regression suite. Lint and typecheck are clean.

Not in this PR

Playwright end-to-end tests, the CI workflow, and release/SBOM tooling are deferred.

Test plan

  • yarn workspace @webex/web-extension-bridge test (lint + 575 unit tests)
  • yarn workspace @webex/web-extension-bridge build:src
  • yarn workspace @webex/web-extension-bridge build:samples, load the unpacked extension
    and walk the README click-path: handshake, push, pull, HANDLER_ERROR, TIMEOUT,
    NO_HANDLER

Change Type

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update
  • Tooling change
  • Internal code refactor

The following scenarios were tested

Vidcast - https://app.vidcast.io/share/69f00dd6-68be-4d85-b0ff-dd476c171f83

The GAI Coding Policy And Copyright Annotation Best Practices

  • GAI was not used (or, no additional notation is required)
  • Code was generated entirely by GAI
  • GAI was used to create a draft that was subsequently customized or modified
  • Coder created a draft manually that was non-substantively modified by GAI (e.g., refactoring was performed by GAI on manually written code)
  • Tool used for AI assistance (GitHub Copilot / Other - specify)
    • Github Copilot
    • Other - Cursor (Opus 5 High)
  • This PR is related to
    • Feature
    • Defect fix
    • Tech Debt
    • Automation

I certified that

  • I have read and followed contributing guidelines
  • I discussed changes with code owners prior to submitting this pull request
  • I have not skipped any automated checks
  • All existing and new tests passed
  • I have updated the documentation accordingly

Make sure to have followed the contributing guidelines before submitting.

@rarajes2
rarajes2 requested a review from a team as a code owner August 12, 2026 03:48

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b5c20beb11

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +168 to +172
const value = await send({
__webexBridgeClient: true,
channel,
command: ClientCommand.GET_BUFFERED,
...(typeof opts.limit === 'number' ? {limit: opts.limit} : {}),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Filter buffered messages in the worker before limiting

When a popup calls getBufferedMessages({topic, limit}), this command sends only limit, so the worker returns the newest N messages across all topics; the client then filters that truncated list locally. If the matching topic has older entries behind newer other-topic pushes, the API returns too few or none even though createExtensionBridge().getBufferedMessages({topic, limit}) would return them. Please pass and handle topic in the worker before applying the limit.

Useful? React with 👍 / 👎.

Comment on lines +52 to +53
try {
serialised = JSON.stringify(payload) as string;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject nested non-JSON values before sending

For payloads like {a: () => {}}, {a: undefined}, or NaN, JSON.stringify either drops or converts the value and still returns a string, but the bridge continues sending the original object. On the page postMessage path nested functions throw a DataCloneError instead of the documented BridgeError, and values such as NaN/undefined can reach handlers despite the JsonValue contract. Please recursively validate JSON value types rather than treating any stringify result as acceptance.

Useful? React with 👍 / 👎.

Comment on lines +407 to +409
void Promise.resolve(tabsApi.sendMessage(tabId, relayRequest)).then(
(response) => settleFromRelay(id, topic, connection.session, response),
() => pending.reject(id, new BridgeError('NOT_CONNECTED', undefined, topic))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Drop dead tab connections when sendMessage fails

When a tab still exists in session storage but the content script is gone, such as after an extension reload, discarded tab, or injection failure, chrome.tabs.sendMessage rejects and the request returns NOT_CONNECTED, but the connection record remains. listConnections() and default active-tab targeting then keep advertising the dead tab until a navigation/removal event happens. Please drop the connection on this rejection before or while rejecting the pending request.

Useful? React with 👍 / 👎.

Comment on lines +56 to +57
if (typeof node !== 'object' || node === null || depth > MAX_WALK_DEPTH) {
return undefined;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject over-deep payloads instead of skipping checks

For a payload nested more than 64 levels deep, this branch returns undefined, which is the same value used for “no reserved key found”. A malicious or malformed payload can put __proto__/constructor below that depth and bypass the reserved-key rejection even though the rest of the code treats those keys as never allowed anywhere in a payload. Please make depth overflow a rejection instead of a clean result.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 224c04f193

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +51 to +52
this.evictIfFull();
this.buckets.set(key, {tokens: this.perSecond - 1, updatedAt: at});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Add an aggregate cap before admitting new topic buckets

When an allow-listed page cycles through more than maxKeys unique topics, every push takes this new-key path, evicts an existing bucket, and receives a fresh full token budget. Because both the content relay and background worker key this limiter by topic, such a page can bypass rate limiting entirely and queue runtime messages and storage writes as fast as it can generate topic names; retain an aggregate per-tab/global bucket when bounding the per-topic map.

Useful? React with 👍 / 👎.

Comment on lines +267 to +269
case RelayKind.DISCONNECT:
logger.debug('tab detached', {channel, tabId, reason: relay.reason});
dropConnection(tabId, relay.reason ?? 'bye');

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Match disconnects to the stored session

If a relay from session A sends a delayed DISCONNECT after the same tab has already connected with session B, this unconditional tab-level removal deletes B's connection and settles B's in-flight requests. Since the disconnect message carries its session token and connections are replaced solely by tabId, remove the record only when its stored session still matches the disconnecting relay.

Useful? React with 👍 / 👎.

@rarajes2 rarajes2 changed the title feat(web-extension-bridge): initial commit - WIP feat(web-extension-bridge): adds @webex/web-extension-bridge Aug 17, 2026
@rarajes2 rarajes2 added the validated If the pull request is validated for automation. label Aug 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

validated If the pull request is validated for automation.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant