aboutsummaryrefslogtreecommitdiff
path: root/extension/src/webext-bridge
diff options
context:
space:
mode:
Diffstat (limited to 'extension/src/webext-bridge')
-rw-r--r--extension/src/webext-bridge/LICENSE.txt21
-rw-r--r--extension/src/webext-bridge/README.md38
-rw-r--r--extension/src/webext-bridge/index.ts4
-rw-r--r--extension/src/webext-bridge/internal/connection-args.ts31
-rw-r--r--extension/src/webext-bridge/internal/delivery-logger.ts28
-rw-r--r--extension/src/webext-bridge/internal/endpoint-fingerprint.ts5
-rw-r--r--extension/src/webext-bridge/internal/endpoint-runtime.ts187
-rw-r--r--extension/src/webext-bridge/internal/endpoint.ts20
-rw-r--r--extension/src/webext-bridge/internal/is-internal-endpoint.ts5
-rw-r--r--extension/src/webext-bridge/internal/message-port.ts52
-rw-r--r--extension/src/webext-bridge/internal/persistent-port.ts126
-rw-r--r--extension/src/webext-bridge/internal/port-message.ts48
-rw-r--r--extension/src/webext-bridge/internal/post-message.ts52
-rw-r--r--extension/src/webext-bridge/internal/stream.ts179
-rw-r--r--extension/src/webext-bridge/internal/types.ts6
-rw-r--r--extension/src/webext-bridge/ports.ts391
-rw-r--r--extension/src/webext-bridge/types.ts100
17 files changed, 0 insertions, 1293 deletions
diff --git a/extension/src/webext-bridge/LICENSE.txt b/extension/src/webext-bridge/LICENSE.txt
deleted file mode 100644
index b3236f3..0000000
--- a/extension/src/webext-bridge/LICENSE.txt
+++ /dev/null
@@ -1,21 +0,0 @@
-MIT License
-
-Copyright (c) 2017 Neek Sandhu
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE. \ No newline at end of file
diff --git a/extension/src/webext-bridge/README.md b/extension/src/webext-bridge/README.md
deleted file mode 100644
index de14bac..0000000
--- a/extension/src/webext-bridge/README.md
+++ /dev/null
@@ -1,38 +0,0 @@
-# zikaari/webext-bridge
-
-The source code in this directory/subdirectory is mostly not my own. The original project can be found here [webext-bridge](https://github.com/zikaari/webext-bridge).
-
-Because of bundling [issues](https://github.com/zikaari/webext-bridge/issues/69), I had to copy the source code here and make some changes to it. The original project is licensed under the MIT license.
-
-## License
-As per AGPLv3 the original license and copyright can be found in this directory as this ia a dirived work.
-
-This file is part of NVault, licensed under AGPLv3.
-NVault is a derivative work of webext-bridge, which is licensed under the MIT license.
-
-The MIT License (MIT)
-
-Copyright (c) 2017 Neek Sandhu
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
-
-### Changes
- - Removed all npm package related files
- - Changed port creation and local state to be function scoped
- - removed files for file-based-exports such as options.ts and popup.ts
diff --git a/extension/src/webext-bridge/index.ts b/extension/src/webext-bridge/index.ts
deleted file mode 100644
index 6e87f07..0000000
--- a/extension/src/webext-bridge/index.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-export * from './types'
-export { isInternalEndpoint } from './internal/is-internal-endpoint'
-export { parseEndpoint } from './internal/endpoint'
-export { createPort, createBackgroundPort } from './ports' \ No newline at end of file
diff --git a/extension/src/webext-bridge/internal/connection-args.ts b/extension/src/webext-bridge/internal/connection-args.ts
deleted file mode 100644
index 9b93e19..0000000
--- a/extension/src/webext-bridge/internal/connection-args.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-import type { EndpointFingerprint } from './endpoint-fingerprint'
-
-export interface ConnectionArgs {
- endpointName: string
- fingerprint: EndpointFingerprint
-}
-
-const isValidConnectionArgs = (
- args: unknown,
- requiredKeys: (keyof ConnectionArgs)[] = ['endpointName', 'fingerprint'],
-): args is ConnectionArgs =>
- typeof args === 'object'
- && args !== null
- && requiredKeys.every(k => k in args)
-
-export const encodeConnectionArgs = (args: ConnectionArgs) => {
- if (!isValidConnectionArgs(args))
- throw new TypeError('Invalid connection args')
-
- return JSON.stringify(args)
-}
-
-export const decodeConnectionArgs = (encodedArgs: string): ConnectionArgs => {
- try {
- const args = JSON.parse(encodedArgs)
- return isValidConnectionArgs(args) ? args : null
- }
- catch (error) {
- return null
- }
-}
diff --git a/extension/src/webext-bridge/internal/delivery-logger.ts b/extension/src/webext-bridge/internal/delivery-logger.ts
deleted file mode 100644
index 395f035..0000000
--- a/extension/src/webext-bridge/internal/delivery-logger.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-import type { InternalMessage } from '../types'
-import type { EndpointFingerprint } from './endpoint-fingerprint'
-
-export interface DeliveryReceipt {
- message: InternalMessage
- to: EndpointFingerprint
- from: {
- endpointId: string
- fingerprint: EndpointFingerprint
- }
-}
-
-export const createDeliveryLogger = () => {
- let logs: ReadonlyArray<DeliveryReceipt> = []
-
- return {
- add: (...receipts: DeliveryReceipt[]) => {
- logs = [...logs, ...receipts]
- },
- remove: (message: string | DeliveryReceipt[]) => {
- logs
- = typeof message === 'string'
- ? logs.filter(receipt => receipt.message.transactionId !== message)
- : logs.filter(receipt => !message.includes(receipt))
- },
- entries: () => logs,
- }
-}
diff --git a/extension/src/webext-bridge/internal/endpoint-fingerprint.ts b/extension/src/webext-bridge/internal/endpoint-fingerprint.ts
deleted file mode 100644
index fe3cc24..0000000
--- a/extension/src/webext-bridge/internal/endpoint-fingerprint.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-import uid from 'tiny-uid'
-
-export type EndpointFingerprint = `uid::${string}`
-
-export const createFingerprint = (): EndpointFingerprint => `uid::${uid(7)}`
diff --git a/extension/src/webext-bridge/internal/endpoint-runtime.ts b/extension/src/webext-bridge/internal/endpoint-runtime.ts
deleted file mode 100644
index 67b4fe0..0000000
--- a/extension/src/webext-bridge/internal/endpoint-runtime.ts
+++ /dev/null
@@ -1,187 +0,0 @@
-import type { JsonValue } from 'type-fest'
-import uuid from 'tiny-uid'
-import { serializeError } from 'serialize-error'
-import type {
- BridgeMessage,
- DataTypeKey,
- Destination,
- GetDataType,
- GetReturnType,
- InternalMessage,
- OnMessageCallback,
- RuntimeContext,
-} from '../types'
-import { parseEndpoint } from './endpoint'
-
-export interface EndpointRuntime {
- sendMessage: <
- ReturnType extends JsonValue,
- K extends DataTypeKey = DataTypeKey,
- >(
- messageID: K,
- data: GetDataType<K, JsonValue>,
- destination?: Destination
- ) => Promise<GetReturnType<K, ReturnType>>
- onMessage: <Data extends JsonValue, K extends DataTypeKey = DataTypeKey>(
- messageID: K,
- callback: OnMessageCallback<GetDataType<K, Data>, GetReturnType<K, any>>
- ) => (() => void)
- /**
- * @internal
- */
- handleMessage: (message: InternalMessage) => void
- endTransaction: (transactionID: string) => void
-}
-
-export const createEndpointRuntime = (
- thisContext: RuntimeContext,
- routeMessage: (msg: InternalMessage) => void,
- localMessage?: (msg: InternalMessage) => void,
-): EndpointRuntime => {
- const runtimeId = uuid()
- const openTransactions = new Map<
- string,
- { resolve: (v: unknown) => void; reject: (e: unknown) => void }
- >()
- const onMessageListeners = new Map<string, OnMessageCallback<JsonValue>>()
-
- const handleMessage = (message: InternalMessage) => {
- if (
- message.destination.context === thisContext
- && !message.destination.frameId
- && !message.destination.tabId
- ) {
- localMessage?.(message)
-
- const { transactionId, messageID, messageType } = message
-
- const handleReply = () => {
- const transactionP = openTransactions.get(transactionId)
- if (transactionP) {
- const { err, data } = message
- if (err) {
- const dehydratedErr = err as Record<string, string>
- const errCtr = self[dehydratedErr.name] as any
- const hydratedErr = new (
- typeof errCtr === 'function' ? errCtr : Error
- )(dehydratedErr.message)
-
- // eslint-disable-next-line no-restricted-syntax
- for (const prop in dehydratedErr)
- hydratedErr[prop] = dehydratedErr[prop]
-
- transactionP.reject(hydratedErr)
- }
- else {
- transactionP.resolve(data)
- }
- openTransactions.delete(transactionId)
- }
- }
-
- const handleNewMessage = async() => {
- let reply: JsonValue | void
- let err: Error
- let noHandlerFoundError = false
-
- try {
- const cb = onMessageListeners.get(messageID)
- if (typeof cb === 'function') {
- // eslint-disable-next-line n/no-callback-literal
- reply = await cb({
- sender: message.origin,
- id: messageID,
- data: message.data,
- timestamp: message.timestamp,
- } as BridgeMessage<JsonValue>)
- }
- else {
- noHandlerFoundError = true
- throw new Error(
- `[webext-bridge] No handler registered in '${thisContext}' to accept messages with id '${messageID}'`,
- )
- }
- }
- catch (error) {
- err = error
- }
- finally {
- if (err) message.err = serializeError(err)
-
- handleMessage({
- ...message,
- messageType: 'reply',
- data: reply,
- origin: { context: thisContext, tabId: null },
- destination: message.origin,
- hops: [],
- })
-
- if (err && !noHandlerFoundError)
- // eslint-disable-next-line no-unsafe-finally
- throw reply
- }
- }
-
- switch (messageType) {
- case 'reply':
- return handleReply()
- case 'message':
- return handleNewMessage()
- }
- }
-
- message.hops.push(`${thisContext}::${runtimeId}`)
-
- return routeMessage(message)
- }
-
- return {
- handleMessage,
- endTransaction: (transactionID) => {
- const transactionP = openTransactions.get(transactionID)
- transactionP?.reject('Transaction was ended before it could complete')
- openTransactions.delete(transactionID)
- },
- sendMessage: (messageID, data, destination = 'background') => {
- const endpoint
- = typeof destination === 'string'
- ? parseEndpoint(destination)
- : destination
- const errFn = 'Bridge#sendMessage ->'
-
- if (!endpoint.context) {
- throw new TypeError(
- `${errFn} Destination must be any one of known destinations`,
- )
- }
-
- return new Promise((resolve, reject) => {
- const payload: InternalMessage = {
- messageID,
- data,
- destination: endpoint,
- messageType: 'message',
- transactionId: uuid(),
- origin: { context: thisContext, tabId: null },
- hops: [],
- timestamp: Date.now(),
- }
-
- openTransactions.set(payload.transactionId, { resolve, reject })
-
- try {
- handleMessage(payload)
- }
- catch (error) {
- openTransactions.delete(payload.transactionId)
- reject(error)
- }
- })
- },
- onMessage: (messageID, callback) => {
- onMessageListeners.set(messageID, callback)
- return () => onMessageListeners.delete(messageID)
- },
- }
-}
diff --git a/extension/src/webext-bridge/internal/endpoint.ts b/extension/src/webext-bridge/internal/endpoint.ts
deleted file mode 100644
index 0c271f2..0000000
--- a/extension/src/webext-bridge/internal/endpoint.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import type { Endpoint, RuntimeContext } from '../types'
-
-const ENDPOINT_RE = /^((?:background$)|devtools|popup|options|content-script|window)(?:@(\d+)(?:\.(\d+))?)?$/
-
-export const parseEndpoint = (endpoint: string): Endpoint => {
- const [, context, tabId, frameId] = endpoint.match(ENDPOINT_RE) || []
-
- return {
- context: context as RuntimeContext,
- tabId: +tabId,
- frameId: frameId ? +frameId : undefined,
- }
-}
-
-export const formatEndpoint = ({ context, tabId, frameId }: Endpoint): string => {
- if (['background', 'popup', 'options'].includes(context))
- return context
-
- return `${context}@${tabId}${frameId ? `.${frameId}` : ''}`
-}
diff --git a/extension/src/webext-bridge/internal/is-internal-endpoint.ts b/extension/src/webext-bridge/internal/is-internal-endpoint.ts
deleted file mode 100644
index 5e6ab4c..0000000
--- a/extension/src/webext-bridge/internal/is-internal-endpoint.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-import type { Endpoint, RuntimeContext } from '../types'
-
-const internalEndpoints: RuntimeContext[] = ['background', 'devtools', 'content-script', 'options', 'popup']
-
-export const isInternalEndpoint = ({ context: ctx }: Endpoint): boolean => internalEndpoints.includes(ctx)
diff --git a/extension/src/webext-bridge/internal/message-port.ts b/extension/src/webext-bridge/internal/message-port.ts
deleted file mode 100644
index 204c11a..0000000
--- a/extension/src/webext-bridge/internal/message-port.ts
+++ /dev/null
@@ -1,52 +0,0 @@
-let promise: Promise<MessagePort>
-
-/**
- * Returns a MessagePort for one-on-one communication
- *
- * Depending on which context's code runs first, either an incoming port from the other side
- * is accepted OR a port will be offered, which the other side will then accept.
- */
-export const getMessagePort = (
- thisContext: 'window' | 'content-script',
- namespace: string,
- onMessage: (e: MessageEvent<any>) => void,
-): Promise<MessagePort> => (
- promise ??= new Promise((resolve) => {
- const acceptMessagingPort = (event: MessageEvent) => {
- const { data: { cmd, scope, context }, ports } = event
- if (cmd === 'webext-port-offer' && scope === namespace && context !== thisContext) {
- window.removeEventListener('message', acceptMessagingPort)
- ports[0].onmessage = onMessage
- ports[0].postMessage('port-accepted')
- return resolve(ports[0])
- }
- }
-
- const offerMessagingPort = () => {
- const channel = new MessageChannel()
- channel.port1.onmessage = (event: MessageEvent) => {
- if (event.data === 'port-accepted') {
- window.removeEventListener('message', acceptMessagingPort)
- return resolve(channel.port1)
- }
-
- onMessage?.(event)
- }
-
- window.postMessage({
- cmd: 'webext-port-offer',
- scope: namespace,
- context: thisContext,
- }, '*', [channel.port2])
- }
-
- window.addEventListener('message', acceptMessagingPort)
-
- // one of the contexts needs to be offset by at least 1 tick to prevent a race condition
- // where both of them are offering, and then also accepting the port at the same time
- if (thisContext === 'window')
- setTimeout(offerMessagingPort, 0)
- else
- offerMessagingPort()
- })
-)
diff --git a/extension/src/webext-bridge/internal/persistent-port.ts b/extension/src/webext-bridge/internal/persistent-port.ts
deleted file mode 100644
index 2281c68..0000000
--- a/extension/src/webext-bridge/internal/persistent-port.ts
+++ /dev/null
@@ -1,126 +0,0 @@
-import browser from 'webextension-polyfill'
-import type { Runtime } from 'webextension-polyfill'
-import type { InternalMessage } from '../types'
-import { createFingerprint } from './endpoint-fingerprint'
-import type { QueuedMessage } from './types'
-import { encodeConnectionArgs } from './connection-args'
-import { createDeliveryLogger } from './delivery-logger'
-import type { StatusMessage } from './port-message'
-import { PortMessage } from './port-message'
-
-/**
- * Manfiest V3 extensions can have their service worker terminated at any point
- * by the browser. That termination of service worker also terminates any messaging
- * porta created by other parts of the extension. This class is a wrapper around the
- * built-in Port object that re-instantiates the port connection everytime it gets
- * suspended
- */
-export const createPersistentPort = (name = '') => {
- const fingerprint = createFingerprint()
- let port: Runtime.Port
- let undeliveredQueue: ReadonlyArray<QueuedMessage> = []
- const pendingResponses = createDeliveryLogger()
- const onMessageListeners = new Set<
- (message: InternalMessage, port: Runtime.Port) => void
- >()
- const onFailureListeners = new Set<(message: InternalMessage) => void>()
-
- const handleMessage = (msg: StatusMessage, port: Runtime.Port) => {
- switch (msg.status) {
- case 'undeliverable':
- if (
- !undeliveredQueue.some(
- m => m.message.messageID === msg.message.messageID,
- )
- ) {
- undeliveredQueue = [
- ...undeliveredQueue,
- {
- message: msg.message,
- resolvedDestination: msg.resolvedDestination,
- },
- ]
- }
-
- return
-
- case 'deliverable':
- undeliveredQueue = undeliveredQueue.reduce((acc, queuedMsg) => {
- if (queuedMsg.resolvedDestination === msg.deliverableTo) {
- PortMessage.toBackground(port, {
- type: 'deliver',
- message: queuedMsg.message,
- })
-
- return acc
- }
-
- return [...acc, queuedMsg]
- }, [] as ReadonlyArray<QueuedMessage>)
-
- return
-
- case 'delivered':
- if (msg.receipt.message.messageType === 'message')
- pendingResponses.add(msg.receipt)
-
- return
-
- case 'incoming':
- if (msg.message.messageType === 'reply')
- pendingResponses.remove(msg.message.messageID)
-
- onMessageListeners.forEach(cb => cb(msg.message, port))
-
- return
-
- case 'terminated': {
- const rogueMsgs = pendingResponses
- .entries()
- .filter(receipt => msg.fingerprint === receipt.to)
- pendingResponses.remove(rogueMsgs)
- rogueMsgs.forEach(({ message }) =>
- onFailureListeners.forEach(cb => cb(message)),
- )
- }
- }
- }
-
- const connect = () => {
- port = browser.runtime.connect({
- name: encodeConnectionArgs({
- endpointName: name,
- fingerprint,
- }),
- })
- port.onMessage.addListener(handleMessage)
- port.onDisconnect.addListener(connect)
-
- PortMessage.toBackground(port, {
- type: 'sync',
- pendingResponses: pendingResponses.entries(),
- pendingDeliveries: [
- ...new Set(
- undeliveredQueue.map(({ resolvedDestination }) => resolvedDestination),
- ),
- ],
- })
- }
-
- connect()
-
- return {
- onFailure(cb: (message: InternalMessage) => void) {
- onFailureListeners.add(cb)
- },
- onMessage(cb: (message: InternalMessage) => void): void {
- onMessageListeners.add(cb)
- },
- postMessage(message: any): void {
- PortMessage.toBackground(port, {
- type: 'deliver',
- message,
- })
- },
- }
-}
diff --git a/extension/src/webext-bridge/internal/port-message.ts b/extension/src/webext-bridge/internal/port-message.ts
deleted file mode 100644
index 056e219..0000000
--- a/extension/src/webext-bridge/internal/port-message.ts
+++ /dev/null
@@ -1,48 +0,0 @@
-import type { Runtime } from 'webextension-polyfill'
-import type { InternalMessage } from '../types'
-import type { DeliveryReceipt } from './delivery-logger'
-import type { EndpointFingerprint } from './endpoint-fingerprint'
-
-export type StatusMessage =
- | {
- status: 'undeliverable'
- message: InternalMessage
- resolvedDestination: string
- }
- | {
- status: 'deliverable'
- deliverableTo: string
- }
- | {
- status: 'delivered'
- receipt: DeliveryReceipt
- }
- | {
- status: 'incoming'
- message: InternalMessage
- }
- | {
- status: 'terminated'
- fingerprint: EndpointFingerprint
- }
-
-export type RequestMessage =
- | {
- type: 'sync'
- pendingResponses: ReadonlyArray<DeliveryReceipt>
- pendingDeliveries: ReadonlyArray<string>
- }
- | {
- type: 'deliver'
- message: InternalMessage
- }
-
-export class PortMessage {
- static toBackground(port: Runtime.Port, message: RequestMessage) {
- return port.postMessage(message)
- }
-
- static toExtensionContext(port: Runtime.Port, message: StatusMessage) {
- return port.postMessage(message)
- }
-}
diff --git a/extension/src/webext-bridge/internal/post-message.ts b/extension/src/webext-bridge/internal/post-message.ts
deleted file mode 100644
index 9db4424..0000000
--- a/extension/src/webext-bridge/internal/post-message.ts
+++ /dev/null
@@ -1,52 +0,0 @@
-import type { InternalMessage } from '../types'
-import { getMessagePort } from './message-port'
-
-export interface EndpointWontRespondError {
- type: 'error'
- transactionID: string
-}
-
-export const usePostMessaging = (thisContext: 'window' | 'content-script') => {
- let allocatedNamespace: string
- let messagingEnabled = false
- let onMessageCallback: (
- msg: InternalMessage | EndpointWontRespondError
- ) => void
- let portP: Promise<MessagePort>
-
- return {
- enable: () => (messagingEnabled = true),
- onMessage: (cb: typeof onMessageCallback) => (onMessageCallback = cb),
- postMessage: async(msg: InternalMessage | EndpointWontRespondError) => {
- if (thisContext !== 'content-script' && thisContext !== 'window')
- throw new Error('Endpoint does not use postMessage')
-
- if (!messagingEnabled)
- throw new Error('Communication with window has not been allowed')
-
- ensureNamespaceSet(allocatedNamespace)
-
- return (await portP).postMessage(msg)
- },
- setNamespace: (nsps: string) => {
- if (allocatedNamespace)
- throw new Error('Namespace once set cannot be changed')
-
- allocatedNamespace = nsps
- portP = getMessagePort(thisContext, nsps, ({ data }) =>
- onMessageCallback?.(data),
- )
- },
- }
-}
-
-function ensureNamespaceSet(namespace: string) {
- if (typeof namespace !== 'string' || namespace.trim().length === 0) {
- throw new Error(
- 'webext-bridge uses window.postMessage to talk with other "window"(s) for message routing'
- + 'which is global/conflicting operation in case there are other scripts using webext-bridge. '
- + 'Call Bridge#setNamespace(nsps) to isolate your app. Example: setNamespace(\'com.facebook.react-devtools\'). '
- + 'Make sure to use same namespace across all your scripts whereever window.postMessage is likely to be used`',
- )
- }
-}
diff --git a/extension/src/webext-bridge/internal/stream.ts b/extension/src/webext-bridge/internal/stream.ts
deleted file mode 100644
index 54cee9d..0000000
--- a/extension/src/webext-bridge/internal/stream.ts
+++ /dev/null
@@ -1,179 +0,0 @@
-import { createNanoEvents } from 'nanoevents'
-import uuid from 'tiny-uid'
-import type { Emitter } from 'nanoevents'
-import type { JsonValue } from 'type-fest'
-import type { Endpoint, HybridUnsubscriber, RuntimeContext, StreamInfo } from '../types'
-import type { EndpointRuntime } from './endpoint-runtime'
-import { parseEndpoint } from './endpoint'
-
-/**
- * Built on top of Bridge. Nothing much special except that Stream allows
- * you to create a namespaced scope under a channel name of your choice
- * and allows continuous e2e communication, with less possibility of
- * conflicting messageId's, since streams are strictly scoped.
- */
-export class Stream {
- private static initDone = false
- private static openStreams: Map<string, Stream> = new Map()
-
- private emitter: Emitter = createNanoEvents()
- private isClosed = false
- constructor(private endpointRuntime: EndpointRuntime, private streamInfo: StreamInfo) {
- if (!Stream.initDone) {
- endpointRuntime.onMessage<{ streamId: string; action: 'transfer' | 'close'; streamTransfer: JsonValue }, string>('__crx_bridge_stream_transfer__', (msg) => {
- const { streamId, streamTransfer, action } = msg.data
- const stream = Stream.openStreams.get(streamId)
- if (stream && !stream.isClosed) {
- if (action === 'transfer')
- stream.emitter.emit('message', streamTransfer)
-
- if (action === 'close') {
- Stream.openStreams.delete(streamId)
- stream.handleStreamClose()
- }
- }
- })
- Stream.initDone = true
- }
-
- Stream.openStreams.set(this.streamInfo.streamId, this)
- }
-
- /**
- * Returns stream info
- */
- public get info(): StreamInfo {
- return this.streamInfo
- }
-
- /**
- * Sends a message to other endpoint.
- * Will trigger onMessage on the other side.
- *
- * Warning: Before sending sensitive data, verify the endpoint using `stream.info.endpoint.isInternal()`
- * The other side could be malicious webpage speaking same language as webext-bridge
- * @param msg
- */
- public send(msg?: JsonValue): void {
- if (this.isClosed)
- throw new Error('Attempting to send a message over closed stream. Use stream.onClose(<callback>) to keep an eye on stream status')
-
- this.endpointRuntime.sendMessage('__crx_bridge_stream_transfer__', {
- streamId: this.streamInfo.streamId,
- streamTransfer: msg,
- action: 'transfer',
- }, this.streamInfo.endpoint)
- }
-
- /**
- * Closes the stream.
- * Will trigger stream.onClose(<callback>) on both endpoints.
- * If needed again, spawn a new Stream, as this instance cannot be re-opened
- * @param msg
- */
- public close(msg?: JsonValue): void {
- if (msg)
- this.send(msg)
-
- this.handleStreamClose()
-
- this.endpointRuntime.sendMessage('__crx_bridge_stream_transfer__', {
- streamId: this.streamInfo.streamId,
- streamTransfer: null,
- action: 'close',
- }, this.streamInfo.endpoint)
- }
-
- /**
- * Registers a callback to fire whenever other endpoint sends a message
- * @param callback
- */
- public onMessage<T extends JsonValue>(callback: (msg?: T) => void): HybridUnsubscriber {
- return this.getDisposable('message', callback)
- }
-
- /**
- * Registers a callback to fire whenever stream.close() is called on either endpoint
- * @param callback
- */
- public onClose<T extends JsonValue>(callback: (msg?: T) => void): HybridUnsubscriber {
- return this.getDisposable('closed', callback)
- }
-
- private handleStreamClose = () => {
- if (!this.isClosed) {
- this.isClosed = true
- this.emitter.emit('closed', true)
- this.emitter.events = {}
- }
- }
-
- private getDisposable(event: string, callback: () => void): HybridUnsubscriber {
- const off = this.emitter.on(event, callback)
-
- return Object.assign(off, {
- dispose: off,
- close: off,
- })
- }
-}
-
-export const createStreamWirings = (endpointRuntime: EndpointRuntime) => {
- const openStreams = new Map<string, Stream>()
- const onOpenStreamCallbacks = new Map<string, (stream: Stream) => void>()
- const streamyEmitter = createNanoEvents()
-
- endpointRuntime.onMessage<{ channel: string; streamId: string }, string>('__crx_bridge_stream_open__', (message) => {
- return new Promise((resolve) => {
- const { sender, data } = message
- const { channel } = data
- let watching = false
- let off = () => { }
-
- const readyup = () => {
- const callback = onOpenStreamCallbacks.get(channel)
-
- if (typeof callback === 'function') {
- callback(new Stream(endpointRuntime, { ...data, endpoint: sender }))
- if (watching)
- off()
-
- resolve(true)
- }
- else if (!watching) {
- watching = true
- off = streamyEmitter.on('did-change-stream-callbacks', readyup)
- }
- }
-
- readyup()
- })
- })
-
- async function openStream(channel: string, destination: RuntimeContext | Endpoint | string): Promise<Stream> {
- if (openStreams.has(channel))
- throw new Error('webext-bridge: A Stream is already open at this channel')
-
- const endpoint = typeof destination === 'string' ? parseEndpoint(destination) : destination
-
- const streamInfo: StreamInfo = { streamId: uuid(), channel, endpoint }
- const stream = new Stream(endpointRuntime, streamInfo)
- stream.onClose(() => openStreams.delete(channel))
- await endpointRuntime.sendMessage('__crx_bridge_stream_open__', streamInfo as unknown as JsonValue, endpoint)
- openStreams.set(channel, stream)
- return stream
- }
-
- function onOpenStreamChannel(channel: string, callback: (stream: Stream) => void): void {
- if (onOpenStreamCallbacks.has(channel))
- throw new Error('webext-bridge: This channel has already been claimed. Stream allows only one-on-one communication')
-
- onOpenStreamCallbacks.set(channel, callback)
- streamyEmitter.emit('did-change-stream-callbacks')
- }
-
- return {
- openStream,
- onOpenStreamChannel,
- }
-}
diff --git a/extension/src/webext-bridge/internal/types.ts b/extension/src/webext-bridge/internal/types.ts
deleted file mode 100644
index 2063adb..0000000
--- a/extension/src/webext-bridge/internal/types.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-import type { InternalMessage } from '../types'
-
-export interface QueuedMessage {
- resolvedDestination: string
- message: InternalMessage
-}
diff --git a/extension/src/webext-bridge/ports.ts b/extension/src/webext-bridge/ports.ts
deleted file mode 100644
index 8edd04e..0000000
--- a/extension/src/webext-bridge/ports.ts
+++ /dev/null
@@ -1,391 +0,0 @@
-import browser, { type Runtime } from 'webextension-polyfill'
-import { formatEndpoint, parseEndpoint } from './internal/endpoint'
-import { decodeConnectionArgs } from './internal/connection-args'
-import { PortMessage } from './internal/port-message'
-import { createEndpointRuntime } from './internal/endpoint-runtime'
-import { createStreamWirings } from './internal/stream'
-import { createPersistentPort } from './internal/persistent-port'
-import { usePostMessaging } from './internal/post-message'
-import { createFingerprint, type EndpointFingerprint } from './internal/endpoint-fingerprint'
-import { createDeliveryLogger, type DeliveryReceipt } from './internal/delivery-logger'
-import type { RequestMessage } from './internal/port-message'
-import type { InternalMessage, RuntimeContext } from './types'
-
-const createContentScriptPort = () => {
-
- const win = usePostMessaging('content-script')
- const port = createPersistentPort()
- const endpointRuntime = createEndpointRuntime('content-script', (message) => {
- if (message.destination.context === 'window') win.postMessage(message)
- else port.postMessage(message)
- })
-
- win.onMessage((message: InternalMessage) => {
- // a message event inside `content-script` means a script inside `window` dispatched it to be forwarded
- // so we're making sure that the origin is not tampered (i.e script is not masquerading it's true identity)
- message.origin = {
- context: 'window',
- tabId: null,
- }
-
- endpointRuntime.handleMessage(message)
- })
-
- port.onMessage(endpointRuntime.handleMessage)
-
- port.onFailure((message) => {
- if (message.origin.context === 'window') {
- win.postMessage({
- type: 'error',
- transactionID: message.transactionId,
- })
-
- return
- }
-
- endpointRuntime.endTransaction(message.transactionId)
- })
-
- return {
- sendMessage: endpointRuntime.sendMessage,
- }
-}
-
-export const createPort = (name: RuntimeContext) => {
-
- switch (name) {
- case 'content-script':
- return createContentScriptPort();
- default:
- break;
- }
-
- const port = createPersistentPort(name)
-
- const endpointRuntime = createEndpointRuntime(
- name,
- message => port.postMessage(message),
- )
-
- port.onMessage(endpointRuntime.handleMessage)
-
- return {
- ...endpointRuntime,
- ...createStreamWirings(endpointRuntime)
- }
-}
-
-interface PortConnection {
- port: Runtime.Port
- fingerprint: EndpointFingerprint
-}
-
-export const createBackgroundPort = () =>{
- const pendingResponses = createDeliveryLogger()
- const connMap = new Map<string, PortConnection>()
- const oncePortConnectedCbs = new Map<string, Set<() => void>>()
- const onceSessionEndCbs = new Map<EndpointFingerprint, Set<() => void>>()
-
- const oncePortConnected = (endpointName: string, cb: () => void) => {
- oncePortConnectedCbs.set(
- endpointName,
- (oncePortConnectedCbs.get(endpointName) || new Set()).add(cb),
- )
-
- return () => {
- const su = oncePortConnectedCbs.get(endpointName)
- if (su?.delete(cb) && su?.size === 0)
- oncePortConnectedCbs.delete(endpointName)
- }
- }
-
- const onceSessionEnded = (
- sessionFingerprint: EndpointFingerprint,
- cb: () => void,
- ) => {
- onceSessionEndCbs.set(
- sessionFingerprint,
- (onceSessionEndCbs.get(sessionFingerprint) || new Set()).add(cb),
- )
- }
-
- const notifyEndpoint = (endpoint: string) => ({
- withFingerprint: (fingerprint: EndpointFingerprint) => {
- const nextChain = <T>(v: T) => ({ and: () => v })
-
- const notifications = {
- aboutIncomingMessage: (message: InternalMessage) => {
- const recipient = connMap.get(endpoint)
-
- PortMessage.toExtensionContext(recipient.port, {
- status: 'incoming',
- message,
- })
-
- return nextChain(notifications)
- },
-
- aboutSuccessfulDelivery: (receipt: DeliveryReceipt) => {
- const sender = connMap.get(endpoint)
- PortMessage.toExtensionContext(sender.port, {
- status: 'delivered',
- receipt,
- })
-
- return nextChain(notifications)
- },
-
- aboutMessageUndeliverability: (
- resolvedDestination: string,
- message: InternalMessage,
- ) => {
- const sender = connMap.get(endpoint)
- if (sender?.fingerprint === fingerprint) {
- PortMessage.toExtensionContext(sender.port, {
- status: 'undeliverable',
- resolvedDestination,
- message,
- })
- }
-
- return nextChain(notifications)
- },
-
- whenDeliverableTo: (targetEndpoint: string) => {
- const notifyDeliverability = () => {
- const origin = connMap.get(endpoint)
- if (
- origin?.fingerprint === fingerprint
- && connMap.has(targetEndpoint)
- ) {
- PortMessage.toExtensionContext(origin.port, {
- status: 'deliverable',
- deliverableTo: targetEndpoint,
- })
-
- return true
- }
- }
-
- if (!notifyDeliverability()) {
- const unsub = oncePortConnected(targetEndpoint, notifyDeliverability)
- onceSessionEnded(fingerprint, unsub)
- }
-
- return nextChain(notifications)
- },
-
- aboutSessionEnded: (endedSessionFingerprint: EndpointFingerprint) => {
- const conn = connMap.get(endpoint)
- if (conn?.fingerprint === fingerprint) {
- PortMessage.toExtensionContext(conn.port, {
- status: 'terminated',
- fingerprint: endedSessionFingerprint,
- })
- }
-
- return nextChain(notifications)
- },
- }
-
- return notifications
- },
- })
-
- const sessFingerprint = createFingerprint()
-
- const endpointRuntime = createEndpointRuntime(
- 'background',
- (message) => {
- if (
- message.origin.context === 'background'
- && ['content-script', 'devtools '].includes(message.destination.context)
- && !message.destination.tabId
- ) {
- throw new TypeError(
- 'When sending messages from background page, use @tabId syntax to target specific tab',
- )
- }
-
- const resolvedSender = formatEndpoint({
- ...message.origin,
- ...(message.origin.context === 'window' && { context: 'content-script' }),
- })
-
- const resolvedDestination = formatEndpoint({
- ...message.destination,
- ...(message.destination.context === 'window' && {
- context: 'content-script',
- }),
- tabId: message.destination.tabId || message.origin.tabId,
- })
-
- // downstream endpoints are agnostic of these attributes, presence of these attrs will make them think the message is not intended for them
- message.destination.tabId = null
- message.destination.frameId = null
-
- const dest = () => connMap.get(resolvedDestination)
- const sender = () => connMap.get(resolvedSender)
-
- const deliver = () => {
- notifyEndpoint(resolvedDestination)
- .withFingerprint(dest().fingerprint)
- .aboutIncomingMessage(message)
-
- const receipt: DeliveryReceipt = {
- message,
- to: dest().fingerprint,
- from: {
- endpointId: resolvedSender,
- fingerprint: sender()?.fingerprint,
- },
- }
-
- if (message.messageType === 'message') pendingResponses.add(receipt)
-
- if (message.messageType === 'reply')
- pendingResponses.remove(message.messageID)
-
- if (sender()) {
- notifyEndpoint(resolvedSender)
- .withFingerprint(sender().fingerprint)
- .aboutSuccessfulDelivery(receipt)
- }
- }
-
- if (dest()?.port) {
- deliver()
- }
- else if (message.messageType === 'message') {
- if (message.origin.context === 'background') {
- oncePortConnected(resolvedDestination, deliver)
- }
- else if (sender()) {
- notifyEndpoint(resolvedSender)
- .withFingerprint(sender().fingerprint)
- .aboutMessageUndeliverability(resolvedDestination, message)
- .and()
- .whenDeliverableTo(resolvedDestination)
- }
- }
- },
- (message) => {
- const resolvedSender = formatEndpoint({
- ...message.origin,
- ...(message.origin.context === 'window' && { context: 'content-script' }),
- })
-
- const sender = connMap.get(resolvedSender)
-
- const receipt: DeliveryReceipt = {
- message,
- to: sessFingerprint,
- from: {
- endpointId: resolvedSender,
- fingerprint: sender.fingerprint,
- },
- }
-
- notifyEndpoint(resolvedSender)
- .withFingerprint(sender.fingerprint)
- .aboutSuccessfulDelivery(receipt)
- },
- )
-
- browser.runtime.onConnect.addListener((incomingPort) => {
- const connArgs = decodeConnectionArgs(incomingPort.name)
-
- if (!connArgs) return
-
- // all other contexts except 'content-script' are aware of, and pass their identity as name
- connArgs.endpointName ||= formatEndpoint({
- context: 'content-script',
- tabId: incomingPort.sender.tab.id,
- frameId: incomingPort.sender.frameId,
- })
-
- // literal tab id in case of content script, however tab id of inspected page in case of devtools context
- const { tabId: linkedTabId, frameId: linkedFrameId } = parseEndpoint(
- connArgs.endpointName,
- )
-
- connMap.set(connArgs.endpointName, {
- fingerprint: connArgs.fingerprint,
- port: incomingPort,
- })
-
- oncePortConnectedCbs.get(connArgs.endpointName)?.forEach(cb => cb())
- oncePortConnectedCbs.delete(connArgs.endpointName)
-
- onceSessionEnded(connArgs.fingerprint, () => {
- const rogueMsgs = pendingResponses
- .entries()
- .filter(pendingMessage => pendingMessage.to === connArgs.fingerprint)
- pendingResponses.remove(rogueMsgs)
-
- rogueMsgs.forEach((rogueMessage) => {
- if (rogueMessage.from.endpointId === 'background') {
- endpointRuntime.endTransaction(rogueMessage.message.transactionId)
- }
- else {
- notifyEndpoint(rogueMessage.from.endpointId)
- .withFingerprint(rogueMessage.from.fingerprint)
- .aboutSessionEnded(connArgs.fingerprint)
- }
- })
- })
-
- incomingPort.onDisconnect.addListener(() => {
- // sometimes previous content script's onDisconnect is called *after* the fresh content-script's
- // onConnect. So without this fingerprint equality check, we would remove the new port from map
- if (
- connMap.get(connArgs.endpointName)?.fingerprint === connArgs.fingerprint
- )
- connMap.delete(connArgs.endpointName)
-
- onceSessionEndCbs.get(connArgs.fingerprint)?.forEach(cb => cb())
- onceSessionEndCbs.delete(connArgs.fingerprint)
- })
-
- incomingPort.onMessage.addListener((msg: RequestMessage) => {
- if (msg.type === 'sync') {
- const allActiveSessions = [...connMap.values()].map(
- conn => conn.fingerprint,
- )
- const stillPending = msg.pendingResponses.filter(fp =>
- allActiveSessions.includes(fp.to),
- )
-
- pendingResponses.add(...stillPending)
-
- msg.pendingResponses
- .filter(
- deliveryReceipt => !allActiveSessions.includes(deliveryReceipt.to),
- )
- .forEach(deliveryReceipt =>
- notifyEndpoint(connArgs.endpointName)
- .withFingerprint(connArgs.fingerprint)
- .aboutSessionEnded(deliveryReceipt.to),
- )
-
- msg.pendingDeliveries.forEach(intendedDestination =>
- notifyEndpoint(connArgs.endpointName)
- .withFingerprint(connArgs.fingerprint)
- .whenDeliverableTo(intendedDestination),
- )
-
- return
- }
-
- if (msg.type === 'deliver' && msg.message?.origin?.context) {
- // origin tab ID is resolved from the port identifier (also prevent "MITM attacks" of extensions)
- msg.message.origin.tabId = linkedTabId
- msg.message.origin.frameId = linkedFrameId
-
- endpointRuntime.handleMessage(msg.message)
- }
- })
- })
-
- return{ ...endpointRuntime }
-} \ No newline at end of file
diff --git a/extension/src/webext-bridge/types.ts b/extension/src/webext-bridge/types.ts
deleted file mode 100644
index d184a5c..0000000
--- a/extension/src/webext-bridge/types.ts
+++ /dev/null
@@ -1,100 +0,0 @@
-import type { JsonValue, Jsonify } from 'type-fest'
-
-export type RuntimeContext =
- | 'devtools'
- | 'background'
- | 'popup'
- | 'options'
- | 'content-script'
- | 'window'
-
-export interface Endpoint {
- context: RuntimeContext
- tabId: number
- frameId?: number
-}
-
-export interface BridgeMessage<T extends JsonValue> {
- sender: Endpoint
- id: string
- data: T
- timestamp: number
-}
-
-export type OnMessageCallback<T extends JsonValue, R = void | JsonValue> = (
- message: BridgeMessage<T>
-) => R | Promise<R>
-
-export interface InternalMessage {
- origin: Endpoint
- destination: Endpoint
- transactionId: string
- hops: string[]
- messageID: string
- messageType: 'message' | 'reply'
- err?: JsonValue
- data?: JsonValue | void
- timestamp: number
-}
-
-export interface StreamInfo {
- streamId: string
- channel: string
- endpoint: Endpoint
-}
-
-export interface HybridUnsubscriber {
- (): void
- dispose: () => void
- close: () => void
-}
-
-export type Destination = Endpoint | RuntimeContext | string
-
-declare const ProtocolWithReturnSymbol: unique symbol
-
-export interface ProtocolWithReturn<Data, Return> {
- data: Jsonify<Data>
- return: Jsonify<Return>
- /**
- * Type differentiator only.
- */
- [ProtocolWithReturnSymbol]: true
-}
-
-/**
- * Extendable by user.
- */
-export interface ProtocolMap {
- // foo: { id: number, name: string }
- // bar: ProtocolWithReturn<string, number>
-}
-
-export type DataTypeKey = keyof ProtocolMap extends never
- ? string
- : keyof ProtocolMap
-
-export type GetDataType<
- K extends DataTypeKey,
- Fallback extends JsonValue = undefined,
-> = K extends keyof ProtocolMap
- ? ProtocolMap[K] extends (...args: infer Args) => any
- ? Args['length'] extends 0
- ? undefined
- : Args[0]
- : ProtocolMap[K] extends ProtocolWithReturn<infer Data, any>
- ? Data
- : ProtocolMap[K]
- : Fallback;
-
-
-export type GetReturnType<
- K extends DataTypeKey,
- Fallback extends JsonValue = undefined
-> = K extends keyof ProtocolMap
- ? ProtocolMap[K] extends (...args: any[]) => infer R
- ? R
- : ProtocolMap[K] extends ProtocolWithReturn<any, infer Return>
- ? Return
- : void
- : Fallback;