aboutsummaryrefslogtreecommitdiff
path: root/extension/src/entries/contentScript
diff options
context:
space:
mode:
authorLibravatar vnugent <public@vaughnnugent.com>2023-09-06 13:51:13 -0400
committerLibravatar vnugent <public@vaughnnugent.com>2023-09-06 13:51:13 -0400
commitcd8e865dad326f85ff2357ad90bbd6aa65dea68e (patch)
tree0d4a0bb8bafc4f807407e99c5e6bf4e1cb34217a /extension/src/entries/contentScript
initial commit
Diffstat (limited to 'extension/src/entries/contentScript')
-rw-r--r--extension/src/entries/contentScript/nostr-shim.js87
-rw-r--r--extension/src/entries/contentScript/primary/App.vue17
-rw-r--r--extension/src/entries/contentScript/primary/components/PromptPopup.vue148
-rw-r--r--extension/src/entries/contentScript/primary/main.js41
-rw-r--r--extension/src/entries/contentScript/primary/style.scss15
-rw-r--r--extension/src/entries/contentScript/renderContent.js48
6 files changed, 356 insertions, 0 deletions
diff --git a/extension/src/entries/contentScript/nostr-shim.js b/extension/src/entries/contentScript/nostr-shim.js
new file mode 100644
index 0000000..26b17a9
--- /dev/null
+++ b/extension/src/entries/contentScript/nostr-shim.js
@@ -0,0 +1,87 @@
+// 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 { sendMessage } from 'webext-bridge/content-script'
+import { apiCall } from '@vnuge/vnlib.browser'
+import { useManagment } from './../../bg-api/content-script'
+
+const { getSiteConfig } = useManagment()
+const nip07Enabled = () => getSiteConfig().then(p => p.autoInject);
+
+//Setup listener for the content script to process nostr messages
+
+const ext = '@vnuge/nvault-extension'
+
+let _promptHandler = () => {}
+
+export const usePrompt = (callback) => {
+ //Register the callback
+ _promptHandler = async (event) => {
+ return new Promise((resolve, reject) => {
+ callback(event).then(resolve).catch(reject)
+ })
+ }
+ return {}
+}
+
+//Only inject the script if the site has autoInject enabled
+nip07Enabled().then(enabled => {
+ console.log('Nip07 enabled:', enabled)
+ if (enabled) {
+ // inject the script that will provide window.nostr
+ let script = document.createElement('script');
+ script.setAttribute('async', 'false');
+ script.setAttribute('type', 'text/javascript');
+ script.setAttribute('src', runtime.getURL('src/entries/nostr-provider.js'));
+ document.head.appendChild(script);
+
+ //Only listen for messages if injection is enabled
+ window.addEventListener('message', async ({ source, data, origin }) => {
+ //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
+ }
+
+ // pass on to background
+ var response;
+ await apiCall(async () => {
+ switch (data.type) {
+ case 'getPublicKey':
+ case 'signEvent':
+ //Check the public key against selected key
+ case 'getRelays':
+ case 'nip04.encrypt':
+ case 'nip04.decrypt':
+ //await propmt for user to allow the request
+ const allow = await _promptHandler({ ...data, origin })
+ //send request to background
+ response = allow ? await sendMessage(data.type, { ...data.payload, origin }) : { error: 'User denied permission' }
+ break;
+ 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);
+ });
+ }
+})
diff --git a/extension/src/entries/contentScript/primary/App.vue b/extension/src/entries/contentScript/primary/App.vue
new file mode 100644
index 0000000..05dfac0
--- /dev/null
+++ b/extension/src/entries/contentScript/primary/App.vue
@@ -0,0 +1,17 @@
+<template>
+ <html>
+ <body id="injected-root">
+ <notifications class="toaster" group="form" position="top-right" />
+ <Prompt></Prompt>
+ </body>
+ </html>
+</template>
+
+<script setup lang="ts">
+import { configureNotifier } from '@vnuge/vnlib.browser';
+import { notify } from "@kyvg/vue3-notification";
+import Prompt from './components/PromptPopup.vue'
+
+configureNotifier({ notify, close: notify.close })
+
+</script> \ 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
new file mode 100644
index 0000000..057f66a
--- /dev/null
+++ b/extension/src/entries/contentScript/primary/components/PromptPopup.vue
@@ -0,0 +1,148 @@
+<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="[selectedKey?.UserName ? '' : 'text-red-500']">
+ {{ selectedKey?.UserName ?? 'Select Identity' }}
+ </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>
+ </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>
+ </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 sm red" @click="close">Close</button>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+</template>
+
+<script setup lang="ts">
+import { ref } from 'vue'
+import { usePrompt } from '~/entries/contentScript/nostr-shim'
+import { computed } from '@vue/reactivity';
+import { onClickOutside } from '@vueuse/core';
+import { useStatus } from '~/bg-api/content-script.ts';
+import { Popover, PopoverButton, PopoverPanel } from '@headlessui/vue'
+import { first } from 'lodash';
+
+const { loggedIn, selectedKey } = useStatus()
+
+const prompt = ref(null)
+
+interface PopupEvent{
+ type: string
+ msg: string
+ origin: string
+ data: any
+ allow: () => void
+ close: () => void
+}
+
+const evStack = ref<PopupEvent[]>([])
+const isOpen = computed(() => evStack.value.length > 0)
+const event = computed<PopupEvent | undefined>(() => first(evStack.value));
+
+const site = computed(() => new URL(event.value?.origin || "https://example.com").host)
+const evData = computed(() => JSON.stringify(event.value || {}, null, 2))
+
+
+const close = () => {
+ //Pop the first event off
+ const res = evStack.value.shift()
+ res?.close()
+}
+const allow = () => {
+ //Pop the first event off
+ const res = evStack.value.shift()
+ res?.allow()
+}
+
+//Setup click outside
+//onClickOutside(prompt, () => isOpen.value ? close() : null)
+
+//Listen for events
+usePrompt(async (ev: PopupEvent) => {
+
+ console.log('usePrompt', ev)
+
+ switch(ev.type){
+ case 'getPublicKey':
+ ev.msg = "your public key"
+ break;
+ case 'signEvent':
+ ev.msg = "sign an event"
+ break;
+ case 'getRelays':
+ ev.msg = "get your preferred relays"
+ break;
+ case 'nip04.encrypt':
+ ev.msg = "encrypt data"
+ break;
+ case 'nip04.decrypt':
+ ev.msg = "decrypt data"
+ break;
+ }
+
+ return new Promise((resolve, reject) => {
+ evStack.value.push({
+ ...ev,
+ allow: () => resolve(true),
+ close: () => resolve(false),
+ })
+ })
+})
+
+
+</script>
+
+<style lang="scss">
+
+
+</style>
diff --git a/extension/src/entries/contentScript/primary/main.js b/extension/src/entries/contentScript/primary/main.js
new file mode 100644
index 0000000..24ef4ef
--- /dev/null
+++ b/extension/src/entries/contentScript/primary/main.js
@@ -0,0 +1,41 @@
+// 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 { createApp } from "vue";
+import renderContent from "../renderContent";
+import App from "./App.vue";
+import Notification from '@kyvg/vue3-notification'
+import '@fontsource/noto-sans-masaram-gondi'
+
+//We need inline styles to inject into the shadow dom
+import tw from "~/assets/tailwind.scss?inline";
+import localStyle from './style.scss?inline'
+
+renderContent([], (appRoot, shadowRoot) => {
+ createApp(App)
+ .use(Notification)
+ .mount(appRoot);
+
+ //Add tailwind styles just to the shadow dom element
+ const style = document.createElement('style')
+ style.innerHTML = tw.toString()
+ shadowRoot.appendChild(style)
+
+ //Add local styles
+ const style2 = document.createElement('style')
+ style2.innerHTML = localStyle.toString()
+ shadowRoot.appendChild(style2)
+}); \ No newline at end of file
diff --git a/extension/src/entries/contentScript/primary/style.scss b/extension/src/entries/contentScript/primary/style.scss
new file mode 100644
index 0000000..bcdbbfd
--- /dev/null
+++ b/extension/src/entries/contentScript/primary/style.scss
@@ -0,0 +1,15 @@
+
+#injected-root{
+
+ .toaster{
+ @apply fixed top-10 right-2 z-[999999999] max-w-[250px];
+ }
+
+ .vue-notification-template.vue-notification.error{
+ @apply bg-red-500 text-white px-4 py-2;
+
+ .notification-title{
+
+ }
+ }
+} \ No newline at end of file
diff --git a/extension/src/entries/contentScript/renderContent.js b/extension/src/entries/contentScript/renderContent.js
new file mode 100644
index 0000000..84c5b9f
--- /dev/null
+++ b/extension/src/entries/contentScript/renderContent.js
@@ -0,0 +1,48 @@
+// 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";
+
+export default async function renderContent(
+ cssPaths,
+ render = (_appRoot) => {}
+) {
+ const appContainer = document.createElement("div");
+ const shadowRoot = appContainer.attachShadow({
+ mode: import.meta.env.DEV ? "open" : "closed",
+ });
+ const appRoot = document.createElement("div");
+
+ if (import.meta.hot) {
+ const { addViteStyleTarget } = await import(
+ "@samrum/vite-plugin-web-extension/client"
+ );
+
+ await addViteStyleTarget(shadowRoot);
+ } else {
+ cssPaths.forEach((cssPath) => {
+ const styleEl = document.createElement("link");
+ styleEl.setAttribute("rel", "stylesheet");
+ styleEl.setAttribute("href", runtime.getURL(cssPath));
+ shadowRoot.appendChild(styleEl);
+ });
+ }
+
+ shadowRoot.appendChild(appRoot);
+ document.body.appendChild(appContainer);
+
+ render(appRoot, shadowRoot);
+} \ No newline at end of file