aboutsummaryrefslogtreecommitdiff
path: root/extension/src
diff options
context:
space:
mode:
Diffstat (limited to 'extension/src')
-rw-r--r--extension/src/assets/inputs.scss3
-rw-r--r--extension/src/entries/contentScript/nostr-shim.js49
-rw-r--r--extension/src/entries/contentScript/primary/components/PromptPopup.vue127
-rw-r--r--extension/src/entries/contentScript/primary/main.js24
-rw-r--r--extension/src/entries/contentScript/renderContent.js4
-rw-r--r--extension/src/entries/nostr-provider.js10
-rw-r--r--extension/src/entries/popup/Components/Login.vue2
-rw-r--r--extension/src/entries/popup/Components/PageContent.vue7
-rw-r--r--extension/src/entries/popup/main.js4
-rw-r--r--extension/src/entries/store/allowedOrigins.ts4
-rw-r--r--extension/src/entries/store/features.ts50
-rw-r--r--extension/src/entries/store/identity.ts15
-rw-r--r--extension/src/features/framework/index.ts44
-rw-r--r--extension/src/features/identity-api.ts59
-rw-r--r--extension/src/features/nip07allow-api.ts16
-rw-r--r--extension/src/features/nostr-api.ts40
-rw-r--r--extension/src/features/server-api/index.ts28
-rw-r--r--extension/src/features/settings.ts4
-rw-r--r--extension/src/features/types.ts14
-rw-r--r--extension/src/messaging/index.ts261
-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
37 files changed, 540 insertions, 1518 deletions
diff --git a/extension/src/assets/inputs.scss b/extension/src/assets/inputs.scss
index 05c8d33..480285e 100644
--- a/extension/src/assets/inputs.scss
+++ b/extension/src/assets/inputs.scss
@@ -45,8 +45,9 @@ label.checkbox {
/*Select */
select.input {
+ @apply appearance-none;
option {
- @apply dark:bg-dark-700 ;
+ @apply dark:bg-dark-700 p-0 m-0;
}
}
diff --git a/extension/src/entries/contentScript/nostr-shim.js b/extension/src/entries/contentScript/nostr-shim.js
index eddc678..418b9c1 100644
--- a/extension/src/entries/contentScript/nostr-shim.js
+++ b/extension/src/entries/contentScript/nostr-shim.js
@@ -17,7 +17,6 @@ import { runtime } from "webextension-polyfill"
import { isEqual, isNil, isEmpty } from 'lodash'
import { apiCall } from '@vnuge/vnlib.browser'
import { useScriptTag, watchOnce } from "@vueuse/core"
-import { createPort } from '../../webext-bridge/'
import { useStore } from '../store'
import { storeToRefs } from 'pinia'
@@ -34,11 +33,14 @@ export const usePrompt = (callback) => _promptHandler.set(callback);
export const onLoad = async () =>{
+ const store = useStore()
+ const { nostr } = store.plugins
+ const { isTabAllowed, selectedKey } = storeToRefs(store)
+
const injectHandler = () => {
//Setup listener for the content script to process nostr messages
const ext = '@vnuge/nvault-extension'
- const { sendMessage } = createPort('content-script')
const scriptUrl = runtime.getURL('src/entries/nostr-provider.js')
@@ -47,6 +49,14 @@ export const onLoad = async () =>{
//Only listen for messages if injection is enabled
window.addEventListener('message', async ({ source, data, origin }) => {
+
+ const invokePrompt = async (cb) => {
+ //await propmt for user to allow the request
+ const allow = await _promptHandler.invoke({ ...data, origin })
+ //send request to background
+ return response = allow ? await cb() : { error: 'User denied permission' }
+ }
+
//Confirm the message format is correct
if (!isEqual(source, window) || isEmpty(data) || isNil(data.type)) {
return
@@ -56,21 +66,42 @@ export const onLoad = async () =>{
return
}
+ //clean any junk/methods with json parse/stringify
+ data = JSON.parse(JSON.stringify(data))
+
// pass on to background
var response;
await apiCall(async () => {
switch (data.type) {
case 'getPublicKey':
+ return invokePrompt(async () => selectedKey.value.PublicKey)
case 'signEvent':
+ return invokePrompt(async () => {
+ const event = data.payload.event
+
+ //Set key id to selected key
+ event.KeyId = selectedKey.value.Id
+ event.pubkey = selectedKey.value.PublicKey;
+
+ return await nostr.signEvent(event);
+ })
//Check the public key against selected key
case 'getRelays':
+ return invokePrompt(async () => await nostr.getRelays())
case 'nip04.encrypt':
+ return invokePrompt(async () => await nostr.nip04Encrypt({
+ pubkey: data.payload.peer,
+ content: data.payload.plaintext,
+ //Set selected key id as our desired decryption key
+ KeyId: selectedKey.value.Id
+ }))
case 'nip04.decrypt':
- //await propmt for user to allow the request
- const allow = await _promptHandler.invoke({ ...data, origin })
- //send request to background
- response = allow ? await sendMessage(data.type, { ...data.payload, origin }, 'background') : { error: 'User denied permission' }
- break;
+ return invokePrompt(async () => await nostr.nip04Decrypt({
+ pubkey: data.payload.peer,
+ content: data.payload.ciphertext,
+ //Set selected key id as our desired decryption key
+ KeyId: selectedKey.value.Id
+ }))
default:
throw new Error('Unknown nostr message type')
}
@@ -80,14 +111,10 @@ export const onLoad = async () =>{
});
}
- const store = useStore()
- const { isTabAllowed } = storeToRefs(store)
-
//Make sure the origin is allowed
if (store.isTabAllowed === false){
//If not allowed yet, wait for the store to update
watchOnce(isTabAllowed, val => val ? injectHandler() : undefined);
- return;
}
else{
injectHandler();
diff --git a/extension/src/entries/contentScript/primary/components/PromptPopup.vue b/extension/src/entries/contentScript/primary/components/PromptPopup.vue
index 195c6db..b8b7cab 100644
--- a/extension/src/entries/contentScript/primary/components/PromptPopup.vue
+++ b/extension/src/entries/contentScript/primary/components/PromptPopup.vue
@@ -1,80 +1,89 @@
<template>
- <div v-show="isOpen" id="nvault-ext-prompt">
- <div class="relative text-white" style="z-index:9147483647 !important" ref="prompt">
- <div class="fixed inset-0 left-0 flex justify-center w-full h-full p-4 bg-black/50">
- <div class="relative w-full max-w-md mx-auto mt-20 mb-auto">
- <div class="w-full p-4 border rounded-lg shadow-lg bg-dark-700 border-dark-400">
- <div v-if="loggedIn" class="">
- <h3 class="">Allow access</h3>
- <div class="pl-1 text-sm">
- Identity:
- </div>
- <div class="p-2 mt-1 text-center border rounded border-dark-400 bg-dark-600">
- <div :class="[keyName ? '' : 'text-red-500']">
- {{ keyName ?? 'Select Identity' }}
+ <div v-show="isOpen" id="nvault-ext-prompt" :class="{'dark': darkMode }">
+
+ <div class="absolute top-0 bottom-0 left-0 right-0 text-white" style="z-index:9147483647 !important" >
+ <div class="fixed inset-0 left-0 w-full h-full bg-black/50" @click.self="close" />
+ <div class="relative w-full max-w-[28rem] mx-auto mt-36 mb-auto" ref="prompt">
+ <div class="w-full p-5 bg-white border rounded-lg shadow-lg dark:bg-dark-900 dark:border-dark-500">
+ <div v-if="loggedIn" class="text-gray-800 dark:text-gray-200">
+
+ <div class="flex flex-row justify-between">
+ <div class="">
+ <div class="text-lg font-bold">
+ Allow access
+ </div>
+ <div class="text-sm">
+ <span class="">
+ Identity:
+ </span>
+ <span :class="[keyName ? '' : 'text-red-500']">
+ {{ keyName ?? 'Select Identity' }}
+ </span>
</div>
</div>
- <div class="mt-5 text-center">
- <span class="text-primary-500">{{ site }}</span>
- would like to access to
- <span class="text-yellow-500">{{ event?.msg }}</span>
- </div>
- <div class="flex gap-2 mt-4">
<div class="">
- <Popover class="relative">
- <PopoverButton class="rounded btn sm">View Raw</PopoverButton>
- <PopoverPanel class="absolute z-10">
- <div class="min-w-[22rem] p-2 border rounded bg-dark-700 border-dark-400 shadow-md text-sm">
- <p class="pl-1">
- Event Data:
- </p>
- <div class="p-2 mt-1 text-left border rounded border-dark-400 bg-dark-600 overflow-y-auto max-h-[22rem]">
-<pre>
-{{ evData }}
-</pre>
- </div>
+ <Popover class="relative">
+ <PopoverButton class="">
+ <fa-icon icon="circle-info" class="w-4 h-4" />
+ </PopoverButton>
+ <PopoverPanel class="absolute right-0 z-10">
+ <div class="min-w-[22rem] p-2 border rounded dark:bg-dark-800 bg-gray-50 dark:border-dark-500 shadow-md text-sm">
+ <p class="pl-1">
+ Event Data:
+ </p>
+ <div class="p-2 mt-1 text-left border rounded dark:border-dark-500 border-gray-300 overflow-auto max-h-[22rem] max-w-lg">
+ <pre>{{ evData }}</pre>
</div>
- </PopoverPanel>
- </Popover>
- </div>
- <div class="ml-auto">
- <button :disabled="selectedKey?.Id == undefined" class="rounded btn primary sm" @click="allow">Allow</button>
- </div>
- <div>
- <button class="rounded btn sm red" @click="close">Close</button>
- </div>
+ </div>
+ </PopoverPanel>
+ </Popover>
</div>
</div>
- <div v-else class="">
- <h3 class="">Log in!</h3>
- <div class="">
- You must log in before you can allow access.
+
+ <div class="py-3 text-sm text-center">
+ <span class="font-bold">{{ site }}</span>
+ would like to access to
+ <span class="font-bold">{{ event?.msg }}</span>
+ </div>
+
+ <div class="flex gap-2 mt-4">
+ <div class="ml-auto">
+ <button class="rounded btn sm" @click="close">Close</button>
</div>
- <div class="flex justify-end gap-2 mt-4">
- <div>
- <button class="rounded btn sm red" @click="close">Close</button>
- </div>
+ <div>
+ <button :disabled="selectedKey?.Id == undefined" class="rounded btn sm" @click="allow">Allow</button>
+ </div>
+ </div>
+ </div>
+ <div v-else class="">
+ <h3 class="">Log in!</h3>
+ <div class="">
+ You must log in before you can allow access.
+ </div>
+ <div class="flex justify-end gap-2 mt-4">
+ <div>
+ <button class="rounded btn xs" @click="close">Close</button>
</div>
</div>
</div>
</div>
</div>
</div>
+
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { usePrompt } from '../../nostr-shim.js'
-import { computed } from '@vue/reactivity';
-import { } from '@vueuse/core';
+import { computed } from 'vue';
import { Popover, PopoverButton, PopoverPanel } from '@headlessui/vue'
-import { first } from 'lodash';
+import { clone, first } from 'lodash';
import { useStore } from '../../../store';
import { storeToRefs } from 'pinia';
const store = useStore()
-const { loggedIn, selectedKey } = storeToRefs(store)
+const { loggedIn, selectedKey, darkMode } = storeToRefs(store)
const keyName = computed(() => selectedKey.value?.UserName)
const prompt = ref(null)
@@ -107,12 +116,11 @@ const allow = () => {
res?.allow()
}
-//Setup click outside
-//onClickOutside(prompt, () => isOpen.value ? close() : null)
-
//Listen for events
usePrompt(async (ev: PopupEvent) => {
+ ev = clone(ev)
+
console.log('[usePrompt] =>', ev)
switch(ev.type){
@@ -131,6 +139,9 @@ usePrompt(async (ev: PopupEvent) => {
case 'nip04.decrypt':
ev.msg = "decrypt data"
break;
+ default:
+ ev.msg = "unknown event"
+ break;
}
return new Promise((resolve) => {
@@ -142,10 +153,4 @@ usePrompt(async (ev: PopupEvent) => {
})
})
-
</script>
-
-<style lang="scss">
-
-
-</style>
diff --git a/extension/src/entries/contentScript/primary/main.js b/extension/src/entries/contentScript/primary/main.js
index bbf0932..e73923d 100644
--- a/extension/src/entries/contentScript/primary/main.js
+++ b/extension/src/entries/contentScript/primary/main.js
@@ -28,6 +28,13 @@ import localStyle from './style.scss?inline'
import { onLoad } from "../nostr-shim";
import { defer } from "lodash";
+/* FONT AWESOME CONFIG */
+import { library } from '@fortawesome/fontawesome-svg-core'
+import { faCircleInfo } from '@fortawesome/free-solid-svg-icons'
+import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
+
+library.add(faCircleInfo)
+
renderContent([], (appRoot, shadowRoot) => {
//Create the background feature wiring
@@ -38,14 +45,6 @@ renderContent([], (appRoot, shadowRoot) => {
.use(identityPlugin)
.use(originPlugin)
- createApp(App)
- .use(store)
- .use(Notification)
- .mount(appRoot);
-
- //Load the nostr shim
- defer(onLoad)
-
//Add tailwind styles just to the shadow dom element
const style = document.createElement('style')
style.innerHTML = tw.toString()
@@ -55,4 +54,13 @@ renderContent([], (appRoot, shadowRoot) => {
const style2 = document.createElement('style')
style2.innerHTML = localStyle.toString()
shadowRoot.appendChild(style2)
+
+ createApp(App)
+ .use(store)
+ .use(Notification)
+ .component('fa-icon', FontAwesomeIcon)
+ .mount(appRoot);
+
+ //Load the nostr shim
+ defer(onLoad)
}); \ No newline at end of file
diff --git a/extension/src/entries/contentScript/renderContent.js b/extension/src/entries/contentScript/renderContent.js
index 84c5b9f..293bdd5 100644
--- a/extension/src/entries/contentScript/renderContent.js
+++ b/extension/src/entries/contentScript/renderContent.js
@@ -21,9 +21,7 @@ export default async function renderContent(
render = (_appRoot) => {}
) {
const appContainer = document.createElement("div");
- const shadowRoot = appContainer.attachShadow({
- mode: import.meta.env.DEV ? "open" : "closed",
- });
+ const shadowRoot = appContainer.attachShadow({ mode: 'closed' });
const appRoot = document.createElement("div");
if (import.meta.hot) {
diff --git a/extension/src/entries/nostr-provider.js b/extension/src/entries/nostr-provider.js
index 1b8807f..9fa3bb7 100644
--- a/extension/src/entries/nostr-provider.js
+++ b/extension/src/entries/nostr-provider.js
@@ -69,9 +69,8 @@ window.addEventListener('message', ({ data }) => {
window.nostr = {
//Redirect calls to the background script
- async getPublicKey(){
- const { PublicKey } = await sendMessage('getPublicKey', {})
- return PublicKey
+ getPublicKey(){
+ return sendMessage('getPublicKey', {})
} ,
async signEvent(event){
@@ -80,9 +79,8 @@ window.nostr = {
return ev
},
- async getRelays(){
- const { relays } = await sendMessage('getRelays', {})
- return relays
+ getRelays(){
+ return sendMessage('getRelays', {})
},
nip04: {
diff --git a/extension/src/entries/popup/Components/Login.vue b/extension/src/entries/popup/Components/Login.vue
index 44df714..93c0178 100644
--- a/extension/src/entries/popup/Components/Login.vue
+++ b/extension/src/entries/popup/Components/Login.vue
@@ -31,7 +31,7 @@ const token = ref('')
const onSubmit = async () => {
await apiCall(async ({ toaster }) => {
await login(token.value)
- toaster.general.success({
+ toaster.form.success({
'title': 'Login successful',
'text': 'Successfully logged into your profile'
})
diff --git a/extension/src/entries/popup/Components/PageContent.vue b/extension/src/entries/popup/Components/PageContent.vue
index 1a3995e..e4fcb49 100644
--- a/extension/src/entries/popup/Components/PageContent.vue
+++ b/extension/src/entries/popup/Components/PageContent.vue
@@ -19,6 +19,12 @@
<fa-icon icon="arrow-right-from-bracket" />
</button>
</div>
+ <div class="my-auto">
+ <button class="rounded btn xs" @click="toggleDark" >
+ <fa-icon class="w-4" v-if="darkMode" icon="sun"/>
+ <fa-icon class="w-4" v-else icon="moon" />
+ </button>
+ </div>
<div class="my-auto">
<button class="rounded btn xs" @click="openOptions">
<fa-icon :icon="['fas', 'gear']"/>
@@ -106,6 +112,7 @@ const { copy, copied } = useClipboard()
const pubKey = computed(() => selectedKey!.value?.PublicKey)
const openOptions = () => runtime.openOptionsPage();
+const toggleDark = () => store.toggleDarkMode()
//Watch for dark mode changes and update the body class
watchEffect(() => darkMode.value ? document.body.classList.add('dark') : document.body.classList.remove('dark'));
diff --git a/extension/src/entries/popup/main.js b/extension/src/entries/popup/main.js
index a259e63..8b8a3d9 100644
--- a/extension/src/entries/popup/main.js
+++ b/extension/src/entries/popup/main.js
@@ -24,10 +24,10 @@ import "./local.scss"
/* FONT AWESOME CONFIG */
import { library } from '@fortawesome/fontawesome-svg-core'
-import { faArrowRightFromBracket, faCopy, faEdit, faGear, faMinus, faPlus, faSpinner } from '@fortawesome/free-solid-svg-icons'
+import { faArrowRightFromBracket, faCopy, faEdit, faGear, faMinus, faMoon, faPlus, faSpinner, faSun } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
-library.add(faSpinner, faEdit, faGear, faCopy, faArrowRightFromBracket, faPlus, faMinus)
+library.add(faSpinner, faEdit, faGear, faCopy, faArrowRightFromBracket, faPlus, faMinus, faSun, faMoon)
const bgPlugin = useBackgroundPiniaPlugin('popup')
diff --git a/extension/src/entries/store/allowedOrigins.ts b/extension/src/entries/store/allowedOrigins.ts
index 7fc5e15..d6de42d 100644
--- a/extension/src/entries/store/allowedOrigins.ts
+++ b/extension/src/entries/store/allowedOrigins.ts
@@ -2,7 +2,7 @@
import 'pinia'
import { } from 'lodash'
import { PiniaPluginContext } from 'pinia'
-import { computed, ref } from 'vue';
+import { computed, shallowRef } from 'vue';
import { onWatchableChange } from '../../features/types';
import { type AllowedOriginStatus } from '../../features/nip07allow-api';
@@ -22,7 +22,7 @@ declare module 'pinia' {
export const originPlugin = ({ store }: PiniaPluginContext) => {
const { plugins } = store
- const status = ref<AllowedOriginStatus>()
+ const status = shallowRef<AllowedOriginStatus>()
onWatchableChange(plugins.allowedOrigins, async () => {
//Update the status
diff --git a/extension/src/entries/store/features.ts b/extension/src/entries/store/features.ts
index 9bf3052..219386f 100644
--- a/extension/src/entries/store/features.ts
+++ b/extension/src/entries/store/features.ts
@@ -2,10 +2,8 @@
import 'pinia'
import { } from 'lodash'
import { PiniaPluginContext } from 'pinia'
-import { type Tabs, tabs } from 'webextension-polyfill'
import {
- SendMessageHandler,
useAuthApi,
useHistoryApi,
useIdentityApi,
@@ -18,9 +16,8 @@ import {
useInjectAllowList
} from "../../features"
-import { RuntimeContext, createPort } from '../../webext-bridge'
-import { ref } from 'vue'
import { onWatchableChange } from '../../features/types'
+import { ChannelContext } from '../../messaging'
export type BgPlugins = ReturnType<typeof usePlugins>
export type BgPluginState<T> = { plugins: BgPlugins } & T
@@ -28,13 +25,12 @@ export type BgPluginState<T> = { plugins: BgPlugins } & T
declare module 'pinia' {
export interface PiniaCustomProperties {
plugins: BgPlugins
- currentTab: Tabs.Tab | undefined
}
}
-const usePlugins = (sendMessage: SendMessageHandler) => {
+const usePlugins = (context: ChannelContext) => {
//Create plugin wrapping function
- const { use } = useForegoundFeatures(sendMessage)
+ const { use } = useForegoundFeatures(context)
return {
settings: use(useSettingsApi),
@@ -49,14 +45,11 @@ const usePlugins = (sendMessage: SendMessageHandler) => {
}
}
-export const useBackgroundPiniaPlugin = (context: RuntimeContext) => {
+export const useBackgroundPiniaPlugin = (context: ChannelContext) => {
//Create port for context
- const { sendMessage } = createPort(context)
- const plugins = usePlugins(sendMessage)
+ const plugins = usePlugins(context)
const { user } = plugins;
- const currentTab = ref<Tabs.Tab | undefined>(undefined)
-
//Plugin store
return ({ store }: PiniaPluginContext) => {
@@ -71,46 +64,13 @@ export const useBackgroundPiniaPlugin = (context: RuntimeContext) => {
//Wait for settings changes
onWatchableChange(plugins.settings, async () => {
-
//Update settings and dark mode on change
store.settings = await plugins.settings.getSiteConfig();
store.darkMode = await plugins.settings.getDarkMode();
- console.log("Settings changed")
}, { immediate: true })
-
-
- const initTab = async () => {
-
- if(!tabs){
- return;
- }
-
- //Get the current tab
- const [active] = await tabs.query({ active: true, currentWindow: true })
- currentTab.value = active
-
- //Watch for changes to the current tab
- tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => {
- //If the url changed, update the current tab
- if (changeInfo.url) {
- currentTab.value = tab
- }
- })
-
- tabs.onActivated.addListener(async ({ tabId }) => {
- //Get the tab
- const tab = await tabs.get(tabId)
- //Update the current tab
- currentTab.value = tab
- })
- }
-
-
- initTab()
return{
plugins,
- currentTab,
}
}
} \ No newline at end of file
diff --git a/extension/src/entries/store/identity.ts b/extension/src/entries/store/identity.ts
index 58a6b67..320263f 100644
--- a/extension/src/entries/store/identity.ts
+++ b/extension/src/entries/store/identity.ts
@@ -3,7 +3,7 @@ import 'pinia'
import { } from 'lodash'
import { PiniaPluginContext } from 'pinia'
import { NostrPubKey } from '../../features'
-import { ref } from 'vue';
+import { shallowRef } from 'vue';
import { onWatchableChange } from '../../features/types';
declare module 'pinia' {
@@ -22,17 +22,22 @@ export const identityPlugin = ({ store }: PiniaPluginContext) => {
const { identity } = store.plugins
- const allKeys = ref<NostrPubKey[]>([])
- const selectedKey = ref<NostrPubKey | undefined>(undefined)
+ const originalReset = store.$reset.bind(store)
+ const allKeys = shallowRef<NostrPubKey[]>([])
+ const selectedKey = shallowRef<NostrPubKey | undefined>(undefined)
onWatchableChange(identity, async () => {
+ console.log('Identity changed')
allKeys.value = await identity.getAllKeys();
- //Get the current key
selectedKey.value = await identity.getPublicKey();
- console.log('Selected key is now', selectedKey.value)
}, { immediate:true })
return {
+ $reset(){
+ originalReset()
+ allKeys.value = []
+ selectedKey.value = undefined
+ },
selectedKey,
allKeys,
selectKey: identity.selectKey,
diff --git a/extension/src/features/framework/index.ts b/extension/src/features/framework/index.ts
index c58ca68..44ae031 100644
--- a/extension/src/features/framework/index.ts
+++ b/extension/src/features/framework/index.ts
@@ -15,12 +15,11 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import { runtime } from "webextension-polyfill";
-import { createBackgroundPort } from '../../webext-bridge'
-import { BridgeMessage, RuntimeContext, isInternalEndpoint } from "../../webext-bridge";
import { serializeError, deserializeError } from 'serialize-error';
import { JsonObject } from "type-fest";
-import { cloneDeep, isObjectLike, set } from "lodash";
+import { cloneDeep, isArray, isObjectLike, set } from "lodash";
import { debugLog } from "@vnuge/vnlib.browser";
+import { ChannelContext, createMessageChannel } from "../../messaging";
export interface BgRuntime<T> {
readonly state: T;
@@ -68,16 +67,15 @@ export interface IBackgroundWrapper<TState> {
}
export interface ProtectedFunction extends Function {
- readonly protection: RuntimeContext[]
+ readonly protection: ChannelContext[]
}
export const optionsOnly = <T extends Function>(func: T): T => protectMethod(func, 'options');
export const popupOnly = <T extends Function>(func: T): T => protectMethod(func, 'popup');
export const contentScriptOnly = <T extends Function>(func: T): T => protectMethod(func, 'content-script');
-export const windowOnly = <T extends Function>(func: T): T => protectMethod(func, 'window');
export const popupAndOptionsOnly = <T extends Function>(func: T): T => protectMethod(func, 'popup', 'options');
-export const protectMethod = <T extends Function>(func: T, ...protection: RuntimeContext[]): T => {
+export const protectMethod = <T extends Function>(func: T, ...protection: ChannelContext[]): T => {
(func as any).protection = protection
return func;
}
@@ -88,14 +86,16 @@ export const protectMethod = <T extends Function>(func: T, ...protection: Runtim
*/
export const useBackgroundFeatures = <TState>(state: TState): IBackgroundWrapper<TState> => {
+ const { openOnMessageChannel } = createMessageChannel('background');
+ const { onMessage } = openOnMessageChannel()
+
+
const rt = {
state,
onConnected: runtime.onConnect.addListener,
onInstalled: runtime.onInstalled.addListener,
} as BgRuntime<TState>
- const { onMessage } = createBackgroundPort()
-
/**
* Each plugin will export named methods. Background methods
* are captured and registered as on-message handlers that
@@ -120,20 +120,25 @@ export const useBackgroundFeatures = <TState>(state: TState): IBackgroundWrapper
const onMessageFuncName = `${feature.name}-${externFuncName}`
//register method with api
- onMessage(onMessageFuncName, async (msg: BridgeMessage<any>) => {
+ onMessage<any>(onMessageFuncName, async (sender, payload) => {
try {
- //Always an internal endpoint
- if (!isInternalEndpoint(msg.sender)) {
+ if ((func as ProtectedFunction).protection
+ && !(func as ProtectedFunction).protection.includes(sender)) {
throw new Error(`Unauthorized external call to ${onMessageFuncName}`)
}
- if ((func as ProtectedFunction).protection
- && !(func as ProtectedFunction).protection.includes(msg.sender.context)) {
- throw new Error(`Unauthorized external call to ${onMessageFuncName}`)
+ const res = await func(...payload)
+
+ if(isArray(res)){
+ return [...res]
+ }
+ else if(isObjectLike(res)){
+ return { ...res }
+ }
+ else{
+ return res
}
- const res = await func(...msg.data)
- return isObjectLike(res) ? { ...res} : res
}
catch (e: any) {
debugLog(`Error in method ${onMessageFuncName}`, e)
@@ -154,8 +159,11 @@ export const useBackgroundFeatures = <TState>(state: TState): IBackgroundWrapper
* Creates a foreground runtime context for unwrapping foreground stub
* methods and redirecting them to thier background handler
*/
-export const useForegoundFeatures = (sendMessage: SendMessageHandler): IForegroundUnwrapper => {
+export const useForegoundFeatures = (context: ChannelContext): IForegroundUnwrapper => {
+ const { openChannel } = createMessageChannel(context);
+ const { sendMessage } = openChannel()
+
/**
* The goal of this function is to get the foreground interface object
* that should match the background implementation. All methods are
@@ -211,4 +219,4 @@ export const exportForegroundApi = <T extends FeatureApi>(args: DummyApiExport<T
}
return () => type
-} \ No newline at end of file
+}
diff --git a/extension/src/features/identity-api.ts b/extension/src/features/identity-api.ts
index 07b6387..d7db5ff 100644
--- a/extension/src/features/identity-api.ts
+++ b/extension/src/features/identity-api.ts
@@ -24,10 +24,10 @@ import {
exportForegroundApi
} from "./framework";
import { AppSettings } from "./settings";
-import { ref, watch } from "vue";
+import { shallowRef, watch } from "vue";
import { useSession } from "@vnuge/vnlib.browser";
-import { get, set, useToggle, watchOnce } from "@vueuse/core";
-import { find, isArray } from "lodash";
+import { set, useToggle, watchOnce } from "@vueuse/core";
+import { defer, isArray } from "lodash";
export interface IdentityApi extends FeatureApi, Watchable {
createIdentity: (identity: NostrPubKey) => Promise<NostrPubKey>
@@ -45,56 +45,57 @@ export const useIdentityApi = (): IFeatureExport<AppSettings, IdentityApi> => {
const { loggedIn } = useSession();
//Get the current selected key
- const selectedKey = ref<NostrPubKey | undefined>();
+ const selectedKey = shallowRef<NostrPubKey | undefined>();
+ const allKeys = shallowRef<NostrPubKey[]>([]);
const [ onTriggered , triggerChange ] = useToggle()
+ const keyLoadWatchLoop = async () => {
+ while(true){
+ //Load keys from server if logged in
+ if(loggedIn.value){
+ const [...keys] = await execRequest(Endpoints.GetKeys);
+ allKeys.value = isArray(keys) ? keys : [];
+ }
+ else{
+ //Clear all keys when logged out
+ allKeys.value = [];
+ }
+
+ //Wait for changes to trigger a new key-load
+ await new Promise((resolve) => watchOnce([onTriggered, loggedIn] as any, () => resolve(null)))
+ }
+ }
+
+ defer(keyLoadWatchLoop)
+
//Clear the selected key if the user logs out
watch(loggedIn, (li) => li ? null : selectedKey.value = undefined)
return {
//Identity is only available in options context
createIdentity: optionsOnly(async (id: NostrPubKey) => {
- await execRequest<NostrPubKey>(Endpoints.CreateId, id)
+ await execRequest(Endpoints.CreateId, id)
triggerChange()
}),
updateIdentity: optionsOnly(async (id: NostrPubKey) => {
- await execRequest<NostrPubKey>(Endpoints.UpdateId, id)
+ await execRequest(Endpoints.UpdateId, id)
triggerChange()
}),
deleteIdentity: optionsOnly(async (key: NostrPubKey) => {
- await execRequest<NostrPubKey>(Endpoints.DeleteKey, key);
+ await execRequest(Endpoints.DeleteKey, key);
triggerChange()
}),
selectKey: popupAndOptionsOnly((key: NostrPubKey): Promise<void> => {
- selectedKey.value = key;
+ set(selectedKey, key);
return Promise.resolve()
}),
- getAllKeys: async (): Promise<NostrPubKey[]> => {
- if(!get(loggedIn)){
- return []
- }
- //Get the keys from the server
- const data = await execRequest<NostrPubKey[]>(Endpoints.GetKeys);
-
- //Response must be an array of key objects
- if (!isArray(data)) {
- return [];
- }
-
- //Make sure the selected keyid is in the list, otherwise unselect the key
- if (data?.length > 0) {
- if (!find(data, k => k.Id === selectedKey.value?.Id)) {
- set(selectedKey, undefined);
- }
- }
-
- return [...data]
+ getAllKeys: (): Promise<NostrPubKey[]> => {
+ return Promise.resolve(allKeys.value);
},
getPublicKey: (): Promise<NostrPubKey | undefined> => {
return Promise.resolve(selectedKey.value);
},
waitForChange: () => {
- console.log('Waiting for change')
return new Promise((resolve) => watchOnce([selectedKey, loggedIn, onTriggered] as any, () => resolve()))
}
}
diff --git a/extension/src/features/nip07allow-api.ts b/extension/src/features/nip07allow-api.ts
index eff4ff8..0612b66 100644
--- a/extension/src/features/nip07allow-api.ts
+++ b/extension/src/features/nip07allow-api.ts
@@ -19,7 +19,7 @@ import { defaultTo, filter, includes, isEqual } from "lodash";
import { BgRuntime, FeatureApi, IFeatureExport, exportForegroundApi, popupAndOptionsOnly } from "./framework";
import { AppSettings } from "./settings";
import { set, get, watchOnce, useToggle } from "@vueuse/core";
-import { computed, ref } from "vue";
+import { computed, shallowRef } from "vue";
interface AllowedSites{
origins: string[];
@@ -46,13 +46,13 @@ export const useInjectAllowList = (): IFeatureExport<AppSettings, InjectAllowlis
const store = useSingleSlotStorage<AllowedSites>(storage.local, 'nip07-allowlist', { origins: [], enabled: true });
//watch current tab
- const allowedOrigins = ref<string[]>([])
- const protectionEnabled = ref<boolean>(true)
+ const allowedOrigins = shallowRef<string[]>([])
+ const protectionEnabled = shallowRef<boolean>(true)
const [manullyTriggered, trigger] = useToggle()
const { currentOrigin, currentTab } = (() => {
- const currentTab = ref<Tabs.Tab | undefined>(undefined)
+ const currentTab = shallowRef<Tabs.Tab | undefined>(undefined)
const currentOrigin = computed(() => currentTab.value?.url ? new URL(currentTab.value.url).origin : undefined)
//Watch for changes to the current tab
@@ -73,7 +73,7 @@ export const useInjectAllowList = (): IFeatureExport<AppSettings, InjectAllowlis
})()
const writeChanges = async () => {
- await store.set({ origins: [...get(allowedOrigins)], enabled: get(protectionEnabled) })
+ await store.set({ origins: get(allowedOrigins), enabled: get(protectionEnabled) })
}
//Initial load
@@ -158,15 +158,15 @@ export const useInjectAllowList = (): IFeatureExport<AppSettings, InjectAllowlis
}),
async getStatus(): Promise<AllowedOriginStatus> {
return{
- allowedOrigins: [...get(allowedOrigins)],
+ allowedOrigins: get(allowedOrigins),
enabled: get(protectionEnabled),
currentOrigin: get(currentOrigin),
isAllowed: isOriginAllowed()
}
},
- waitForChange: () => {
+ async waitForChange() {
//Wait for the trigger to change
- return new Promise((resolve) => watchOnce([currentTab, protectionEnabled, manullyTriggered] as any, () => resolve()));
+ await new Promise((resolve) => watchOnce([currentTab, protectionEnabled, manullyTriggered] as any, () => resolve(null)));
},
}
},
diff --git a/extension/src/features/nostr-api.ts b/extension/src/features/nostr-api.ts
index 307522d..743f8f1 100644
--- a/extension/src/features/nostr-api.ts
+++ b/extension/src/features/nostr-api.ts
@@ -14,10 +14,11 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import { Endpoints, useServerApi } from "./server-api";
-import { NostrRelay, EventMessage, NostrEvent } from './types'
-import { FeatureApi, BgRuntime, IFeatureExport, optionsOnly, exportForegroundApi } from "./framework";
-import { AppSettings } from "./settings";
+import { type FeatureApi, type BgRuntime, type IFeatureExport, optionsOnly, exportForegroundApi } from "./framework";
+import { type AppSettings } from "./settings";
import { useTagFilter } from "./tagfilter-api";
+import type { NostrRelay, EncryptionRequest, NostrEvent } from './types';
+import { cloneDeep } from "lodash";
/**
@@ -28,8 +29,8 @@ export interface NostrApi extends FeatureApi {
getRelays: () => Promise<NostrRelay[]>;
signEvent: (event: NostrEvent) => Promise<NostrEvent | undefined>;
setRelay: (relay: NostrRelay) => Promise<NostrRelay | undefined>;
- nip04Encrypt: (data: EventMessage) => Promise<string>;
- nip04Decrypt: (data: EventMessage) => Promise<string>;
+ nip04Encrypt: (data: EncryptionRequest) => Promise<string>;
+ nip04Decrypt: (data: EncryptionRequest) => Promise<string>;
}
export const useNostrApi = (): IFeatureExport<AppSettings, NostrApi> => {
@@ -43,28 +44,41 @@ export const useNostrApi = (): IFeatureExport<AppSettings, NostrApi> => {
return {
getRelays: async (): Promise<NostrRelay[]> => {
//Get preferred relays for the current user
- const data = await execRequest<NostrRelay[]>(Endpoints.GetRelays)
- return [...data]
+ const [...relays] = await execRequest(Endpoints.GetRelays)
+ return relays;
},
signEvent: async (req: NostrEvent): Promise<NostrEvent | undefined> => {
+ //Store copy to prevent mutation
+ req = cloneDeep(req)
+
//If tag filter is enabled, filter before continuing
if(state.currentConfig.value.tagFilter){
await filterTags(req)
}
//Sign the event
- const event = await execRequest<NostrEvent>(Endpoints.SignEvent, req);
+ const event = await execRequest(Endpoints.SignEvent, req);
return event;
},
- nip04Encrypt: async (data: EventMessage): Promise<string> => {
- return execRequest<string>(Endpoints.Encrypt, data);
+ nip04Encrypt: async (data: EncryptionRequest): Promise<string> => {
+ const message: EncryptionRequest = {
+ content: data.content,
+ KeyId: data.KeyId,
+ pubkey: data.pubkey
+ }
+ return execRequest(Endpoints.Encrypt, message);
},
- nip04Decrypt: (data: EventMessage): Promise<string> => {
- return execRequest<string>(Endpoints.Decrypt, data);
+ nip04Decrypt: (data: EncryptionRequest): Promise<string> => {
+ const message: EncryptionRequest = {
+ content: data.content,
+ KeyId: data.KeyId,
+ pubkey: data.pubkey
+ }
+ return execRequest(Endpoints.Decrypt, message);
},
setRelay: optionsOnly((relay: NostrRelay): Promise<NostrRelay | undefined> => {
- return execRequest<NostrRelay>(Endpoints.SetRelay, relay)
+ return execRequest(Endpoints.SetRelay, relay)
}),
}
},
diff --git a/extension/src/features/server-api/index.ts b/extension/src/features/server-api/index.ts
index 6aa34da..6637aaa 100644
--- a/extension/src/features/server-api/index.ts
+++ b/extension/src/features/server-api/index.ts
@@ -18,10 +18,9 @@ import { computed } from "vue"
import { get } from '@vueuse/core'
import { type WebMessage, type UserProfile } from "@vnuge/vnlib.browser"
import { initEndponts } from "./endpoints"
-import { type NostrIdentiy } from "../foreground/types"
import { cloneDeep } from "lodash"
import { type AppSettings } from "../settings"
-import type { NostrEvent, NostrRelay } from "../types"
+import type { EncryptionRequest, NostrEvent, NostrPubKey, NostrRelay } from "../types"
export enum Endpoints {
GetKeys = 'getKeys',
@@ -36,7 +35,20 @@ export enum Endpoints {
UpdateProfile = 'updateProfile',
}
-export const useServerApi = (settings: AppSettings) => {
+export interface ExecRequestHandler{
+ (id: Endpoints.GetKeys):Promise<NostrPubKey[]>
+ (id: Endpoints.DeleteKey, key: NostrPubKey):Promise<void>
+ (id: Endpoints.SignEvent, event: NostrEvent):Promise<NostrEvent>
+ (id: Endpoints.GetRelays):Promise<NostrRelay[]>
+ (id: Endpoints.SetRelay, relay: NostrRelay):Promise<NostrRelay>
+ (id: Endpoints.Encrypt, data: EncryptionRequest):Promise<string>
+ (id: Endpoints.Decrypt, data: EncryptionRequest):Promise<string>
+ (id: Endpoints.CreateId, identity: NostrPubKey):Promise<NostrPubKey>
+ (id: Endpoints.UpdateId, identity: NostrPubKey):Promise<NostrPubKey>
+ (id: Endpoints.UpdateProfile, profile: UserProfile):Promise<string>
+}
+
+export const useServerApi = (settings: AppSettings): { execRequest: ExecRequestHandler } => {
const { registerEndpoint, execRequest } = initEndponts()
//ref to nostr endpoint url
@@ -54,7 +66,7 @@ export const useServerApi = (settings: AppSettings) => {
registerEndpoint({
id: Endpoints.DeleteKey,
method: 'DELETE',
- path: (key:NostrIdentiy) => `${get(nostrUrl)}?type=identity&key_id=${key.Id}`,
+ path: (key: NostrPubKey) => `${get(nostrUrl)}?type=identity&key_id=${key.Id}`,
onRequest: () => Promise.resolve(),
onResponse: async (response: WebMessage) => response.getResultOrThrow()
})
@@ -91,7 +103,7 @@ export const useServerApi = (settings: AppSettings) => {
id: Endpoints.CreateId,
method: 'PUT',
path: () => `${get(nostrUrl)}?type=identity`,
- onRequest: (identity:NostrIdentiy) => Promise.resolve(identity),
+ onRequest: (identity: NostrPubKey) => Promise.resolve(identity),
onResponse: async (response: WebMessage<NostrEvent>) => response.getResultOrThrow()
})
@@ -99,7 +111,7 @@ export const useServerApi = (settings: AppSettings) => {
id: Endpoints.UpdateId,
method: 'PATCH',
path: () => `${get(nostrUrl)}?type=identity`,
- onRequest: (identity:NostrIdentiy) => {
+ onRequest: (identity:NostrPubKey) => {
const id = cloneDeep(identity) as any;
delete id.Created;
delete id.LastModified;
@@ -121,7 +133,7 @@ export const useServerApi = (settings: AppSettings) => {
id:Endpoints.Encrypt,
method:'POST',
path: () => `${get(nostrUrl)}?type=encrypt`,
- onRequest: (data: NostrEvent) => Promise.resolve(data),
+ onRequest: (data: EncryptionRequest) => Promise.resolve(data),
onResponse: async (response: WebMessage<string>) => response.getResultOrThrow()
})
@@ -129,7 +141,7 @@ export const useServerApi = (settings: AppSettings) => {
id:Endpoints.Decrypt,
method:'POST',
path: () => `${get(nostrUrl)}?type=decrypt`,
- onRequest: (data: NostrEvent) => Promise.resolve(data),
+ onRequest: (data: EncryptionRequest) => Promise.resolve(data),
onResponse: async (response: WebMessage<string>) => response.getResultOrThrow()
})
diff --git a/extension/src/features/settings.ts b/extension/src/features/settings.ts
index a67957c..059c2d2 100644
--- a/extension/src/features/settings.ts
+++ b/extension/src/features/settings.ts
@@ -19,7 +19,7 @@ import { configureApi, debugLog } from '@vnuge/vnlib.browser'
import { readonly, ref, Ref } from "vue";
import { JsonObject } from "type-fest";
import { Watchable, useSingleSlotStorage } from "./types";
-import { BgRuntime, FeatureApi, optionsOnly, IFeatureExport, exportForegroundApi } from './framework'
+import { BgRuntime, FeatureApi, optionsOnly, IFeatureExport, exportForegroundApi, popupAndOptionsOnly } from './framework'
import { get, watchOnce } from "@vueuse/core";
export interface PluginConfig extends JsonObject {
@@ -143,7 +143,7 @@ export const useSettingsApi = () : IFeatureExport<AppSettings, SettingsApi> =>{
return state.currentConfig.value
}),
- setDarkMode: optionsOnly(async (darkMode: boolean) => {
+ setDarkMode: popupAndOptionsOnly(async (darkMode: boolean) => {
console.log('Setting dark mode to', darkMode, 'from', _darkMode.value)
_darkMode.value = darkMode
}),
diff --git a/extension/src/features/types.ts b/extension/src/features/types.ts
index bd0afee..a64be93 100644
--- a/extension/src/features/types.ts
+++ b/extension/src/features/types.ts
@@ -35,8 +35,18 @@ export interface TaggedNostrEvent extends NostrEvent {
tags?: any[][]
}
-export interface EventMessage extends JsonObject {
- readonly event: NostrEvent
+export interface EncryptionRequest extends JsonObject {
+ readonly KeyId: string
+ /**
+ * The plaintext to encrypt or ciphertext
+ * to decrypt
+ */
+ readonly content: string
+ /**
+ * The other peer's public key used
+ * for encryption
+ */
+ readonly pubkey: string
}
export interface NostrRelay extends JsonObject {
diff --git a/extension/src/messaging/index.ts b/extension/src/messaging/index.ts
new file mode 100644
index 0000000..dfc5aee
--- /dev/null
+++ b/extension/src/messaging/index.ts
@@ -0,0 +1,261 @@
+import uuid from 'tiny-uid'
+import { isEqual } from 'lodash'
+import { Runtime, runtime } from 'webextension-polyfill'
+import { serializeError, isErrorLike, type ErrorObject, deserializeError } from 'serialize-error'
+import type { JsonObject } from 'type-fest'
+
+export type ChannelContext = 'background' | 'content-script' | 'popup' | 'devtools' | 'options'
+export type OnMessageCallback<T extends JsonObject> = (sender: ChannelContext, payload: any) => Promise<T>
+
+export interface ClientChannel {
+ sendMessage<TSource extends JsonObject, TResult extends JsonObject>(name: string, message: TSource): Promise<TResult>
+ disconnect(): void
+}
+
+export interface ListenerChannel {
+ onMessage<T extends JsonObject>(name: string, onMessage: OnMessageCallback<T>): () => void
+ onDisconnected(cb: () => void): void
+}
+
+export interface MessageChannel{
+ openChannel(): ClientChannel
+ openOnMessageChannel(): ListenerChannel
+}
+
+interface InternalChannelMessage{
+ readonly transactionId: string
+ readonly payload: JsonObject
+ readonly srcContext: ChannelContext
+ readonly destContext: ChannelContext
+ readonly handlerName: string
+}
+
+interface InternalChannelMessageResponse{
+ readonly request: InternalChannelMessage
+ readonly response: JsonObject | undefined
+ readonly error?: ErrorObject
+}
+
+interface SendMessageToken<T extends JsonObject> {
+ readonly request: InternalChannelMessage;
+ resolve(response: T): void;
+ reject(error: Error): void
+}
+
+interface RxChannelState{
+ addHandler(name:string, handler: OnMessageCallback<any>): () => void
+ removeHandler(name:string, handler: OnMessageCallback<any>): void
+ handlePortMessage(message: InternalChannelMessage, port: Runtime.Port): void
+ onDisconnected(): void
+}
+
+export const createMessageChannel = (localContext: ChannelContext): MessageChannel => {
+
+ const createRxChannel = (): RxChannelState => {
+
+ const createPortResponse = (request: InternalChannelMessage, response?: JsonObject, error?: ErrorObject)
+ : InternalChannelMessageResponse => {
+ return { request, response, error }
+ }
+
+ //Stores open messages
+ const handlerMap = new Map<string, OnMessageCallback<any>>()
+
+ const handleMessageInternal = async (message: InternalChannelMessage): Promise<any> => {
+ //OnMessage hanlders will always respond to a destination context
+ if (!isEqual(message.destContext, localContext)) {
+ throw new Error(`Invalid destination context ${message.destContext}`)
+ }
+ switch (message.srcContext) {
+ case 'background':
+ throw new Error('Background context is not supported as a source')
+ case 'content-script':
+ case 'popup':
+ case 'devtools':
+ case 'options':
+ break;
+ default:
+ throw new Error(`Invalid source context ${message.srcContext}`)
+ }
+
+ //Try to get the handler
+ const handler = handlerMap.get(message.handlerName)
+ if (handler === undefined) {
+ throw new Error(`No handler for ${message.handlerName}`)
+ }
+
+ return handler(message.srcContext, message.payload);
+ }
+
+ return {
+ addHandler(name: string, handler: OnMessageCallback<any>) {
+ handlerMap.set(name, handler)
+ //Return a function to remove the handler
+ return () => handlerMap.delete(name)
+ },
+ removeHandler(name: string) {
+ handlerMap.delete(name)
+ },
+ async handlePortMessage(message: InternalChannelMessage, port: Runtime.Port) {
+
+ let isConnected = true
+ const onDisconnected = () => {
+ isConnected = false
+ }
+
+ //Add disconnect handler so we can know if the port has disconnected
+ port.onDisconnect.addListener(onDisconnected)
+
+ try {
+
+ //Invoke internal message handler and convert to promise response
+ const response = await handleMessageInternal(message);
+
+ if (!isConnected) {
+ return
+ }
+
+ port.postMessage(
+ createPortResponse(message, response)
+ )
+ }
+ catch (err: unknown) {
+ if (!isConnected) {
+ return
+ }
+
+ //try to serialize the error
+ const handlerError = isErrorLike(err) ? serializeError(err) : err as ErrorObject
+
+ //Send the error back to the port
+ port.postMessage(
+ createPortResponse(message, undefined, handlerError)
+ )
+ }
+ finally {
+ //remove disconnect handler
+ port.onDisconnect.removeListener(onDisconnected)
+ }
+ },
+ onDisconnected() {
+ }
+ }
+ }
+
+ const createTxChannel = (destContext: ChannelContext) => {
+
+ const handlerMap = new Map<string, SendMessageToken<any>>()
+
+ return {
+ sendMessage: (port: Runtime.Port) => {
+ return <T extends JsonObject>(name: string, message: JsonObject): Promise<T> => {
+ //unique transaction id for message, used to match in response map
+ const transactionId = uuid(32)
+
+ //Create itnernal request wrapper
+ const request: InternalChannelMessage = {
+ transactionId,
+ payload: message,
+ srcContext: localContext,
+ destContext,
+ handlerName: name
+ }
+
+ //Create promise
+ const promise = new Promise<T>((resolve, reject) => {
+ //Add to handler map
+ handlerMap.set(transactionId, { request, resolve, reject })
+ })
+
+ //Send request
+ port.postMessage(request)
+
+ //Return promise
+ return promise
+ }
+ },
+ onMessage: (message: InternalChannelMessageResponse) => {
+ const { transactionId } = message.request
+
+ //Get the handler
+ const handler = handlerMap.get(transactionId)
+ if (handler === undefined) {
+ throw new Error(`No waiting response handler for ${transactionId}`)
+ }
+
+ //Remove the handler
+ handlerMap.delete(transactionId)
+
+ //Check for error
+ if (message.error !== undefined) {
+ //Deserialize error
+ const err = deserializeError(message.error)
+ handler.reject(err)
+ }
+ else {
+ handler.resolve(message.response)
+ }
+ },
+ onReconnect: (port: Runtime.Port) => {
+ //resend pending messages
+ handlerMap.forEach((token, _) => port.postMessage(token.request))
+ }
+ }
+ }
+
+ return {
+ openChannel(): ClientChannel {
+ //Open the transmission channel
+ const { sendMessage, onReconnect, onMessage } = createTxChannel('background');
+
+ let port: Runtime.Port;
+
+ /**
+ * Creates a persistent connection to the background script.
+ * When the port closes, it is reopend and all pending messages
+ * are resent
+ */
+ const connect = () => {
+ port = runtime.connect()
+ port.onMessage.addListener(onMessage)
+ //reconnect on disconnect
+ port.onDisconnect.addListener(connect)
+ //resend pending messages
+ onReconnect(port)
+ }
+
+ if (localContext === 'background') {
+ throw new Error('Send channels are not currently supported by ')
+ }
+
+ connect()
+
+ return {
+ //Init send-message handler
+ sendMessage: sendMessage(port!),
+ disconnect: port!.disconnect
+ }
+ },
+ openOnMessageChannel(): ListenerChannel {
+ const { addHandler, handlePortMessage, onDisconnected } = createRxChannel()
+
+ const onDisconnectedHandlers = new Set<() => void>()
+
+ //Listen for new connections
+ runtime.onConnect.addListener((port: Runtime.Port) => {
+ port.onMessage.addListener(handlePortMessage);
+ port.onDisconnect.addListener(onDisconnected);
+ //Call all local handlers on on disconnect
+ port.onDisconnect.addListener(() => {
+ onDisconnectedHandlers.forEach(cb => cb())
+ })
+ })
+
+ return {
+ onMessage: addHandler,
+ //add to onDisconnectedHandlers
+ onDisconnected: onDisconnectedHandlers.add,
+ }
+ }
+ }
+} \ No newline at end of file
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;