• Stars
    star
    521
  • Rank 84,952 (Top 2 %)
  • Language
    TypeScript
  • License
    MIT License
  • Created over 3 years ago
  • Updated 2 months ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

💬 Messaging in Web Extensions made easy. Batteries included.

webext-bridge

中文

Messaging in WebExtensions made super easy. Out of the box.

Example

// Inside devtools script

import { sendMessage } from "webext-bridge/devtools";

button.addEventListener("click", async () => {
  const res = await sendMessage(
    "get-selection",
    { ignoreCasing: true },
    "content-script"
  );
  console.log(res); // > "The brown fox is alive and well"
});
// Inside content script

import { sendMessage, onMessage } from "webext-bridge/content-script";

onMessage("get-selection", async (message) => {
  const {
    sender,
    data: { ignoreCasing },
  } = message;

  console.log(sender.context, sender.tabId); // > devtools  156

  const { selection } = await sendMessage(
    "get-preferences",
    { sync: false },
    "background"
  );
  return calculateSelection(data.ignoreCasing, selection);
});
// Inside background script

import { onMessage } from "webext-bridge/background";

onMessage("get-preferences", ({ data }) => {
  const { sync } = data;

  return loadUserPreferences(sync);
});

Examples above require transpilation and/or bundling using webpack/babel/rollup

webext-bridge handles everything for you as efficiently as possible. No more chrome.runtime.sendMessage or chrome.runtime.onConnect or chrome.runtime.connect ....

Setup

Install

$ npm i webext-bridge

Light it up

Just import { } from 'webext-bridge/{context}' wherever you need it and use as shown in example above

Even if your extension doesn't need a background page or wont be sending/receiving messages in background script.
webext-bridge uses background/event context as staging area for messages, therefore it must loaded in background/event page for it to work.
(Attempting to send message from any context will fail silently if webext-bridge isn't available in background page).
See troubleshooting section for more.

Type Safe Protocols

As we are likely to use sendMessage and onMessage in different contexts, keeping the type consistent could be hard, and its easy to make mistakes. webext-bridge provide a smarter way to make the type for protocols much easier.

Create shim.d.ts file with the following content and make sure it's been included in tsconfig.json.

// shim.d.ts

import { ProtocolWithReturn } from "webext-bridge";

declare module "webext-bridge" {
  export interface ProtocolMap {
    foo: { title: string };
    // to specify the return type of the message,
    // use the `ProtocolWithReturn` type wrapper
    bar: ProtocolWithReturn<CustomDataType, CustomReturnType>;
  }
}
import { onMessage } from 'webext-bridge/content-script'

onMessage('foo', ({ data }) => {
  // type of `data` will be `{ title: string }`
  console.log(data.title)
}
import { sendMessage } from "webext-bridge/background";

const returnData = await sendMessage("bar", {
  /* ... */
});
// type of `returnData` will be `CustomReturnType` as specified

API

sendMessage(messageId: string, data: any, destination: string)

Sends a message to some other part of your extension.

Notes:

  • If there is no listener on the other side an error will be thrown where sendMessage was called.

  • Listener on the other may want to reply. Get the reply by awaiting the returned Promise

  • An error thrown in listener callback (in the destination context) will behave as usual, that is, bubble up, but the same error will also be thrown where sendMessage was called

  • If the listener receives the message but the destination disconnects (tab closure for exmaple) before responding, sendMessage will throw an error in the sender context.

messageId

Required | string

Any string that both sides of your extension agree on. Could be get-flag-count or getFlagCount, as long as it's same on receiver's onMessage listener.

data

Required | any

Any serializable value you want to pass to other side, latter can access this value by refering to data property of first argument to onMessage callback function.

destination

Required | string |

The actual identifier of other endpoint. Example: devtools or content-script or background or content-script@133 or devtools@453

content-script, window and devtools destinations can be suffixed with @<tabId> to target specific tab. Example: devtools@351, points to devtools panel inspecting tab with id 351.

For content-script, a specific frameId can be specified by appending the frameId to the suffix @<tabId>.<frameId>.

Read Behavior section to see how destinations (or endpoints) are treated.

Note: For security reasons, if you want to receive or send messages to or from window context, one of your extension's content script must call allowWindowMessaging(<namespace: string>) to unlock message routing. Also call setNamespace(<namespace: string>) in those window contexts. Use same namespace string in those two calls, so webext-bridge knows which message belongs to which extension (in case multiple extensions are using webext-bridge in one page)


onMessage(messageId: string, callback: fn)

Register one and only one listener, per messageId per context. That will be called upon sendMessage from other side.

Optionally, send a response to sender by returning any value or if async a Promise.

messageId

Required | string

Any string that both sides of your extension agree on. Could be get-flag-count or getFlagCount, as long as it's same in sender's sendMessage call.

callback

Required | fn

A callback function Bridge should call when a message is received with same messageId. The callback function will be called with one argument, a BridgeMessage which has sender, data and timestamp as its properties.

Optionally, this callback can return a value or a Promise, resolved value will sent as reply to sender.

Read security note before using this.


allowWindowMessaging(namespace: string)

Caution: Dangerous action

API available only to content scripts

Unlocks the transmission of messages to and from window (top frame of loaded page) contexts in the tab where it is called. webext-bridge by default won't transmit any payload to or from window contexts for security reasons. This method can be called from a content script (in top frame of tab), which opens a gateway for messages.

Once again, window = the top frame of any tab. That means allowing window messaging without checking origin first will let JavaScript loaded at https://evil.com talk with your extension and possibly give indirect access to things you won't want to, like history API. You're expected to ensure the safety and privacy of your extension's users.

namespace

Required | string

Can be a domain name reversed like com.github.facebook.react_devtools or any uuid. Call setNamespace in window context with same value, so that webext-bridge knows which payload belongs to which extension (in case there are other extensions using webext-bridge in a tab). Make sure namespace string is unique enough to ensure no collisions happen.


setNamespace(namespace: string)

API available to scripts in top frame of loaded remote page

Sets the namespace Bridge should use when relaying messages to and from window context. In a sense, it connects the callee context to the extension which called allowWindowMessaging(<namespace: string>) in it's content script with same namespace.

namespace

Required | string

Can be a domain name reversed like com.github.facebook.react_devtools or any uuid. Call setNamespace in window context with same value, so that webext-bridge knows which payload belongs to which extension (in case there are other extensions using webext-bridge in a tab). Make sure namespace string is unique enough to ensure no collisions happen.

Extras

The following API is built on top of sendMessage and onMessage, basically, it's just a wrapper, the routing and security rules still apply the same way.

openStream(channel: string, destination: string)

Opens a Stream between caller and destination.

Returns a Promise which resolves with Stream when the destination is ready (loaded and onOpenStreamChannel callback registered). Example below illustrates a use case for Stream

channel

Required | string

Stream(s) are strictly scoped sendMessage(s). Scopes could be different features of your extension that need to talk to the other side, and those scopes are named using a channel id.

destination

Required | string

Same as destination in sendMessage(msgId, data, destination)


onOpenStreamChannel(channel: string, callback: fn)

Registers a listener for when a Stream opens. Only one listener per channel per context

channel

Required | string

Stream(s) are strictly scoped sendMessage(s). Scopes could be different features of your extension that need to talk to the other side, and those scopes are named using a channel id.

callback

Required | fn

Callback that should be called whenever Stream is opened from the other side. Callback will be called with one argument, the Stream object, documented below.

Stream(s) can be opened by a malicious webpage(s) if your extension's content script in that tab has called allowWindowMessaging, if working with sensitive information use isInternalEndpoint(stream.info.endpoint) to check, if false call stream.close() immediately.

Stream Example
// background.js

// To-Do

Behavior

Following rules apply to destination being specified in sendMessage(msgId, data, destination) and openStream(channelId, initialData, destination)

  • Specifying devtools as destination from content-script will auto-route payload to inspecting devtools page if open and listening. If devtools are not open, message will be queued up and delivered when devtools are opened and the user switches to your extension's devtools panel.

  • Specifying content-script as destination from devtools will auto-route the message to inspected window's top content-script page if listening. If page is loading, message will be queued up and delivered when page is ready and listening.

  • If window context (which could be a script injected by content script) are source or destination of any payload, transmission must be first unlocked by calling allowWindowMessaging(<namespace: string>) inside that page's top content script, since Bridge will first deliver the payload to content-script using rules above, and latter will take over and forward accordingly. content-script <-> window messaging happens using window.postMessage API. Therefore to avoid conflicts, Bridge requires you to call setNamespace(uuidOrReverseDomain) inside the said window script (injected or remote, doesn't matter).

  • Specifying devtools or content-script or window from background will throw an error. When calling from background, destination must be suffixed with tab id. Like devtools@745 for devtools inspecting tab id 745 or content-script@351 for top content-script at tab id 351.

Serious security note

The following note only applies if and only if, you will be sending/receiving messages to/from window contexts. There's no security concern if you will be only working with content-script, background, popup, options, or devtools scope, which is the default setting.

window context(s) in tab A get unlocked the moment you call allowWindowMessaging(namespace) somewhere in your extension's content script(s) that's also loaded in tab A.

Unlike chrome.runtime.sendMessage and chrome.runtime.connect, which requires extension's manifest to specify sites allowed to talk with the extension, webext-bridge has no such measure by design, which means any webpage whether you intended or not, can do sendMessage(msgId, data, 'background') or something similar that produces same effect, as long as it uses same protocol used by webext-bridge and namespace set to same as yours.

So to be safe, if you will be interacting with window contexts, treat webext-bridge as you would treat window.postMessage API.

Before you call allowWindowMessaging, check if that page's window.location.origin is something you expect already.

As an example if you plan on having something critical, always verify the sender before responding:

// background.js

import { onMessage, isInternalEndpoint } from "webext-bridge/background";

onMessage("getUserBrowsingHistory", (message) => {
  const { data, sender } = message;
  // Respond only if request is from 'devtools', 'content-script', 'popup', 'options', or 'background' endpoint
  if (isInternalEndpoint(sender)) {
    const { range } = data;
    return getHistory(range);
  }
});

Troubleshooting

  • Doesn't work?
    If window contexts are not part of the puzzle, webext-bridge works out of the box for messaging between devtools <-> background <-> content-script(s). If even that is not working, it's likely that webext-bridge hasn't been loaded in background page of your extension, which is used by webext-bridge as a relay. If you don't need a background page for yourself, here's bare minimum to get webext-bridge going.
// background.js (requires transpiration/bundling using webpack(recommended))

import "webext-bridge/background";
// manifest.json

{
  "background": {
    "scripts": ["path/to/transpiled/background.js"]
  }
}
  • Can't send messages to window?
    Sending or receiving messages from or to window requires you to open the messaging gateway in content script(s) for that particular tab. Call allowWindowMessaging(<namespaceA: string>) in any of your content script(s) in that tab and call setNamespace(<namespaceB: string>) in the script loaded in top frame i.e the window context. Make sure that namespaceA === namespaceB. If you're doing this, read the security note above

More Repositories

1

amplitudejs

AmplitudeJS: Open Source HTML5 Web Audio Library. Design your web audio player, the way you want. No dependencies required.
JavaScript
4,035
star
2

financial-freedom

🔥🔥🔥 An open source alternative to Mint, YNAB, and more. Stay on budget and build wealth without sacrificing your privacy.
Vue
1,866
star
3

docker-php

🐳 Production-ready Docker images for PHP. Optimized for Laravel, WordPress, and more!
Shell
1,647
star
4

roastandbrew

Updated content available! We learned a lot since we originally wrote this article. We now have this updated for Laravel 8, Vue, and NuxtJS 👉 https://srvrsi.de/book
Vue
301
star
5

spin

Replicate your production environment locally using Docker. Just run "spin up". It's really that easy.
Shell
96
star
6

uploading-files-vuejs-axios

Contains all of the example components to upload files with VueJS and Axios.
Vue
48
star
7

docker-ssh

Simple SSH container. Great for secure connections into clusters.
Dockerfile
37
star
8

ansible-vault-automator

Easily encrypt, edit, and decrypt files through Finder (rather than command line). When you choose to edit an encrypted file, you can edit it directly in Sublime Text 3 instead of nano or vim.
Shell
21
star
9

ansible-postal

⚡️ Deploy a secure Postal server in under 6 minutes using Ansible.
Shell
18
star
10

docker-wordpress

Learn how to run WordPress with Docker. Read about our experiences and start off with an easy boilerplate.
PHP
17
star
11

s6-overlay

This project serves as a base image for other containers. It includes Ubuntu with S6 overlay.
Dockerfile
16
star
12

airlock-nuxt-frontend

NuxtJS implementation with NuxtJS auth module to work with Laravel Airlock API. The Laravel API is here: https://github.com/serversideup/airlock-laravel-api
Vue
14
star
13

self-hosted-email-guide

This guide makes self-hosting email easy. Maintain control and protect your privacy.
Vue
12
star
14

wp-dev-recipes

Modular, reusable code snippets for all WordPress devs.
Vue
11
star
15

airlock-laravel-api

Updated content available! Check out our book on how to authenticate securely 👉 https://srvrsi.de/book
PHP
9
star
16

fetch-api-vuejs

Examples on handling API requests with the Fetch API and VueJS
Vue
8
star
17

ansible-role-linux-common

A simple playbook to secure your server, prep your users, and prepare your server for other uses.
Shell
6
star
18

ipaddress-fyi

Easily control access to the Internet’s most popular services
JavaScript
6
star
19

amplitudejs-player-examples

HTML
6
star
20

wp-sandbox

A Wordpress Coming soon page for developers that does not interfere with your testing.
PHP
6
star
21

github-action-docker-build

Build and publish docker images automatically with GitHub Actions ⚡️
4
star
22

laravel-auth-api

API Boilerplate for SPA authentication using Laravel Sanctum and NuxtJS
PHP
3
star
23

spin-site

JavaScript
2
star
24

github-action-docker-swarm-deploy

2
star
25

spin-laravel-example

PHP
2
star
26

media-temple-amazon-s3-backup-for-dedicated-virtu

A GIT book that is a comprehensive guide to installing, configuring, and automating Media Temple backups to Amazon S3.
JavaScript
2
star
27

vue-3-vuex-4-queue

Client side queue example using Vue 3 and Vuex 4.
Vue
1
star
28

tailwindcss-mobile-utilities

Quickly give your app a native feel using Tailwind Utility Classes.
1
star
29

laravel-auth-frontend

The NuxtJS Frontend Boilerplate for Authentication
Vue
1
star
30

amplitudejs-site

Vue
1
star
31

docker-utility

A super small, but super powerful docker image for CI processes.
Dockerfile
1
star
32

ansible-role-swarm

1
star