diff options
author | vnugent <public@vaughnnugent.com> | 2023-11-22 15:07:08 -0500 |
---|---|---|
committer | vnugent <public@vaughnnugent.com> | 2023-11-22 15:07:08 -0500 |
commit | e272adcc3f32e31fe7668551453b8e34bc823c3e (patch) | |
tree | 680c695184ddbc27227578afa9f169d98a69f55a /extension/src/entries | |
parent | 2ba94602a87c87b47f566745bdab40ce75e0e879 (diff) |
feature and internal api polish
Diffstat (limited to 'extension/src/entries')
-rw-r--r-- | extension/src/entries/contentScript/nostr-shim.js | 123 | ||||
-rw-r--r-- | extension/src/entries/contentScript/primary/components/PromptPopup.vue | 10 | ||||
-rw-r--r-- | extension/src/entries/contentScript/primary/main.js | 10 | ||||
-rw-r--r-- | extension/src/entries/contentScript/util.ts | 138 | ||||
-rw-r--r-- | extension/src/entries/nostr-provider.js | 6 | ||||
-rw-r--r-- | extension/src/entries/options/components/Identities.vue | 9 | ||||
-rw-r--r-- | extension/src/entries/options/components/SiteSettings.vue | 5 | ||||
-rw-r--r-- | extension/src/entries/store/allowedOrigins.ts | 10 | ||||
-rw-r--r-- | extension/src/entries/store/features.ts | 4 | ||||
-rw-r--r-- | extension/src/entries/store/identity.ts | 4 |
10 files changed, 165 insertions, 154 deletions
diff --git a/extension/src/entries/contentScript/nostr-shim.js b/extension/src/entries/contentScript/nostr-shim.js deleted file mode 100644 index 418b9c1..0000000 --- a/extension/src/entries/contentScript/nostr-shim.js +++ /dev/null @@ -1,123 +0,0 @@ -// Copyright (C) 2023 Vaughn Nugent -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as -// published by the Free Software Foundation, either version 3 of the -// License, or (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see <https://www.gnu.org/licenses/>. - -import { runtime } from "webextension-polyfill" -import { isEqual, isNil, isEmpty } from 'lodash' -import { apiCall } from '@vnuge/vnlib.browser' -import { useScriptTag, watchOnce } from "@vueuse/core" -import { useStore } from '../store' -import { storeToRefs } from 'pinia' - -const _promptHandler = (() => { - let _handler = undefined; - return{ - invoke: (event) => _handler(event), - set: (handler) => _handler = handler - } -})() - -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 scriptUrl = runtime.getURL('src/entries/nostr-provider.js') - - //setup script tag - useScriptTag(scriptUrl, undefined, { manual: false, defer: true }) - - //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 - } - //Confirm extension is for us - if (!isEqual(data.ext, ext)) { - 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': - 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') - } - }) - // return response message, must have the same id as the request - window.postMessage({ ext, id: data.id, response }, origin); - }); - } - - //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); - } - else{ - injectHandler(); - } - -}
\ No newline at end of file diff --git a/extension/src/entries/contentScript/primary/components/PromptPopup.vue b/extension/src/entries/contentScript/primary/components/PromptPopup.vue index b8b7cab..381f7b3 100644 --- a/extension/src/entries/contentScript/primary/components/PromptPopup.vue +++ b/extension/src/entries/contentScript/primary/components/PromptPopup.vue @@ -75,7 +75,7 @@ <script setup lang="ts"> import { ref } from 'vue' -import { usePrompt } from '../../nostr-shim.js' +import { usePrompt, type UserPermissionRequest } from '../../util' import { computed } from 'vue'; import { Popover, PopoverButton, PopoverPanel } from '@headlessui/vue' import { clone, first } from 'lodash'; @@ -88,11 +88,7 @@ const keyName = computed(() => selectedKey.value?.UserName) const prompt = ref(null) -interface PopupEvent{ - type: string - msg: string - origin: string - data: any +interface PopupEvent extends UserPermissionRequest { allow: () => void close: () => void } @@ -117,7 +113,7 @@ const allow = () => { } //Listen for events -usePrompt(async (ev: PopupEvent) => { +usePrompt((ev: UserPermissionRequest):Promise<boolean> => { ev = clone(ev) diff --git a/extension/src/entries/contentScript/primary/main.js b/extension/src/entries/contentScript/primary/main.js index e73923d..dbfa07b 100644 --- a/extension/src/entries/contentScript/primary/main.js +++ b/extension/src/entries/contentScript/primary/main.js @@ -13,7 +13,7 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see <https://www.gnu.org/licenses/>. - +import { runtime } from "webextension-polyfill"; import { createApp } from "vue"; import { createPinia } from 'pinia'; import { useBackgroundPiniaPlugin, identityPlugin, originPlugin } from '../../store' @@ -25,7 +25,7 @@ import '@fontsource/noto-sans-masaram-gondi' //We need inline styles to inject into the shadow dom import tw from "~/assets/all.scss?inline"; import localStyle from './style.scss?inline' -import { onLoad } from "../nostr-shim"; +import { onLoad } from "../util"; import { defer } from "lodash"; /* FONT AWESOME CONFIG */ @@ -35,6 +35,10 @@ import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome' library.add(faCircleInfo) +//The extension name, same as nostr-provider script path +const ext = '@vnuge/nvault-extension' +const scriptUrl = runtime.getURL('src/entries/nostr-provider.js') + renderContent([], (appRoot, shadowRoot) => { //Create the background feature wiring @@ -62,5 +66,5 @@ renderContent([], (appRoot, shadowRoot) => { .mount(appRoot); //Load the nostr shim - defer(onLoad) + defer(() => onLoad(ext, scriptUrl)) });
\ No newline at end of file diff --git a/extension/src/entries/contentScript/util.ts b/extension/src/entries/contentScript/util.ts new file mode 100644 index 0000000..aa6aac3 --- /dev/null +++ b/extension/src/entries/contentScript/util.ts @@ -0,0 +1,138 @@ +// Copyright (C) 2023 Vaughn Nugent +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +import { isEqual, isNil, isEmpty } from 'lodash' +import { apiCall } from '@vnuge/vnlib.browser' +import { Store, storeToRefs } from 'pinia' +import { useScriptTag, watchOnce } from "@vueuse/core" +import { useStore } from '../store' + +export type PromptHandler = (request: UserPermissionRequest) => Promise<boolean> + +export interface UserPermissionRequest { + type: string + msg: string + origin: string + data: any +} + +const _promptHandler = (() => { + let _handler: PromptHandler | undefined = undefined; + return { + invoke(event:UserPermissionRequest){ + if (!_handler) { + throw new Error('No prompt handler set') + } + return _handler(event) + }, + set(handler: PromptHandler) { + _handler = handler + } + } +})() + + +const registerWindowHandler = (store: Store, extName: string) => { + + const { selectedKey } = storeToRefs(store) + const { nostr } = store.plugins; + + //Only listen for messages if injection is enabled + window.addEventListener('message', async ({ source, data, origin }) => { + + //clean any junk/methods with json parse/stringify + data = JSON.parse(JSON.stringify(data)) + + const invokePrompt = async (cb:(...args:any) => Promise<any>) => { + //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 + } + //Confirm extension is for us + if (!isEqual(data.ext, extName)) { + return + } + + // 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': + 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') + } + }) + // return response message, must have the same id as the request + window.postMessage({ ext: extName, id: data.id, response }, origin); + }); +} + +export const usePrompt = (callback: PromptHandler) => _promptHandler.set(callback); + +export const onLoad = async (extName: string, scriptUrl: string) => { + + const store = useStore() + const { isTabAllowed } = storeToRefs(store) + + const injectHandler = () => { + //inject the nostr provider script into the page + useScriptTag(scriptUrl, undefined, { manual: false, defer: true }) + //Regsiter listener for messages from the injected script + registerWindowHandler(store, extName) + } + + //Make sure the origin is allowed + if (isTabAllowed.value === false) { + //If not allowed yet, wait for the store to update + watchOnce(isTabAllowed, val => val ? injectHandler() : undefined); + } + else { + injectHandler(); + } +}
\ No newline at end of file diff --git a/extension/src/entries/nostr-provider.js b/extension/src/entries/nostr-provider.js index 9fa3bb7..7d8f3c5 100644 --- a/extension/src/entries/nostr-provider.js +++ b/extension/src/entries/nostr-provider.js @@ -74,9 +74,9 @@ window.nostr = { } , async signEvent(event){ - const { event:ev } = await sendMessage('signEvent', { event }) - debugLog("Signed event", ev); - return ev + const signed = await sendMessage('signEvent', { event }) + debugLog("Signed event", signed); + return signed }, getRelays(){ diff --git a/extension/src/entries/options/components/Identities.vue b/extension/src/entries/options/components/Identities.vue index e3af74e..0e3e79d 100644 --- a/extension/src/entries/options/components/Identities.vue +++ b/extension/src/entries/options/components/Identities.vue @@ -158,11 +158,10 @@ const onNip05Download = () => { //create file blob const blob = new Blob([JSON.stringify({ names:nip05 })], { type: 'application/json' }) - //Download the file - downloadAnchor.value!.href = URL.createObjectURL(blob); - downloadAnchor.value?.setAttribute('download', 'nostr.json') - downloadAnchor.value?.click(); - + const anchor = get(downloadAnchor); + anchor!.href = URL.createObjectURL(blob); + anchor!.setAttribute('download', 'nip05.json') + anchor!.click(); }) } diff --git a/extension/src/entries/options/components/SiteSettings.vue b/extension/src/entries/options/components/SiteSettings.vue index c0c2e93..b31bb9c 100644 --- a/extension/src/entries/options/components/SiteSettings.vue +++ b/extension/src/entries/options/components/SiteSettings.vue @@ -7,7 +7,7 @@ Extension settings </h3> <div class="my-6"> - <fieldset :disabled="waiting.value"> + <fieldset :disabled="waiting"> <div class=""> <div class="w-full"> <div class="flex flex-row w-full"> @@ -139,7 +139,7 @@ const { apply, data, buffer, modified, update } = useDataBuffer(settings.value, }) //Watch for store settings changes and apply them -watch(settings, v => apply(v.value)) +watch(settings, v => apply(v)) const originProtection = computed({ get: () => isOriginProtectionOn.value, @@ -196,7 +196,6 @@ const onSave = async () => { toggleEdit(); } - const testConnection = async () =>{ return await apiCall(async ({axios, toaster}) =>{ try{ diff --git a/extension/src/entries/store/allowedOrigins.ts b/extension/src/entries/store/allowedOrigins.ts index d6de42d..661dd43 100644 --- a/extension/src/entries/store/allowedOrigins.ts +++ b/extension/src/entries/store/allowedOrigins.ts @@ -3,15 +3,15 @@ import 'pinia' import { } from 'lodash' import { PiniaPluginContext } from 'pinia' import { computed, shallowRef } from 'vue'; -import { onWatchableChange } from '../../features/types'; +import { onWatchableChange } from '../../features'; import { type AllowedOriginStatus } from '../../features/nip07allow-api'; declare module 'pinia' { export interface PiniaCustomProperties { - isTabAllowed: boolean; - currentOrigin: string | undefined; - allowedOrigins: Array<string>; - isOriginProtectionOn: boolean; + readonly isTabAllowed: boolean; + readonly currentOrigin: string | undefined; + readonly allowedOrigins: Array<string>; + readonly isOriginProtectionOn: boolean; allowOrigin(origin?:string): Promise<void>; dissallowOrigin(origin?:string): Promise<void>; disableOriginProtection(): Promise<void>; diff --git a/extension/src/entries/store/features.ts b/extension/src/entries/store/features.ts index 219386f..c619b0e 100644 --- a/extension/src/entries/store/features.ts +++ b/extension/src/entries/store/features.ts @@ -13,10 +13,10 @@ import { useSettingsApi, useForegoundFeatures, useEventTagFilterApi, - useInjectAllowList + useInjectAllowList, + onWatchableChange } from "../../features" -import { onWatchableChange } from '../../features/types' import { ChannelContext } from '../../messaging' export type BgPlugins = ReturnType<typeof usePlugins> diff --git a/extension/src/entries/store/identity.ts b/extension/src/entries/store/identity.ts index 320263f..b1635f2 100644 --- a/extension/src/entries/store/identity.ts +++ b/extension/src/entries/store/identity.ts @@ -2,9 +2,8 @@ import 'pinia' import { } from 'lodash' import { PiniaPluginContext } from 'pinia' -import { NostrPubKey } from '../../features' +import { onWatchableChange, type NostrPubKey } from '../../features' import { shallowRef } from 'vue'; -import { onWatchableChange } from '../../features/types'; declare module 'pinia' { export interface PiniaCustomStateProperties { @@ -17,7 +16,6 @@ declare module 'pinia' { } } - export const identityPlugin = ({ store }: PiniaPluginContext) => { const { identity } = store.plugins |