aboutsummaryrefslogtreecommitdiff
path: root/extension/src
diff options
context:
space:
mode:
authorLibravatar vnugent <public@vaughnnugent.com>2023-11-19 14:50:46 -0500
committerLibravatar vnugent <public@vaughnnugent.com>2023-11-19 14:50:46 -0500
commitbc7b86a242673d7831f6105d000995d9f4d63e09 (patch)
tree8da5c92047e92174b80ff6f460f8c3148e1e00ca /extension/src
parent0b609c17199e937518c42365b360288acfa872be (diff)
hasty not working update to get my workspace clean
Diffstat (limited to 'extension/src')
-rw-r--r--extension/src/assets/inputs.scss2
-rw-r--r--extension/src/bg-api/bg-api.ts145
-rw-r--r--extension/src/bg-api/content-script.ts48
-rw-r--r--extension/src/bg-api/options.ts80
-rw-r--r--extension/src/bg-api/popup.ts66
-rw-r--r--extension/src/bg-api/types.ts50
-rw-r--r--extension/src/entries/background/auth-api.ts161
-rw-r--r--extension/src/entries/background/history.ts82
-rw-r--r--extension/src/entries/background/identity-api.ts59
-rw-r--r--extension/src/entries/background/main.ts119
-rw-r--r--extension/src/entries/background/nostr-api.ts110
-rw-r--r--extension/src/entries/background/server-api/index.ts84
-rw-r--r--extension/src/entries/background/settings.ts125
-rw-r--r--extension/src/entries/background/types.ts76
-rw-r--r--extension/src/entries/contentScript/nostr-shim.js73
-rw-r--r--extension/src/entries/contentScript/primary/components/PromptPopup.vue19
-rw-r--r--extension/src/entries/contentScript/primary/main.js17
-rw-r--r--extension/src/entries/options/App.vue48
-rw-r--r--extension/src/entries/options/components/Identities.vue56
-rw-r--r--extension/src/entries/options/components/Privacy.vue61
-rw-r--r--extension/src/entries/options/components/SiteSettings.vue118
-rw-r--r--extension/src/entries/options/main.js17
-rw-r--r--extension/src/entries/popup/App.vue2
-rw-r--r--extension/src/entries/popup/Components/IdentitySelection.vue20
-rw-r--r--extension/src/entries/popup/Components/Login.vue16
-rw-r--r--extension/src/entries/popup/Components/PageContent.vue113
-rw-r--r--extension/src/entries/popup/local.scss18
-rw-r--r--extension/src/entries/popup/main.js15
-rw-r--r--extension/src/entries/store/allowedOrigins.ts43
-rw-r--r--extension/src/entries/store/features.ts116
-rw-r--r--extension/src/entries/store/identity.ts43
-rw-r--r--extension/src/entries/store/index.ts60
-rw-r--r--extension/src/entries/store/types.ts9
-rw-r--r--extension/src/features/account-api.ts190
-rw-r--r--extension/src/features/auth-api.ts123
-rw-r--r--extension/src/features/framework/index.ts214
-rw-r--r--extension/src/features/history.ts42
-rw-r--r--extension/src/features/identity-api.ts112
-rw-r--r--extension/src/features/index.ts33
-rw-r--r--extension/src/features/nip07allow-api.ts181
-rw-r--r--extension/src/features/nostr-api.ts79
-rw-r--r--extension/src/features/permissions.ts (renamed from extension/src/entries/background/permissions.ts)4
-rw-r--r--extension/src/features/server-api/endpoints.ts (renamed from extension/src/entries/background/server-api/endpoints.ts)12
-rw-r--r--extension/src/features/server-api/index.ts137
-rw-r--r--extension/src/features/settings.ts165
-rw-r--r--extension/src/features/tagfilter-api.ts125
-rw-r--r--extension/src/features/types.ts147
-rw-r--r--extension/src/manifest.js5
48 files changed, 2219 insertions, 1421 deletions
diff --git a/extension/src/assets/inputs.scss b/extension/src/assets/inputs.scss
index 62d03d4..05c8d33 100644
--- a/extension/src/assets/inputs.scss
+++ b/extension/src/assets/inputs.scss
@@ -1,7 +1,7 @@
input.input,
select.input,
textarea.input {
- @apply duration-100 ease-in-out outline-none border px-2 py-1.5;
+ @apply duration-100 ease-in-out outline-none border px-1.5 py-1;
@apply border-gray-200 bg-inherit dark:border-dark-400 dark:text-white hover:border-gray-300 hover:dark:border-dark-200;
}
diff --git a/extension/src/bg-api/bg-api.ts b/extension/src/bg-api/bg-api.ts
deleted file mode 100644
index 86e6b68..0000000
--- a/extension/src/bg-api/bg-api.ts
+++ /dev/null
@@ -1,145 +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 { assign, isEqual, orderBy, transform } from 'lodash'
-import { apiCall, debugLog } from "@vnuge/vnlib.browser"
-import { reactive, toRefs } from "vue"
-import { useWindowFocus } from '@vueuse/core'
-import { NostrPubKey } from '../entries/background/types'
-import { ClientStatus, NostrIdentiy, SendMessageHandler, UseStatusResult, PluginConfig } from './types'
-
-
-//Hold local status
-const status = reactive<ClientStatus>({
- loggedIn: false,
- userName: '',
- selectedKey: undefined,
- darkMode: true
-})
-
-const focused = useWindowFocus()
-
-const updateStatusAsync = async (sendMessage: SendMessageHandler) => {
-
- //Get the status from the background script
- const result = await sendMessage<ClientStatus>('getStatus', {}, 'background')
-
- const ls = status as any;
- const res = result as any;
-
- //Check if the status has changed
- for (const key in result) {
- if (!isEqual(ls[key], res)){
- //Update the status and break
- assign(status, result)
- break;
- }
- }
-
- //Get the selected publicKey
- const selected = await sendMessage<NostrPubKey>('getPublicKey', {}, 'background')
-
- if(!isEqual(status.selectedKey, selected)){
- debugLog('Selected key changed')
- assign(status, { selectedKey: selected })
- }
-}
-
-/**
- * Keeps a reactive status object that up to date with the background script
- * @returns {Readonly<Ref<{}>>}
- */
-export const useStatus = (sendMessage: SendMessageHandler, bypassFocus : boolean): UseStatusResult => {
- //Configure timer get status from the background, only when the window is focused
- setInterval(() => (bypassFocus || focused.value) ? updateStatusAsync(sendMessage) : null, 200);
-
- //return a refs object
- return {
- toRefs: () => toRefs(status),
- update: () => updateStatusAsync(sendMessage)
- }
-}
-
-export const useManagment = (sendMessage: SendMessageHandler) =>{
-
-
- const getProfile = async () => {
- //Send the login request to the background script
- return await apiCall(async () => await sendMessage('getProfile', {}, 'background'))
- }
-
- const getAllKeys = async (): Promise<NostrPubKey[]> => {
- //Send the login request to the background script
- const keys = (await apiCall(async () => await sendMessage('getAllKeys', {}, 'background')) ?? []) as NostrPubKey[]
-
- const formattedKeys = transform(keys, (result, key) => {
- result.push({
- ...key,
- Created: new Date(key.Created).toLocaleString(),
- LastModified: new Date(key.LastModified).toLocaleString()
- })
- }, [] as NostrPubKey[])
-
- return orderBy(formattedKeys, 'Created', 'desc')
- }
-
- const selectKey = async (key: NostrPubKey) => {
- await apiCall(async () => {
- //Send the login request to the background script
- await sendMessage('selectKey', { ...key }, 'background')
- })
- //Update the status after the key is selected
- updateStatusAsync(sendMessage)
- }
-
- const createIdentity = async (identity: NostrIdentiy) => {
- await apiCall(async ({toaster}) => {
- //Send the login request to the background script
- await sendMessage('createIdentity', { ...identity }, 'background')
- toaster.form.success({
- title: 'Success',
- text: 'Identity created successfully'
- })
- })
- }
-
- const updateIdentity = async (identity: NostrIdentiy) => {
- await apiCall(async ({toaster}) => {
- //Send the login request to the background script
- await sendMessage('updateIdentity', { ...identity }, 'background')
- toaster.form.success({
- title: 'Success',
- text: 'Identity updated successfully'
- })
- })
- }
-
- const getSiteConfig = async (): Promise<PluginConfig | undefined> => {
- return await apiCall(async () => {
- //Send the login request to the background script
- return await sendMessage<PluginConfig>('getSiteConfig', {}, 'background')
- })
- }
-
- return {
- getProfile,
- getAllKeys,
- selectKey,
- createIdentity,
- updateIdentity,
- getSiteConfig
- }
-} \ No newline at end of file
diff --git a/extension/src/bg-api/content-script.ts b/extension/src/bg-api/content-script.ts
deleted file mode 100644
index 7b64e81..0000000
--- a/extension/src/bg-api/content-script.ts
+++ /dev/null
@@ -1,48 +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 { apiCall } from "@vnuge/vnlib.browser";
-import { useManagment as _mgmt, useStatus as _sts } from "./bg-api"
-import { sendMessage } from "webext-bridge/content-script"
-
-export const useStatus = (() => {
- const status = _sts(sendMessage, false);
-
- return () => {
- const refs = status.toRefs();
- //run status when called and dont await
- status.update();
- return refs;
- }
-})()
-
-export const useManagment = (() => {
- const mgmt = _mgmt(sendMessage);
-
- const isEnabledSite = async () => {
- await apiCall(async ({ toaster }) => {
-
- //Send the login request to the background script
- const data = await sendMessage('isSiteEnabled', { }, 'background')
- })
- }
-
- return () => {
- return {
- ...mgmt,
- }
- }
-})()
diff --git a/extension/src/bg-api/options.ts b/extension/src/bg-api/options.ts
deleted file mode 100644
index 4313f35..0000000
--- a/extension/src/bg-api/options.ts
+++ /dev/null
@@ -1,80 +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 { apiCall } from "@vnuge/vnlib.browser";
-import { useManagment as _mgmt, useStatus as _sts } from "./bg-api"
-import { sendMessage } from "webext-bridge/options"
-import { truncate } from "lodash";
-import { NostrIdentiy, PluginConfig } from "./types";
-
-enum HistoryType {
- get = 'get',
- clear = 'clear',
- remove = 'remove',
- push = 'push'
-}
-
-interface HistoryMessage{
- readonly action: string,
- readonly event?: any
-}
-
-export const useManagment = (() => {
- const mgmt = _mgmt(sendMessage);
-
- const saveSiteConfig = async (config: PluginConfig) => {
- await apiCall(async ({ toaster }) => {
- //Send the login request to the background script
- await sendMessage('setSiteConfig', { ...config }, 'background')
-
- toaster.form.info({
- title: 'Saved',
- text: 'Site config saved'
- })
- })
- }
-
- const deleteIdentity = async (key: NostrIdentiy) => {
- await apiCall(async ({ toaster }) => {
- //Delete the desired key async, if it fails it will throw
- await sendMessage('deleteKey', { ...key }, 'background')
-
- toaster.form.success({
- title: 'Success',
- text: `Successfully delete key ${truncate(key.Id, { length: 7 })}`
- })
- })
- }
-
- return () => {
- return {
- ...mgmt,
- saveSiteConfig,
- deleteIdentity
- }
- }
-})()
-
-export const useStatus = (() => {
- //Bypass the window focus check for the options page
- const status = _sts(sendMessage, true);
- return () => {
- const refs = status.toRefs();
- //run status when called and dont await
- status.update();
- return refs;
- }
-})()
diff --git a/extension/src/bg-api/popup.ts b/extension/src/bg-api/popup.ts
deleted file mode 100644
index f81bcfb..0000000
--- a/extension/src/bg-api/popup.ts
+++ /dev/null
@@ -1,66 +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 { useManagment as _mgmt, useStatus as _sts } from "./bg-api"
-import { sendMessage } from "webext-bridge/popup"
-import { apiCall, debugLog } from "@vnuge/vnlib.browser"
-
-export const useManagment = (() =>{
- const mgmt = _mgmt(sendMessage);
-
- const login = async (token: string) => {
- await apiCall(async ({ toaster }) => {
-
- //Send the login request to the background script
- await sendMessage('login', { token }, 'background')
-
- toaster.form.success({
- title: 'Success',
- text: 'Logged in successfully'
- })
- })
- }
-
- const logout = async () => {
- await apiCall(async ({ toaster }) => {
- //Send the login request to the background script
- await sendMessage('logout', {}, 'background')
-
- toaster.form.success({
- title: 'Success',
- text: 'Successfully logged out'
- })
- })
- }
-
- return () => {
- return {
- ...mgmt,
- login,
- logout
- }
- }
-})()
-
-export const useStatus = (() =>{
- const status = _sts(sendMessage, false);
-
- return () => {
- const refs = status.toRefs();
- //run status when called and dont await
- status.update();
- return refs
- }
-})() \ No newline at end of file
diff --git a/extension/src/bg-api/types.ts b/extension/src/bg-api/types.ts
deleted file mode 100644
index 6fc2f84..0000000
--- a/extension/src/bg-api/types.ts
+++ /dev/null
@@ -1,50 +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 { ToRefs } from 'vue';
-import { NostrPubKey } from '../entries/background/types'
-import { JsonObject } from "type-fest";
-
-export interface ClientStatus extends JsonObject {
- readonly loggedIn: boolean;
- readonly userName: string;
- readonly selectedKey?: NostrPubKey;
- readonly darkMode: boolean;
-}
-
-export interface NostrIdentiy extends NostrPubKey {
- readonly UserName: string;
- readonly ExistingKey: string;
-}
-
-export interface SendMessageHandler {
- <T extends JsonObject>(action: string, data: any, context: string): Promise<T>
-}
-
-export interface UseStatusResult {
- toRefs: () => ToRefs<ClientStatus>,
- update: () => Promise<void>
-}
-
-export interface PluginConfig extends JsonObject {
- readonly apiUrl: string;
- readonly accountBasePath: string;
- readonly nostrEndpoint: string;
- readonly heartbeat: boolean;
- readonly maxHistory: number;
- readonly darkMode: boolean;
- readonly autoInject: boolean;
-} \ No newline at end of file
diff --git a/extension/src/entries/background/auth-api.ts b/extension/src/entries/background/auth-api.ts
deleted file mode 100644
index 64a46e4..0000000
--- a/extension/src/entries/background/auth-api.ts
+++ /dev/null
@@ -1,161 +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 { debugLog, useAxios, usePkiAuth, useSession, useSessionUtils, useUser } from "@vnuge/vnlib.browser";
-import { AxiosInstance } from "axios";
-import { runtime } from "webextension-polyfill";
-import { BridgeMessage } from "webext-bridge";
-import { useSettings } from "./settings";
-import { JsonObject } from "type-fest";
-import { ClientStatus, LoginMessage } from "./types";
-
-interface ApiHandle {
- axios: AxiosInstance
-}
-
-export interface ProectedHandler<T extends JsonObject> {
- (message: T): Promise<any>
-}
-
-export interface MessageHandler<T extends JsonObject> {
- (message: T): Promise<any>
-}
-
-export interface ApiMessageHandler<T extends JsonObject> {
- (message: T, apiHandle: { axios: AxiosInstance }): Promise<any>
-}
-
-
-export const useAuthApi = (() => {
-
- const { loggedIn } = useSession();
- const { clearLoginState } = useSessionUtils();
- const { logout, getProfile, heartbeat, userName } = useUser();
- const { currentConfig } = useSettings();
-
- const apiCall = async <T>(asyncFunc: (h: ApiHandle) => Promise<T>): Promise<T> => {
- try {
- //Get configured axios instance from vnlib
- const axios = useAxios(null);
-
- //Exec the async function
- return await asyncFunc({ axios })
- } catch (errMsg) {
- debugLog(errMsg)
- // See if the error has an axios response
- throw { ...errMsg };
- }
- }
-
- const handleMessage = <T extends JsonObject> (cbHandler: MessageHandler<T>) => {
- return (message: BridgeMessage<T>): Promise<any> => {
- return cbHandler(message.data)
- }
- }
-
- const handleProtectedMessage = <T extends JsonObject> (cbHandler: ProectedHandler<T>) => {
- return (message: BridgeMessage<T>): Promise<any> => {
- if (message.sender.context === 'options' || message.sender.context === 'popup') {
- return cbHandler(message.data)
- }
- throw new Error('Unauthorized')
- }
- }
-
- const handleApiCall = <T extends JsonObject>(cbHandler: ApiMessageHandler<T>) => {
- return (message: BridgeMessage<T>): Promise<any> => {
- return apiCall((m) => cbHandler(message.data, m))
- }
- }
-
- const handleProtectedApicall = <T extends JsonObject>(cbHandler: ApiMessageHandler<T>) => {
- return (message: BridgeMessage<T>): Promise<any> => {
- if (message.sender.context === 'options' || message.sender.context === 'popup') {
- return apiCall((m) => cbHandler(message.data, m))
- }
- throw new Error('Unauthorized')
- }
- }
-
- const onLogin = handleProtectedApicall(async (data : LoginMessage): Promise<any> => {
- //Perform login
- const { login } = usePkiAuth(`${currentConfig.value.accountBasePath}/pki`);
- await login(data.token)
- return true;
- })
-
- const onLogout = handleProtectedApicall(async () : Promise<void> => {
- await logout()
- //Cleanup after logout
- clearLoginState()
- })
-
- const onGetProfile = handleProtectedApicall(() : Promise<any> => getProfile())
-
- const onGetStatus = async (): Promise<ClientStatus> => {
- return {
- //Logged in if the cookie is set and the api flag is set
- loggedIn: loggedIn.value,
- //username
- userName: userName.value,
- //dark mode flag
- darkMode: currentConfig.value.darkMode
- }
- }
-
- //We can send post messages to the server heartbeat endpoint to get status
- const runHeartbeat = async () => {
- //Only run if the api thinks its logged in, and config is enabled
- if (!loggedIn.value || currentConfig.value.heartbeat !== true) {
- return
- }
-
- try {
- //Post against the heartbeat endpoint
- await heartbeat()
- }
- catch (error) {
- if (error.response?.status === 401 || error.response?.status === 403) {
- //If we get a 401, the user is no longer logged in
- clearLoginState()
- }
- }
- }
-
- //Setup autoheartbeat
- runtime.onInstalled.addListener(async () => {
- //Configure interval to run every 5 minutes to update the status
- setInterval(runHeartbeat, 60 * 1000);
-
- //Run immediately
- runHeartbeat();
- });
-
- return () => {
- return{
- loggedIn,
- apiCall,
- handleMessage,
- handleProtectedMessage,
- handleApiCall,
- handleProtectedApicall,
- userName,
- onLogin,
- onLogout,
- onGetProfile,
- onGetStatus
- }
- }
-})() \ No newline at end of file
diff --git a/extension/src/entries/background/history.ts b/extension/src/entries/background/history.ts
deleted file mode 100644
index b3f3733..0000000
--- a/extension/src/entries/background/history.ts
+++ /dev/null
@@ -1,82 +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, storage } from "webextension-polyfill";
-import { useSettings } from "./settings";
-import { isEqual, remove } from "lodash";
-import { ref } from "vue";
-
-const evHistory = ref([]);
-
-export interface HistoryEvent extends Object{
-
-}
-
-export const useHistory = (() => {
- const { currentConfig } = useSettings();
-
- const pushEvent = (event: HistoryEvent) => {
-
- //Limit the history to 50 events
- if (evHistory.value.length > currentConfig.value.maxHistory) {
- evHistory.value.shift();
- }
-
- evHistory.value.push(event);
-
- //Save the history but dont wait for it
- storage.local.set({ eventHistory: evHistory });
- }
-
- const getHistory = (): HistoryEvent[] => {
- return [...evHistory.value];
- }
-
- const clearHistory = () => {
- evHistory.value.length = 0;
- storage.local.set({ eventHistory: evHistory });
- }
-
- const removeItem = (event: HistoryEvent) => {
- //Remove the event from the history
- remove(evHistory.value, (ev) => isEqual(ev, event));
- //Save the history but dont wait for it
- storage.local.set({ eventHistory: evHistory });
- }
-
- const onStartup = async () => {
- //Recover the history array
- const { eventHistory } = await storage.local.get('eventHistory');
-
- //Push the history into the array
- evHistory.value.push(...eventHistory);
- }
-
- //Reload the history on startup
- runtime.onStartup.addListener(onStartup);
- runtime.onInstalled.addListener(onStartup);
-
- return () =>{
- return {
- pushEvent,
- getHistory,
- clearHistory,
- removeItem
- }
- }
-})()
-
-
-//Listen for messages \ No newline at end of file
diff --git a/extension/src/entries/background/identity-api.ts b/extension/src/entries/background/identity-api.ts
deleted file mode 100644
index c835f7c..0000000
--- a/extension/src/entries/background/identity-api.ts
+++ /dev/null
@@ -1,59 +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 { NostrIdentiy } from "../../bg-api/types";
-import { useAuthApi } from "./auth-api";
-import { useSettings } from "./settings";
-
-export const useIdentityApi = (() => {
-
- const { handleProtectedApicall } = useAuthApi();
- const { currentConfig } = useSettings();
-
- const onCreateIdentity = handleProtectedApicall<NostrIdentiy>(async (data, { axios }) => {
- //Create a new identity
- const response = await axios.put(`${currentConfig.value.nostrEndpoint}?type=identity`, data)
-
- if (response.data.success) {
- return response.data.result;
- }
-
- //If we get here, the login failed
- throw { response }
- })
-
- const onUpdateIdentity = handleProtectedApicall(async (data, { axios }) => {
- delete data.Created;
- delete data.LastModified;
-
- //Create a new identity
- const response = await axios.patch(`${currentConfig.value.nostrEndpoint}?type=identity`, data)
-
- if (response.data.success) {
- return response.data.result;
- }
-
- //If we get here, the login failed
- throw { response }
- })
-
- return () =>{
- return{
- onCreateIdentity,
- onUpdateIdentity
- }
- }
-
-})() \ No newline at end of file
diff --git a/extension/src/entries/background/main.ts b/extension/src/entries/background/main.ts
index 7573b7a..1d74f26 100644
--- a/extension/src/entries/background/main.ts
+++ b/extension/src/entries/background/main.ts
@@ -13,93 +13,32 @@
// 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 { useHistory } from "./history";
-import { useNostrApi } from "./nostr-api";
-import { useIdentityApi } from "./identity-api";
-import { useSettings } from "./settings";
-import { onMessage } from "webext-bridge/background";
-import { useAuthApi } from "./auth-api";
-import { JsonObject } from "type-fest";
-
-//Init the history api
-useHistory();
-
-runtime.onInstalled.addListener(() => {
- console.info("Extension installed successfully");
-});
-
-
-//Register settings handlers
-const { onGetSiteConfig, onSetSitConfig } = useSettings();
-
-onMessage('getSiteConfig', onGetSiteConfig);
-onMessage('setSiteConfig', onSetSitConfig);
-
-//Register the api handlers
-const { onGetProfile, onGetStatus, onLogin, onLogout, handleProtectedMessage } = useAuthApi();
-
-onMessage('getProfile', onGetProfile);
-onMessage('getStatus', onGetStatus);
-onMessage('login', onLogin);
-onMessage('logout', onLogout);
-
-//Register the identity handlers
-const { onCreateIdentity, onUpdateIdentity } = useIdentityApi();
-
-onMessage('createIdentity', onCreateIdentity);
-onMessage('updateIdentity', onUpdateIdentity);
-
-//Register the nostr handlers
-const {
- onGetPubKey,
- onSelectKey,
- onSignEvent,
- onGetAllKeys,
- onGetRelays,
- onNip04Decrypt,
- onNip04Encrypt,
- onDeleteKey,
- onSetRelay
-} = useNostrApi();
-
-onMessage('getPublicKey', onGetPubKey);
-onMessage('selectKey', onSelectKey);
-onMessage('signEvent', onSignEvent);
-onMessage('getAllKeys', onGetAllKeys);
-onMessage('getRelays', onGetRelays);
-onMessage('setRelay', onSetRelay);
-onMessage('deleteKey', onDeleteKey);
-onMessage('nip04.decrypt', onNip04Decrypt);
-onMessage('nip04.encrypt', onNip04Encrypt);
-
-//Use history api
-const { getHistory, clearHistory, removeItem, pushEvent } = useHistory();
-
-enum HistoryType {
- get = 'get',
- clear = 'clear',
- remove = 'remove',
- push = 'push'
-}
-
-interface HistoryMessage extends JsonObject {
- action: HistoryType,
- event: string
-}
-
-onMessage<HistoryMessage>('history', handleProtectedMessage(async (data) =>{
- switch(data.action){
- case HistoryType.get:
- return getHistory();
- case HistoryType.clear:
- clearHistory();
- break;
- case HistoryType.remove:
- removeItem(data.event);
- break;
- case HistoryType.push:
- pushEvent(data.event);
- break;
- }
-})) \ No newline at end of file
+import {
+ useHistoryApi,
+ useNostrApi,
+ useIdentityApi,
+ useSettingsApi,
+ useAuthApi,
+ useLocalPki,
+ useAppSettings,
+ usePkiApi,
+ useEventTagFilterApi,
+ useInjectAllowList
+} from "../../features";
+import { useBackgroundFeatures } from "../../features/framework";
+
+const settings = useAppSettings();
+const { register } = useBackgroundFeatures(settings)
+
+//Resgiter background features
+register([
+ useSettingsApi,
+ useAuthApi,
+ useIdentityApi,
+ useHistoryApi,
+ useNostrApi,
+ useLocalPki,
+ usePkiApi,
+ useEventTagFilterApi,
+ useInjectAllowList
+]) \ No newline at end of file
diff --git a/extension/src/entries/background/nostr-api.ts b/extension/src/entries/background/nostr-api.ts
deleted file mode 100644
index 3c862ff..0000000
--- a/extension/src/entries/background/nostr-api.ts
+++ /dev/null
@@ -1,110 +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 { useSettings } from "./settings";
-import { useAuthApi } from "./auth-api";
-import { computed, ref, watch } from "vue";
-
-import { find, isArray } from "lodash";
-import { Endpoints, initApi } from "./server-api";
-import { NostrRelay, NostrPubKey, EventMessage, NostrEvent } from './types'
-
-export const useNostrApi = (() => {
-
- const { currentConfig } = useSettings();
- const { handleProtectedApicall, handleApiCall, handleProtectedMessage, loggedIn } = useAuthApi();
-
- const nostrUrl = computed(() => currentConfig.value.nostrEndpoint || '/nostr')
-
- //Init the api endpooints
- const { execRequest } = initApi(nostrUrl);
-
- //Get the current selected key
- const selectedKey = ref<NostrPubKey | null>({} as NostrPubKey)
-
- //Selected key is allowed from content script
- const onGetPubKey = () => ({ ...selectedKey.value });
-
- const onDeleteKey = handleProtectedApicall<NostrPubKey>(data => execRequest<NostrPubKey>(Endpoints.DeleteKey, data))
-
- //Set the selected key to the value
- const onSelectKey = handleProtectedMessage<NostrPubKey>(data => (selectedKey.value = data, Promise.resolve()));
-
- const onGetAllKeys = handleProtectedApicall(async () => {
- //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)) {
- selectedKey.value = null;
- }
- }
-
- return [...data]
- })
-
- //Unprotect the signing handler so it can be called from the content script
- const onSignEvent = handleApiCall<EventMessage>(async (data) => {
- //Set the key id from our current selection
- data.event.KeyId = selectedKey.value?.Id || ''; //Pass key selection error to server
- //Sign the event
- const event = await execRequest<NostrEvent>(Endpoints.SignEvent, data.event);
- return { event };
- })
-
- const onGetRelays = handleApiCall<any>(async () => {
- //Get preferred relays for the current user
- const data = await execRequest<NostrRelay[]>(Endpoints.GetRelays)
- return [...data]
- })
-
- const onSetRelay = handleProtectedApicall<NostrRelay>(data => execRequest<NostrRelay>(Endpoints.SetRelay, data));
-
-
- const onNip04Encrypt = handleProtectedMessage(async (data) => {
- console.log('nip04.encrypt', data)
- return { ciphertext: 'ciphertext' }
- })
-
- const onNip04Decrypt = handleProtectedMessage(async (data) => {
- console.log('nip04.decrypt', data)
- return { plaintext: 'plaintext' }
- })
-
- //Clear the selected key if the user logs out
- watch(loggedIn, (li) => li ? null : selectedKey.value = null)
-
- return () => {
- return{
- selectedKey,
- nostrUrl,
- onGetPubKey,
- onSelectKey,
- onGetAllKeys,
- onSignEvent,
- onGetRelays,
- onSetRelay,
- onNip04Encrypt,
- onNip04Decrypt,
- onDeleteKey
- }
- }
-})() \ No newline at end of file
diff --git a/extension/src/entries/background/server-api/index.ts b/extension/src/entries/background/server-api/index.ts
deleted file mode 100644
index 84598df..0000000
--- a/extension/src/entries/background/server-api/index.ts
+++ /dev/null
@@ -1,84 +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 { Ref } from "vue"
-import { get } from '@vueuse/core'
-import { WebMessage } from "@vnuge/vnlib.browser"
-import { initEndponts } from "./endpoints"
-import { NostrEvent, NostrRelay } from "../types"
-import { NostrIdentiy } from "../../../bg-api/types"
-
-export enum Endpoints {
- GetKeys = 'getKeys',
- DeleteKey = 'deleteKey',
- SignEvent = 'signEvent',
- GetRelays = 'getRelays',
- SetRelay = 'setRelay',
- Encrypt = 'encrypt',
- Decrypt = 'decrypt',
-}
-
-export const initApi = (nostrUrl: Ref<string>) => {
- const { registerEndpoint, execRequest } = initEndponts()
-
- registerEndpoint({
- id: Endpoints.GetKeys,
- method: 'GET',
- path: () => `${get(nostrUrl)}?type=getKeys`,
- onRequest: () => Promise.resolve(),
- onResponse: (response) => Promise.resolve(response)
- })
-
- registerEndpoint({
- id: Endpoints.DeleteKey,
- method: 'DELETE',
- path: (key:NostrIdentiy) => `${get(nostrUrl)}?type=identity&key_id=${key.Id}`,
- onRequest: () => Promise.resolve(),
- onResponse: (response: WebMessage) => response.getResultOrThrow()
- })
-
- registerEndpoint({
- id: Endpoints.SignEvent,
- method: 'POST',
- path: () => `${get(nostrUrl)}?type=signEvent`,
- onRequest: (event) => Promise.resolve(event),
- onResponse: async (response: WebMessage<NostrEvent>) => {
- const res = response.getResultOrThrow()
- delete res.KeyId;
- return res;
- }
- })
-
- registerEndpoint({
- id: Endpoints.GetRelays,
- method: 'GET',
- path: () => `${get(nostrUrl)}?type=getRelays`,
- onRequest: () => Promise.resolve(),
- onResponse: (response) => Promise.resolve(response)
- })
-
- registerEndpoint({
- id: Endpoints.SetRelay,
- method: 'POST',
- path: () => `${get(nostrUrl)}?type=relay`,
- onRequest: (relay:NostrRelay) => Promise.resolve(relay),
- onResponse: (response) => Promise.resolve(response)
- })
-
- return {
- execRequest
- }
-} \ No newline at end of file
diff --git a/extension/src/entries/background/settings.ts b/extension/src/entries/background/settings.ts
deleted file mode 100644
index 98d6aa6..0000000
--- a/extension/src/entries/background/settings.ts
+++ /dev/null
@@ -1,125 +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, storage } from "webextension-polyfill"
-import { isEmpty, isEqual, merge } from 'lodash'
-import { configureApi, debugLog } from '@vnuge/vnlib.browser'
-import { readonly, ref } from "vue";
-import { BridgeMessage } from "webext-bridge";
-import { JsonObject } from "type-fest";
-
-export interface PluginConfig extends JsonObject {
- readonly apiUrl: string;
- readonly accountBasePath: string;
- readonly nostrEndpoint: string;
- readonly heartbeat: boolean;
- readonly maxHistory: number;
- readonly darkMode: boolean;
- readonly autoInject: boolean;
-}
-
-//Default storage config
-const defaultConfig : PluginConfig = {
- apiUrl: import.meta.env.VITE_API_URL,
- accountBasePath: import.meta.env.VITE_ACCOUNTS_BASE_PATH,
- nostrEndpoint: import.meta.env.VITE_NOSTR_ENDPOINT,
- heartbeat: import.meta.env.VITE_HEARTBEAT_ENABLED === 'true',
- maxHistory: 50,
- darkMode: false,
- autoInject: true
-};
-
-export const useSettings = (() =>{
-
- const currentConfig = ref<PluginConfig>({} as PluginConfig);
-
- const getCurrentConfig = async () => {
- const { siteConfig } = await storage.local.get('siteConfig');
-
- //Store a default config if none exists
- if(isEmpty(siteConfig)){
- await storage.local.set({ siteConfig: defaultConfig });
- }
-
- //Merge the default config with the site config
- return merge(defaultConfig, siteConfig)
- }
-
- const restoreApiSettings = async () => {
- //Set the current config
- currentConfig.value = await getCurrentConfig();;
-
- //Configure the vnlib api
- configureApi({
- session: {
- cookiesEnabled: false,
- bidSize: 32,
- storage: localStorage
- },
- user: {
- accountBasePath: currentConfig.value.accountBasePath,
- storage: localStorage,
- },
- axios: {
- baseURL: currentConfig.value.apiUrl,
- tokenHeader: import.meta.env.VITE_WEB_TOKEN_HEADER,
- }
- })
- }
-
- const saveConfig = async (config: PluginConfig) : Promise<void> => {
- await storage.local.set({ siteConfig: config });
- }
-
- const onGetSiteConfig = async ({ } :BridgeMessage<any>): Promise<PluginConfig> => {
- return { ...currentConfig.value }
- }
-
- const onSetSitConfig = async ({ sender, data }: BridgeMessage<PluginConfig>) : Promise<void> => {
- //Config messages should only come from the options page
- if (sender.context !== 'options') {
- throw new Error('Unauthorized');
- }
-
- //Save the config
- await saveConfig(data);
-
- //Restore the api settings
- restoreApiSettings();
-
- debugLog('Config settings saved!');
- }
-
- runtime.onInstalled.addListener(() => {
- restoreApiSettings();
- debugLog('Server settings restored from storage');
- });
-
- runtime.onConnect.addListener(async () => {
- //refresh the config on connect
- currentConfig.value = await getCurrentConfig();
- })
-
- return () =>{
- return{
- getCurrentConfig,
- restoreApiSettings,
- saveConfig,
- currentConfig:readonly(currentConfig),
- onGetSiteConfig,
- onSetSitConfig
- }
- }
-})() \ No newline at end of file
diff --git a/extension/src/entries/background/types.ts b/extension/src/entries/background/types.ts
deleted file mode 100644
index d459ea1..0000000
--- a/extension/src/entries/background/types.ts
+++ /dev/null
@@ -1,76 +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 { JsonObject } from "type-fest";
-
-export interface NostrPubKey extends JsonObject {
- readonly Id: string,
- readonly UserName: string,
- readonly PublicKey: string,
- readonly Created: string,
- readonly LastModified: string
-}
-
-export interface NostrEvent extends JsonObject {
- KeyId: string,
- readonly id: string,
- readonly pubkey: string,
- readonly content: string,
-}
-
-export interface EventMessage extends JsonObject {
- readonly event: NostrEvent
-}
-
-export interface NostrRelay extends JsonObject {
- readonly Id: string,
- readonly url: string,
- readonly flags: number,
- readonly Created: string,
- readonly LastModified: string
-}
-
-export interface LoginMessage extends JsonObject {
- readonly token: string
-}
-
-export interface ClientStatus {
- readonly loggedIn: boolean;
- readonly userName: string | null;
- readonly darkMode: boolean;
-}
-
-export enum NostrRelayFlags {
- None = 0,
- Default = 1,
- Preferred = 2,
-}
-
-export enum NostrRelayMessageType{
- updateRelay = 1,
- addRelay = 2,
- deleteRelay = 3
-}
-
-export interface NostrRelayMessage extends JsonObject {
- readonly relay: NostrRelay
- readonly type: NostrRelayMessageType
-}
-
-export interface LoginMessage extends JsonObject {
- readonly token: string
- readonly username: string
- readonly password: string
-} \ No newline at end of file
diff --git a/extension/src/entries/contentScript/nostr-shim.js b/extension/src/entries/contentScript/nostr-shim.js
index d0ca973..eddc678 100644
--- a/extension/src/entries/contentScript/nostr-shim.js
+++ b/extension/src/entries/contentScript/nostr-shim.js
@@ -13,42 +13,37 @@
// 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 { runtime } from "webextension-polyfill"
+import { isEqual, isNil, isEmpty } from 'lodash'
import { apiCall } from '@vnuge/vnlib.browser'
-import { useManagment } from './../../bg-api/content-script'
+import { useScriptTag, watchOnce } from "@vueuse/core"
+import { createPort } from '../../webext-bridge/'
+import { useStore } from '../store'
+import { storeToRefs } from 'pinia'
-const { getSiteConfig } = useManagment()
-const nip07Enabled = () => getSiteConfig().then(p => p.autoInject);
+const _promptHandler = (() => {
+ let _handler = undefined;
+ return{
+ invoke: (event) => _handler(event),
+ set: (handler) => _handler = handler
+ }
+})()
-//Setup listener for the content script to process nostr messages
+export const usePrompt = (callback) => _promptHandler.set(callback);
-const ext = '@vnuge/nvault-extension'
-let _promptHandler = () => Promise.resolve({})
+export const onLoad = async () =>{
-export const usePrompt = (callback) => {
- //Register the callback
- _promptHandler = async (event) => {
- return new Promise((resolve, reject) => {
- callback(event).then(resolve).catch(reject)
- })
- }
- return {}
-}
+ 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')
-//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);
+ //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 }) => {
@@ -72,9 +67,9 @@ nip07Enabled().then(enabled => {
case 'nip04.encrypt':
case 'nip04.decrypt':
//await propmt for user to allow the request
- const allow = await _promptHandler({ ...data, origin })
+ const allow = await _promptHandler.invoke({ ...data, origin })
//send request to background
- response = allow ? await sendMessage(data.type, { ...data.payload, origin }) : { error: 'User denied permission' }
+ response = allow ? await sendMessage(data.type, { ...data.payload, origin }, 'background') : { error: 'User denied permission' }
break;
default:
throw new Error('Unknown nostr message type')
@@ -84,4 +79,18 @@ nip07Enabled().then(enabled => {
window.postMessage({ ext, id: data.id, response }, origin);
});
}
-})
+
+ 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();
+ }
+
+} \ 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 3747c57..195c6db 100644
--- a/extension/src/entries/contentScript/primary/components/PromptPopup.vue
+++ b/extension/src/entries/contentScript/primary/components/PromptPopup.vue
@@ -10,8 +10,8 @@
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 :class="[keyName ? '' : 'text-red-500']">
+ {{ keyName ?? 'Select Identity' }}
</div>
</div>
<div class="mt-5 text-center">
@@ -65,14 +65,17 @@
<script setup lang="ts">
import { ref } from 'vue'
-import { usePrompt } from '~/entries/contentScript/nostr-shim'
+import { usePrompt } from '../../nostr-shim.js'
import { computed } from '@vue/reactivity';
-import { onClickOutside } from '@vueuse/core';
-import { useStatus } from '~/bg-api/content-script.ts';
+import { } from '@vueuse/core';
import { Popover, PopoverButton, PopoverPanel } from '@headlessui/vue'
import { first } from 'lodash';
+import { useStore } from '../../../store';
+import { storeToRefs } from 'pinia';
-const { loggedIn, selectedKey } = useStatus()
+const store = useStore()
+const { loggedIn, selectedKey } = storeToRefs(store)
+const keyName = computed(() => selectedKey.value?.UserName)
const prompt = ref(null)
@@ -110,7 +113,7 @@ const allow = () => {
//Listen for events
usePrompt(async (ev: PopupEvent) => {
- console.log('usePrompt', ev)
+ console.log('[usePrompt] =>', ev)
switch(ev.type){
case 'getPublicKey':
@@ -130,7 +133,7 @@ usePrompt(async (ev: PopupEvent) => {
break;
}
- return new Promise((resolve, reject) => {
+ return new Promise((resolve) => {
evStack.value.push({
...ev,
allow: () => resolve(true),
diff --git a/extension/src/entries/contentScript/primary/main.js b/extension/src/entries/contentScript/primary/main.js
index 047d0e9..bbf0932 100644
--- a/extension/src/entries/contentScript/primary/main.js
+++ b/extension/src/entries/contentScript/primary/main.js
@@ -15,6 +15,8 @@
import { createApp } from "vue";
+import { createPinia } from 'pinia';
+import { useBackgroundPiniaPlugin, identityPlugin, originPlugin } from '../../store'
import renderContent from "../renderContent";
import App from "./App.vue";
import Notification from '@kyvg/vue3-notification'
@@ -23,12 +25,27 @@ 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 { defer } from "lodash";
renderContent([], (appRoot, shadowRoot) => {
+
+ //Create the background feature wiring
+ const bgPlugins = useBackgroundPiniaPlugin('content-script')
+ //Init store and add plugins
+ const store = createPinia()
+ .use(bgPlugins)
+ .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()
diff --git a/extension/src/entries/options/App.vue b/extension/src/entries/options/App.vue
index f55b73b..dd71209 100644
--- a/extension/src/entries/options/App.vue
+++ b/extension/src/entries/options/App.vue
@@ -53,7 +53,7 @@
</TabList>
<TabPanels>
<TabPanel class="mt-4">
- <Identities :all-keys="allKeys" @edit-key="editKey" @update-all="reloadKeys"/>
+ <Identities :all-keys="allKeys" @edit-key="editKey"/>
</TabPanel>
<TabPanel>
<Privacy/>
@@ -107,23 +107,24 @@ import {
TabPanels,
TabPanel,
} from '@headlessui/vue'
-import { configureNotifier } from '@vnuge/vnlib.browser';
-import { useManagment, useStatus, NostrPubKey } from '~/bg-api/options.ts';
+import { apiCall, configureNotifier } from '@vnuge/vnlib.browser';
+import { storeToRefs } from "pinia";
+import { type NostrPubKey } from '../../features/';
import { notify } from "@kyvg/vue3-notification";
-import { watchDebounced } from '@vueuse/core';
import SiteSettings from './components/SiteSettings.vue';
import Identities from './components/Identities.vue';
import Privacy from "./components/Privacy.vue";
+import { useStore } from "../store";
+
//Configure the notifier to use the notification library
configureNotifier({ notify, close: notify.close })
-const { userName, darkMode } = useStatus()
-const { getAllKeys, updateIdentity, getSiteConfig, saveSiteConfig } = useManagment()
+const store = useStore()
+const { allKeys, darkMode, userName } = storeToRefs(store)
const selectedTab = ref(0)
-const allKeys = ref([])
-const keyBuffer = ref(null)
+const keyBuffer = ref<NostrPubKey>({} as NostrPubKey)
const editKey = (key: NostrPubKey) =>{
//Goto hidden tab
@@ -140,31 +141,24 @@ const doneEditing = () =>{
}
const onUpdate = async () =>{
- //Update identity
- await updateIdentity(keyBuffer.value)
+
+ await apiCall(async ({ toaster }) => {
+ //Update identity
+ await store.updateIdentity(keyBuffer.value)
+ //Show success
+ toaster.general.success({
+ 'title':'Success',
+ 'text': `Successfully updated ${keyBuffer.value!.UserName}`
+ })
+ })
+
//Goto hidden tab
selectedTab.value = 0
//Set selected key
keyBuffer.value = null
}
-const reloadKeys = async () =>{
- //Load all keys (identities)
- const keys = await getAllKeys()
- allKeys.value = keys;
-}
-
-const toggleDark = async () => {
- const config = await getSiteConfig();
- config.darkMode = !config.darkMode;
- await saveSiteConfig(config);
-}
-
-//Initial load
-reloadKeys();
-
-//If the tab changes to the identities tab, reload the keys
-watchDebounced(selectedTab, id => id == 0 ? reloadKeys() : null, { debounce: 100 })
+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/options/components/Identities.vue b/extension/src/entries/options/components/Identities.vue
index d7fd75d..e3af74e 100644
--- a/extension/src/entries/options/components/Identities.vue
+++ b/extension/src/entries/options/components/Identities.vue
@@ -39,7 +39,7 @@
</Popover>
</div>
</div>
- <div v-for="key in allKeys" :key="key" class="mt-2 mb-3">
+ <div v-for="key in allKeys" :key="key.Id" class="mt-2 mb-3">
<div class="" :class="{'selected': isSelected(key)}" @click.self="selectKey(key)">
<div class="mb-8">
@@ -83,7 +83,7 @@
<script setup lang="ts">
import { isEqual, map } from 'lodash'
-import { ref, toRefs } from "vue";
+import { ref } from "vue";
import {
Popover,
PopoverButton,
@@ -91,30 +91,26 @@ import {
PopoverOverlay
} from '@headlessui/vue'
import { apiCall, configureNotifier } from '@vnuge/vnlib.browser';
-import { useManagment, useStatus } from '~/bg-api/options.ts';
+import { NostrPubKey } from '../../../features';
import { notify } from "@kyvg/vue3-notification";
-import { useClipboard } from '@vueuse/core';
-import { NostrIdentiy } from '~/bg-api/bg-api';
-import { NostrPubKey } from '../../background/types';
+import { get, useClipboard } from '@vueuse/core';
+import { useStore } from '../../store';
+import { storeToRefs } from 'pinia';
-const emit = defineEmits(['edit-key', 'update-all'])
-const props = defineProps<{
- allKeys:NostrIdentiy[]
-}>()
-
-const { allKeys } = toRefs(props)
+const emit = defineEmits(['edit-key'])
//Configre the notifier to use the toaster
configureNotifier({ notify, close: notify.close })
const downloadAnchor = ref<HTMLAnchorElement>()
-const { selectedKey } = useStatus()
-const { selectKey, createIdentity, deleteIdentity, getAllKeys } = useManagment()
+const store = useStore()
+const { selectedKey, allKeys } = storeToRefs(store)
const { copy } = useClipboard()
-const isSelected = (me : NostrIdentiy) => isEqual(me, selectedKey.value)
-const editKey = (key : NostrIdentiy) => emit('edit-key', key);
+const isSelected = (me : NostrPubKey) => isEqual(me, selectedKey.value)
+const editKey = (key : NostrPubKey) => emit('edit-key', key);
+const selectKey = (key: NostrPubKey) => store.selectKey(key)
const onCreate = async (e: Event, onClose : () => void) => {
@@ -123,35 +119,39 @@ const onCreate = async (e: Event, onClose : () => void) => {
//try to get existing key field
const ExistingKey = e.target['key']?.value as string
- //Create new identity
- await createIdentity({ UserName, ExistingKey })
- //Update keys
- emit('update-all');
+ await apiCall(async () => {
+ //Create new identity
+ await store.createIdentity({ UserName, ExistingKey })
+ })
+
onClose()
}
-const prettyPrintDate = (key : NostrIdentiy) => {
+const prettyPrintDate = (key : NostrPubKey) => {
const d = new Date(key.LastModified)
return `${d.toLocaleDateString()} ${d.toLocaleTimeString()}`
}
-const onDeleteKey = async (key : NostrIdentiy) => {
+const onDeleteKey = async (key : NostrPubKey) => {
if(!confirm(`Are you sure you want to delete ${key.UserName}?`)){
return;
}
- //Delete identity
- await deleteIdentity(key)
-
- //Update keys
- emit('update-all');
+ apiCall(async ({ toaster }) => {
+ //Delete identity
+ await store.deleteIdentity(key)
+ toaster.general.success({
+ 'title': 'Success',
+ 'text': `${key.UserName} has been deleted`
+ })
+ })
}
const onNip05Download = () => {
apiCall(async () => {
//Get all public keys from the server
- const keys = await getAllKeys() as NostrPubKey[]
+ const keys = get(allKeys)
const nip05 = {}
//Map the keys to the NIP-05 format
map(keys, k => nip05[k.UserName] = k.PublicKey)
diff --git a/extension/src/entries/options/components/Privacy.vue b/extension/src/entries/options/components/Privacy.vue
index 7d2ce4d..d54f679 100644
--- a/extension/src/entries/options/components/Privacy.vue
+++ b/extension/src/entries/options/components/Privacy.vue
@@ -1,9 +1,68 @@
<template>
<div class="flex flex-col w-full mt-4 sm:px-2">
-
+ <div class="flex flex-row gap-1">
+ <div class="text-2xl">
+ Tracking protection
+ </div>
+ <div class="mt-auto" :class="[isOriginProtectionOn ? 'text-primary-600' : 'text-red-500']">
+ {{ isOriginProtectionOn ? 'active' : 'inactive' }}
+ </div>
+ </div>
+ <div class="">
+ <div class="p-2">
+ <div class="my-1">
+ <form @submit.prevent="allowOrigin()">
+ <input class="w-full max-w-xs input primary" type="text" v-model="newOrigin" placeholder="Add new origin"/>
+ <button type="submit" class="ml-1 btn xs" >
+ <fa-icon icon="plus" />
+ </button>
+ </form>
+ </div>
+ <label class="font-semibold">Whitelist:</label>
+ <ul class="pl-1 list-disc list-inside">
+ <li v-for="origin in allowedOrigins" :key="origin" class="my-1 text-sm">
+ <span class="">
+ {{ origin }}
+ </span>
+ <span>
+ <button class="ml-1 text-xs text-red-500" @click="store.dissallowOrigin(origin)">
+ remove
+ </button>
+ </span>
+ </li>
+ </ul>
+ </div>
+ </div>
</div>
</template>
<script setup lang="ts">
+import { storeToRefs } from 'pinia';
+import { useStore } from '../../store';
+import { useFormToaster } from '@vnuge/vnlib.browser';
+import { ref } from 'vue';
+
+const store = useStore()
+const { isOriginProtectionOn, allowedOrigins } = storeToRefs(store)
+const newOrigin = ref('')
+const { error, info } = useFormToaster()
+
+const allowOrigin = async () =>{
+ try {
+ await store.allowOrigin(newOrigin.value)
+ }
+ catch (err: any) {
+ error({
+ title: 'Failed to allow origin',
+ text: err.message
+ })
+ return;
+ }
+ info({
+ title: 'Origin allowed',
+ text: `Origin ${newOrigin.value} has been allowed`
+ })
+ newOrigin.value = ''
+}
</script> \ No newline at end of file
diff --git a/extension/src/entries/options/components/SiteSettings.vue b/extension/src/entries/options/components/SiteSettings.vue
index 2b32a93..c0c2e93 100644
--- a/extension/src/entries/options/components/SiteSettings.vue
+++ b/extension/src/entries/options/components/SiteSettings.vue
@@ -7,23 +7,23 @@
Extension settings
</h3>
<div class="my-6">
- <fieldset :disabled="waiting">
+ <fieldset :disabled="waiting.value">
<div class="">
<div class="w-full">
<div class="flex flex-row w-full">
<Switch
- v-model="buffer.autoInject"
- :class="buffer.autoInject ? 'bg-black dark:bg-gray-50' : 'bg-gray-200 dark:bg-dark-600'"
+ v-model="originProtection"
+ :class="originProtection ? 'bg-black dark:bg-gray-50' : 'bg-gray-200 dark:bg-dark-600'"
class="relative inline-flex items-center h-5 rounded-full w-11"
>
- <span class="sr-only">NIP-07</span>
+ <span class="sr-only">Origin protection</span>
<span
- :class="buffer.autoInject ? 'translate-x-6' : 'translate-x-1'"
+ :class="originProtection ? 'translate-x-6' : 'translate-x-1'"
class="inline-block w-4 h-4 transition transform bg-white rounded-full dark:bg-dark-900"
/>
</Switch>
<div class="my-auto ml-2 text-sm dark:text-gray-200">
- Always on NIP-07
+ Tracking protection
</div>
</div>
</div>
@@ -69,27 +69,42 @@
</a>
</div>
</div>
- <fieldset :disabled="waiting || !editMode">
+ <fieldset>
<div class="pl-1 mt-2">
</div>
<div class="mt-2">
<label class="pl-1">BaseUrl</label>
- <input class="w-full input" v-model="v$.apiUrl.$model" :class="{'error': v$.apiUrl.$invalid }" />
+ <input
+ class="w-full input"
+ :class="{'error': v$.apiUrl.$invalid }"
+ v-model="v$.apiUrl.$model"
+ :readonly="!editMode"
+ />
<p class="pl-1 mt-1 text-xs text-gray-600 dark:text-gray-400">
* The http path to the vault server (must start with http:// or https://)
</p>
</div>
<div class="mt-2">
<label class="pl-1">Account endpoint</label>
- <input class="w-full input" v-model="v$.accountBasePath.$model" :class="{ 'error': v$.accountBasePath.$invalid }" />
+ <input
+ class="w-full input"
+ v-model="v$.accountBasePath.$model"
+ :class="{ 'error': v$.accountBasePath.$invalid }"
+ :readonly="!editMode"
+ />
<p class="pl-1 mt-1 text-xs text-gray-600 dark:text-gray-400">
* This is the path to the account server endpoint (must start with /)
</p>
</div>
<div class="mt-2">
<label class="pl-1">Nostr endpoint</label>
- <input class="w-full input" v-model="v$.nostrEndpoint.$model" :class="{ 'error': v$.nostrEndpoint.$invalid }" />
+ <input
+ class="w-full input"
+ v-model="v$.nostrEndpoint.$model"
+ :class="{ 'error': v$.nostrEndpoint.$invalid }"
+ :readonly="!editMode"
+ />
<p class="pl-1 mt-1 text-xs text-gray-600 dark:text-gray-400">
* This is the path to the Nostr plugin endpoint path (must start with /)
</p>
@@ -104,25 +119,31 @@
</template>
<script setup lang="ts">
-import { apiCall, useDataBuffer, useFormToaster, useVuelidateWrapper, useWait } from '@vnuge/vnlib.browser';
-import { computed, ref, watch } from 'vue';
-import { useManagment } from '~/bg-api/options.ts';
+import { apiCall, useDataBuffer, useVuelidateWrapper, useWait } from '@vnuge/vnlib.browser';
+import { computed, watch } from 'vue';
import { useToggle, watchDebounced } from '@vueuse/core';
import { maxLength, helpers, required } from '@vuelidate/validators'
-import { clone, isNil } from 'lodash';
-import{ Switch } from '@headlessui/vue'
+import { Switch } from '@headlessui/vue'
+import { useStore } from '../../store';
+import { storeToRefs } from 'pinia';
import useVuelidate from '@vuelidate/core'
const { waiting } = useWait();
-const { info } = useFormToaster();
-const { getSiteConfig, saveSiteConfig } = useManagment();
-
-const { apply, data, buffer, modified } = useDataBuffer({
- apiUrl: '',
- accountBasePath: '',
- nostrEndpoint:'',
- heartbeat:false,
- autoInject:true,
+const store = useStore()
+const { settings, isOriginProtectionOn } = storeToRefs(store)
+
+const { apply, data, buffer, modified, update } = useDataBuffer(settings.value, async sb =>{
+ const newConfig = await store.saveSiteConfig(sb.buffer)
+ apply(newConfig)
+ return newConfig;
+})
+
+//Watch for store settings changes and apply them
+watch(settings, v => apply(v.value))
+
+const originProtection = computed({
+ get: () => isOriginProtectionOn.value,
+ set: v => store.setOriginProtection(v)
})
const url = (val : string) => /^https?:\/\/[a-zA-Z0-9\.\:\/-]+$/.test(val);
@@ -145,15 +166,13 @@ const vRules = {
alphaNum: helpers.withMessage('Nostr path is not a valid endpoint path that begins with /', path)
},
heartbeat: {},
- darkMode:{}
}
//Configure validator and validate function
const v$ = useVuelidate(vRules, buffer)
-const { validate } = useVuelidateWrapper(v$);
+const { validate } = useVuelidateWrapper(v$ as any);
-const editMode = ref(false);
-const toggleEdit = useToggle(editMode);
+const [ editMode, toggleEdit ] = useToggle(false);
const autoInject = computed(() => buffer.autoInject)
const heartbeat = computed(() => buffer.heartbeat)
@@ -171,24 +190,12 @@ const onSave = async () => {
return;
}
- info({
- title: 'Reloading in 4 seconds',
- text: 'Your configuration will be saved and the extension will reload in 4 seconds'
- })
-
- await new Promise(r => setTimeout(r, 4000));
-
- publishConfig();
+ await update();
//disable dit
toggleEdit();
}
-const publishConfig = async () =>{
- const c = clone(buffer);
- await saveSiteConfig(c);
- await loadConfig();
-}
const testConnection = async () =>{
return await apiCall(async ({axios, toaster}) =>{
@@ -201,38 +208,17 @@ const testConnection = async () =>{
return true;
}
catch(e){
- if(isNil(e.response?.status)){
- toaster.form.error({
- title: 'Network error',
- text: `Please verify your vault server address`
- });
- }
-
toaster.form.error({
title: 'Warning',
- text: `Failed to connect to the vault server. Status code: ${e.response.status}`
+ text: `Failed to connect to the vault server. Status code: ${(e as any).response?.status}`
});
}
})
}
-const loadConfig = async () => {
- const config = await getSiteConfig();
- apply(config);
-}
-
-const init = async () => {
- await loadConfig();
-
- //Watch for changes to autoinject value and publish changes when it does
- watchDebounced(autoInject, publishConfig, { debounce: 500, immediate: false })
- watchDebounced(heartbeat, publishConfig, { debounce: 500, immediate: false })
-}
-
-//If edit mode is toggled off, reload config
-watch(editMode, v => v ? null : loadConfig());
-
-init();
+//Watch for changes to autoinject value and publish changes when it does
+watchDebounced(autoInject, update, { debounce: 500, immediate: false })
+watchDebounced(heartbeat, update, { debounce: 500, immediate: false })
</script>
diff --git a/extension/src/entries/options/main.js b/extension/src/entries/options/main.js
index b9892ba..827c426 100644
--- a/extension/src/entries/options/main.js
+++ b/extension/src/entries/options/main.js
@@ -22,12 +22,23 @@ import Notifications from "@kyvg/vue3-notification";
/* FONT AWESOME CONFIG */
import { library } from '@fortawesome/fontawesome-svg-core'
-import { faChevronLeft, faChevronRight, faCopy, faDownload, faEdit, faExternalLinkAlt, faLock, faLockOpen, faMoon, faSun, faTrash } from '@fortawesome/free-solid-svg-icons'
+import { faChevronLeft, faChevronRight, faCopy, faDownload, faEdit, faExternalLinkAlt, faLock, faLockOpen, faMoon, faPlus, faSun, faTrash } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
+import { createPinia } from "pinia";
+import { identityPlugin, originPlugin, useBackgroundPiniaPlugin } from "../store";
-library.add(faCopy, faEdit, faChevronLeft, faMoon, faSun, faLock, faLockOpen, faExternalLinkAlt, faTrash, faDownload, faChevronRight)
+library.add(faCopy, faEdit, faChevronLeft, faMoon, faSun, faLock, faLockOpen, faExternalLinkAlt, faTrash, faDownload, faChevronRight, faPlus)
+
+//Create the background feature wiring
+const bgPlugins = useBackgroundPiniaPlugin('options')
+
+const pinia = createPinia()
+ .use(bgPlugins) //Add the background pinia plugin
+ .use(identityPlugin) //Add the identity plugin
+ .use(originPlugin) //Add the origin plugin
createApp(App)
.use(Notifications)
+ .use(pinia)
.component('fa-icon', FontAwesomeIcon)
- .mount("#app"); \ No newline at end of file
+ .mount("#app");
diff --git a/extension/src/entries/popup/App.vue b/extension/src/entries/popup/App.vue
index 0181bbb..8c49fa6 100644
--- a/extension/src/entries/popup/App.vue
+++ b/extension/src/entries/popup/App.vue
@@ -17,6 +17,6 @@ main {
text-align: center;
color: #2c3e50;
- @apply dark:bg-dark-800 bg-white dark:text-gray-200;
+ @apply dark:bg-dark-900 bg-white dark:text-gray-200;
}
</style>
diff --git a/extension/src/entries/popup/Components/IdentitySelection.vue b/extension/src/entries/popup/Components/IdentitySelection.vue
index d1a7333..99d8e34 100644
--- a/extension/src/entries/popup/Components/IdentitySelection.vue
+++ b/extension/src/entries/popup/Components/IdentitySelection.vue
@@ -1,5 +1,5 @@
<template>
- <div class="px-3 text-left">
+ <div class="text-left">
<div class="w-full">
<div class="">
<select class="w-full input"
@@ -19,28 +19,22 @@
<script setup lang="ts">
import { find } from 'lodash'
import { computed } from "vue";
-import { useStatus, useManagment, NostrPubKey } from "~/bg-api/popup.ts";
+import { useStore } from "../../store";
import { useWait } from '@vnuge/vnlib.browser'
-import { computedAsync } from '@vueuse/core';
+import { storeToRefs } from 'pinia';
-const { selectedKey } = useStatus();
const { waiting } = useWait();
-const { getAllKeys, selectKey } = useManagment();
-
-const allKeys = computedAsync<NostrPubKey[]>(async () => await getAllKeys(), []);
+const store = useStore();
+const { selectedKey, allKeys } = storeToRefs(store);
const onSelected = async ({target}) =>{
//Select the key of the given id
const selected = find(allKeys.value, {Id: target.value})
if(selected){
- await selectKey(selected)
+ await store.selectKey(selected)
}
}
-const selected = computed(() => selectedKey?.value || { Id:"0" })
+const selected = computed(() => selectedKey?.value || { Id:"" })
</script>
-
-<style lang="scss">
-
-</style> \ No newline at end of file
diff --git a/extension/src/entries/popup/Components/Login.vue b/extension/src/entries/popup/Components/Login.vue
index c863430..44df714 100644
--- a/extension/src/entries/popup/Components/Login.vue
+++ b/extension/src/entries/popup/Components/Login.vue
@@ -19,18 +19,24 @@
</template>
<script setup lang="ts">
-import { useWait } from "@vnuge/vnlib.browser";
+import { apiCall, useWait } from "@vnuge/vnlib.browser";
import { ref } from "vue";
-import { useManagment } from "~/bg-api/popup.ts";
+import { useStore } from "../../store";
-const { login } = useManagment()
+const { login } = useStore()
const { waiting } = useWait()
const token = ref('')
const onSubmit = async () => {
- //console.log(token.value)
- await login(token.value)
+ await apiCall(async ({ toaster }) => {
+ await login(token.value)
+ toaster.general.success({
+ 'title': 'Login successful',
+ 'text': 'Successfully logged into your profile'
+ })
+ })
+
}
</script>
diff --git a/extension/src/entries/popup/Components/PageContent.vue b/extension/src/entries/popup/Components/PageContent.vue
index eda9bab..1a3995e 100644
--- a/extension/src/entries/popup/Components/PageContent.vue
+++ b/extension/src/entries/popup/Components/PageContent.vue
@@ -4,35 +4,49 @@
class="flex flex-col text-left w-[20rem] min-h-[25rem]"
>
- <div class="flex flex-row w-full px-1 pl-4">
- <div class="flex-auto my-auto">
- <h3>NVault</h3>
+ <div class="flex flex-row w-full gap-2 p-1.5 bg-black text-white dark:bg-dark-600 shadow">
+ <div class="flex flex-row flex-auto my-auto">
+ <div class="my-auto mr-2">
+ <img class="h-6" src="/icons/32.png" />
+ </div>
+ <h3 class="block my-auto">NVault</h3>
+ <div class="px-3 py-.5 m-auto text-sm rounded-full h-fit active-badge" :class="[isTabAllowed ? 'active' : 'inactive']">
+ {{ isTabAllowed ? 'Active' : 'Inactive' }}
+ </div>
</div>
<div class="my-auto" v-if="loggedIn">
- <button class="rounded btn sm red" @click.prevent="logout">
+ <button class="rounded btn xs" @click.prevent="logout">
<fa-icon icon="arrow-right-from-bracket" />
</button>
</div>
- <div class="p-2 my-auto">
- <button class="rounded btn sm" @click="openOptions">
+ <div class="my-auto">
+ <button class="rounded btn xs" @click="openOptions">
<fa-icon :icon="['fas', 'gear']"/>
</button>
</div>
</div>
+
<div v-if="!loggedIn">
<Login></Login>
</div>
- <div v-else class="flex justify-center pb-4">
- <div class="w-full m-auto">
- <div class="mt-2 text-center">
- {{ userName }}
- <div class="mt-4">
+
+ <div v-else class="flex justify-center">
+ <div class="w-full px-3 m-auto">
+
+ <div class="text-sm text-center">
+ {{ userName }}
+ </div>
+
+ <div class="">
+ <label class="mb-0.5 text-sm dark:text-dark-100">
+ Identity
+ </label>
<IdentitySelection></IdentitySelection>
</div>
- <div class="mt-2.5 min-h-[6rem]">
- <div class="flex flex-col justify-center">
-
- <div class="flex flex-row gap-2 p-2 mx-3 my-3 bg-gray-100 border border-gray-200 rounded dark:bg-dark-700 dark:border-dark-400">
+
+ <div class="w-full mt-1">
+ <div class="flex flex-col">
+ <div class="flex flex-row gap-2 p-1.5 bg-gray-100 border border-gray-200 dark:bg-dark-800 dark:border-dark-400">
<div class="text-sm break-all">
{{ pubKey ?? 'No key selected' }}
</div>
@@ -40,13 +54,30 @@
<fa-icon class="mr-1" icon="copy" @click="copy(pubKey)"/>
</div>
</div>
-
</div>
</div>
- <div class="mt-3 text-sm">
- Always on NIP-07: <span class="font-semibold" :class="{'text-blue-500':autoInject}">{{ autoInject }}</span>
+
+ <div class="mt-4">
+ <label class="block mb-1 text-xs text-left dark:text-dark-100" >
+ Current origin
+ </label>
+
+ <div v-if="isOriginProtectionOn" class="flex flex-row w-full gap-2">
+ <input :value="currentOrigin" class="flex-1 p-1 mx-0 text-sm input" readonly/>
+
+ <button v-if="isTabAllowed" class="btn xs" @click="store.dissallowOrigin()">
+ <fa-icon icon="minus" />
+ </button>
+ <button v-else class="btn xs" @click="store.allowOrigin()">
+ <fa-icon icon="plus" />
+ </button>
+ </div>
+
+ <div v-else class="text-xs text-center">
+ <span class="">Tracking protection disabled</span>
+ </div>
</div>
- </div>
+
</div>
</div>
@@ -57,9 +88,10 @@
<script setup lang="ts">
import { computed, watchEffect } from "vue";
-import { useStatus, useManagment } from "~/bg-api/popup.ts";
-import { configureNotifier } from "@vnuge/vnlib.browser";
-import { asyncComputed, useClipboard, watchDebounced } from '@vueuse/core'
+import { storeToRefs } from "pinia";
+import { useStore } from "../../store";
+import { apiCall, configureNotifier } from "@vnuge/vnlib.browser";
+import { useClipboard } from '@vueuse/core'
import { notify } from "@kyvg/vue3-notification";
import { runtime } from "webextension-polyfill";
import Login from "./Login.vue";
@@ -67,38 +99,25 @@ import IdentitySelection from "./IdentitySelection.vue";
configureNotifier({notify, close:notify.close})
-const { loggedIn, userName, selectedKey, darkMode } = useStatus()
-const { logout, getProfile, getSiteConfig } = useManagment()
-
+const store = useStore()
+const { loggedIn, selectedKey, userName, darkMode, isTabAllowed, currentOrigin, isOriginProtectionOn } = storeToRefs(store)
const { copy, copied } = useClipboard()
-const pubKey = computed(() => selectedKey.value?.PublicKey)
-const qrCode = computed(() => pubKey.value ? `nostr:npub1${pubKey.value}` : null)
-
-watchDebounced(loggedIn, async () => {
- //Manually update the user's profile if they are logged in and the profile is not yet loaded
- if(loggedIn.value && !userName.value){
- getProfile()
- }
-},{ debounce:100, immediate: true })
+const pubKey = computed(() => selectedKey!.value?.PublicKey)
const openOptions = () => runtime.openOptionsPage();
//Watch for dark mode changes and update the body class
watchEffect(() => darkMode.value ? document.body.classList.add('dark') : document.body.classList.remove('dark'));
-const autoInject = asyncComputed(() => getSiteConfig().then<Boolean>(p => p.autoInject), false)
-
-</script>
-
-<style lang="scss">
-
-.toaster{
- position: fixed;
- top: 15px;
- right: 0;
- z-index: 9999;
- max-width: 230px;
+const logout = () =>{
+ apiCall(async ({ toaster }) =>{
+ await store.logout()
+ toaster.general.success({
+ 'title':'Success',
+ 'text': 'You have been logged out'
+ })
+ })
}
-</style>
+</script>
diff --git a/extension/src/entries/popup/local.scss b/extension/src/entries/popup/local.scss
new file mode 100644
index 0000000..9c64b98
--- /dev/null
+++ b/extension/src/entries/popup/local.scss
@@ -0,0 +1,18 @@
+
+.toaster {
+ position: fixed;
+ top: 15px;
+ right: 0;
+ z-index: 9999;
+ max-width: 230px;
+}
+
+.active-badge{
+ @apply text-white;
+ &.active{
+ @apply bg-primary-500 dark:bg-primary-600;
+ }
+ &.inactive{
+
+ }
+} \ No newline at end of file
diff --git a/extension/src/entries/popup/main.js b/extension/src/entries/popup/main.js
index 4a32bb7..a259e63 100644
--- a/extension/src/entries/popup/main.js
+++ b/extension/src/entries/popup/main.js
@@ -14,19 +14,30 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import { createApp } from "vue";
+import { createPinia } from "pinia";
+import { identityPlugin, originPlugin, useBackgroundPiniaPlugin } from '../store'
import App from "./App.vue";
import Notifications from "@kyvg/vue3-notification";
import '@fontsource/noto-sans-masaram-gondi'
import "~/assets/all.scss";
+import "./local.scss"
/* FONT AWESOME CONFIG */
import { library } from '@fortawesome/fontawesome-svg-core'
-import { faArrowRightFromBracket, faCopy, faEdit, faGear, faSpinner } from '@fortawesome/free-solid-svg-icons'
+import { faArrowRightFromBracket, faCopy, faEdit, faGear, faMinus, faPlus, faSpinner } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
-library.add(faSpinner, faEdit, faGear, faCopy, faArrowRightFromBracket)
+library.add(faSpinner, faEdit, faGear, faCopy, faArrowRightFromBracket, faPlus, faMinus)
+
+const bgPlugin = useBackgroundPiniaPlugin('popup')
+
+const pinia = createPinia()
+ .use(bgPlugin) //Add the background pinia plugin
+ .use(identityPlugin)
+ .use(originPlugin)
createApp(App)
.use(Notifications)
+ .use(pinia)
.component('fa-icon', FontAwesomeIcon)
.mount("#app");
diff --git a/extension/src/entries/store/allowedOrigins.ts b/extension/src/entries/store/allowedOrigins.ts
new file mode 100644
index 0000000..7fc5e15
--- /dev/null
+++ b/extension/src/entries/store/allowedOrigins.ts
@@ -0,0 +1,43 @@
+
+import 'pinia'
+import { } from 'lodash'
+import { PiniaPluginContext } from 'pinia'
+import { computed, ref } from 'vue';
+import { onWatchableChange } from '../../features/types';
+import { type AllowedOriginStatus } from '../../features/nip07allow-api';
+
+declare module 'pinia' {
+ export interface PiniaCustomProperties {
+ isTabAllowed: boolean;
+ currentOrigin: string | undefined;
+ allowedOrigins: Array<string>;
+ isOriginProtectionOn: boolean;
+ allowOrigin(origin?:string): Promise<void>;
+ dissallowOrigin(origin?:string): Promise<void>;
+ disableOriginProtection(): Promise<void>;
+ setOriginProtection(value: boolean): Promise<void>;
+ }
+}
+
+export const originPlugin = ({ store }: PiniaPluginContext) => {
+
+ const { plugins } = store
+ const status = ref<AllowedOriginStatus>()
+
+ onWatchableChange(plugins.allowedOrigins, async () => {
+ //Update the status
+ status.value = await plugins.allowedOrigins.getStatus()
+ }, { immediate: true })
+
+ return {
+ allowedOrigins: computed(() => status.value?.allowedOrigins || []),
+ isTabAllowed: computed(() => status.value?.isAllowed == true),
+ currentOrigin: computed(() => status.value?.currentOrigin),
+ isOriginProtectionOn: computed(() => status.value?.enabled == true),
+ //Push to the allow list, will trigger a change if needed
+ allowOrigin: plugins.allowedOrigins.addOrigin,
+ //Remove from allow list, will trigger a change if needed
+ dissallowOrigin: plugins.allowedOrigins.removeOrigin,
+ setOriginProtection: plugins.allowedOrigins.enable
+ }
+}
diff --git a/extension/src/entries/store/features.ts b/extension/src/entries/store/features.ts
new file mode 100644
index 0000000..9bf3052
--- /dev/null
+++ b/extension/src/entries/store/features.ts
@@ -0,0 +1,116 @@
+
+import 'pinia'
+import { } from 'lodash'
+import { PiniaPluginContext } from 'pinia'
+import { type Tabs, tabs } from 'webextension-polyfill'
+
+import {
+ SendMessageHandler,
+ useAuthApi,
+ useHistoryApi,
+ useIdentityApi,
+ useNostrApi,
+ useLocalPki,
+ usePkiApi,
+ useSettingsApi,
+ useForegoundFeatures,
+ useEventTagFilterApi,
+ useInjectAllowList
+} from "../../features"
+
+import { RuntimeContext, createPort } from '../../webext-bridge'
+import { ref } from 'vue'
+import { onWatchableChange } from '../../features/types'
+
+export type BgPlugins = ReturnType<typeof usePlugins>
+export type BgPluginState<T> = { plugins: BgPlugins } & T
+
+declare module 'pinia' {
+ export interface PiniaCustomProperties {
+ plugins: BgPlugins
+ currentTab: Tabs.Tab | undefined
+ }
+}
+
+const usePlugins = (sendMessage: SendMessageHandler) => {
+ //Create plugin wrapping function
+ const { use } = useForegoundFeatures(sendMessage)
+
+ return {
+ settings: use(useSettingsApi),
+ user: use(useAuthApi),
+ identity: use(useIdentityApi),
+ nostr: use(useNostrApi),
+ history: use(useHistoryApi),
+ localPki: use(useLocalPki),
+ pki: use(usePkiApi),
+ tagFilter: use(useEventTagFilterApi),
+ allowedOrigins: use(useInjectAllowList)
+ }
+}
+
+export const useBackgroundPiniaPlugin = (context: RuntimeContext) => {
+ //Create port for context
+ const { sendMessage } = createPort(context)
+ const plugins = usePlugins(sendMessage)
+ const { user } = plugins;
+
+ const currentTab = ref<Tabs.Tab | undefined>(undefined)
+
+ //Plugin store
+ return ({ store }: PiniaPluginContext) => {
+
+ //watch for status changes
+ onWatchableChange(user, async () => {
+ //Get status update and set the values
+ const { loggedIn, userName } = await user.getStatus();
+ store.loggedIn = loggedIn;
+ store.userName = userName;
+
+ }, { immediate: true })
+
+ //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
new file mode 100644
index 0000000..58a6b67
--- /dev/null
+++ b/extension/src/entries/store/identity.ts
@@ -0,0 +1,43 @@
+
+import 'pinia'
+import { } from 'lodash'
+import { PiniaPluginContext } from 'pinia'
+import { NostrPubKey } from '../../features'
+import { ref } from 'vue';
+import { onWatchableChange } from '../../features/types';
+
+declare module 'pinia' {
+ export interface PiniaCustomStateProperties {
+ allKeys: NostrPubKey[];
+ selectedKey: NostrPubKey | undefined;
+ deleteIdentity(key: Partial<NostrPubKey>): Promise<void>;
+ createIdentity(id: Partial<NostrPubKey>): Promise<NostrPubKey>;
+ updateIdentity(id: NostrPubKey): Promise<NostrPubKey>;
+ selectKey(key: NostrPubKey): Promise<void>;
+ }
+}
+
+
+export const identityPlugin = ({ store }: PiniaPluginContext) => {
+
+ const { identity } = store.plugins
+
+ const allKeys = ref<NostrPubKey[]>([])
+ const selectedKey = ref<NostrPubKey | undefined>(undefined)
+
+ onWatchableChange(identity, async () => {
+ 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 {
+ selectedKey,
+ allKeys,
+ selectKey: identity.selectKey,
+ deleteIdentity: identity.deleteIdentity,
+ createIdentity: identity.createIdentity,
+ updateIdentity: identity.updateIdentity
+ }
+} \ No newline at end of file
diff --git a/extension/src/entries/store/index.ts b/extension/src/entries/store/index.ts
new file mode 100644
index 0000000..07fce6d
--- /dev/null
+++ b/extension/src/entries/store/index.ts
@@ -0,0 +1,60 @@
+// 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 'pinia'
+import { } from 'lodash'
+import { defineStore } from 'pinia'
+import { PluginConfig } from '../../features/'
+import { NostrStoreState } from './types'
+
+export type * from './types'
+export * from './allowedOrigins'
+export * from './features'
+export * from './identity'
+
+export const useStore = defineStore({
+ id: 'main',
+ state: (): NostrStoreState =>({
+ loggedIn: false,
+ userName: '',
+ settings: undefined as any,
+ darkMode: false
+ }),
+ actions: {
+
+ async login (token: string) {
+ await this.plugins.user.login(token);
+ },
+
+ async logout () {
+ await this.plugins.user.logout();
+ },
+
+ saveSiteConfig(config: PluginConfig) {
+ return this.plugins.settings.setSiteConfig(config)
+ },
+
+ async toggleDarkMode(){
+ await this.plugins.settings.setDarkMode(this.darkMode === false)
+ },
+
+ checkIsCurrentOriginAllowed() {
+
+ }
+ },
+ getters:{
+
+ },
+}) \ No newline at end of file
diff --git a/extension/src/entries/store/types.ts b/extension/src/entries/store/types.ts
new file mode 100644
index 0000000..7addda4
--- /dev/null
+++ b/extension/src/entries/store/types.ts
@@ -0,0 +1,9 @@
+import { } from "webextension-polyfill";
+import { PluginConfig } from "../../features";
+
+export interface NostrStoreState {
+ loggedIn: boolean;
+ userName: string | null;
+ settings: PluginConfig;
+ darkMode: boolean;
+} \ No newline at end of file
diff --git a/extension/src/features/account-api.ts b/extension/src/features/account-api.ts
new file mode 100644
index 0000000..9c701c3
--- /dev/null
+++ b/extension/src/features/account-api.ts
@@ -0,0 +1,190 @@
+// 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 { useMfaConfig, usePkiConfig, PkiPublicKey, debugLog } from "@vnuge/vnlib.browser";
+import { ArrayToHexString, Base64ToUint8Array } from "@vnuge/vnlib.browser/dist/binhelpers";
+import { JsonObject } from "type-fest";
+import { useSingleSlotStorage } from "./types";
+import { computed, watch } from "vue";
+import { storage } from "webextension-polyfill";
+import { JWK, SignJWT, importJWK } from "jose";
+import { cloneDeep } from "lodash";
+import { FeatureApi, BgRuntime, IFeatureExport, exportForegroundApi, optionsOnly, popupAndOptionsOnly } from "./framework";
+import { AppSettings } from "./settings";
+
+
+export interface EcKeyParams extends JsonObject {
+ readonly namedCurve: string
+}
+
+export interface PkiPubKey extends JsonObject, PkiPublicKey {
+ readonly kid: string,
+ readonly alg: string,
+ readonly use: string,
+ readonly kty: string,
+ readonly x: string,
+ readonly y: string,
+ readonly serial: string
+ readonly userName: string
+}
+
+export interface PkiApi extends FeatureApi{
+ getAllKeys(): Promise<PkiPubKey[]>
+ removeKey(kid: PkiPubKey): Promise<void>
+ isEnabled(): Promise<boolean>
+}
+
+export const usePkiApi = (): IFeatureExport<AppSettings, PkiApi> => {
+ return{
+ background: ({ state } : BgRuntime<AppSettings>):PkiApi =>{
+ const accountPath = computed(() => state.currentConfig.value.accountBasePath)
+ const mfaEndpoint = computed(() => `${accountPath.value}/mfa`)
+ const pkiEndpoint = computed(() => `${accountPath.value}/pki`)
+
+ //Compute config
+ const mfaConfig = useMfaConfig(mfaEndpoint);
+ const pkiConfig = usePkiConfig(pkiEndpoint, mfaConfig);
+
+ //Refresh the config when the endpoint changes
+ watch(mfaEndpoint, () => pkiConfig.refresh());
+
+ return{
+ getAllKeys: optionsOnly(async () => {
+ const res = await pkiConfig.getAllKeys();
+ return res as PkiPubKey[]
+ }),
+ removeKey: optionsOnly(async (key: PkiPubKey) => {
+ await pkiConfig.removeKey(key.kid)
+ }),
+ isEnabled: popupAndOptionsOnly(async () => {
+ return pkiConfig.enabled.value
+ })
+ }
+ },
+ foreground: exportForegroundApi<PkiApi>([
+ 'getAllKeys',
+ 'removeKey',
+ 'isEnabled'
+ ])
+ }
+}
+
+interface PkiSettings {
+ userName: string,
+ privateKey?:JWK
+}
+
+export interface LocalPkiApi extends FeatureApi {
+ regenerateKey: (userName:string, params: EcKeyParams) => Promise<void>
+ getPubKey: () => Promise<PkiPubKey | undefined>
+ generateOtp: () => Promise<string>
+}
+
+export const useLocalPki = (): IFeatureExport<AppSettings, LocalPkiApi> => {
+
+ return{
+ //Setup registration
+ background: ({ state } : BgRuntime<AppSettings>) =>{
+ const { get, set } = useSingleSlotStorage<PkiSettings>(storage.local, 'pki-settings')
+
+ const getPubKey = async (): Promise<PkiPubKey | undefined> => {
+ const setting = await get()
+
+ if (!setting?.privateKey) {
+ return undefined
+ }
+
+ //Clone the private key, remove the private parts
+ const c = cloneDeep(setting.privateKey)
+
+ delete c.d
+ delete c.p
+ delete c.q
+ delete c.dp
+ delete c.dq
+ delete c.qi
+
+ return {
+ ...c,
+ userName: setting.userName
+ } as PkiPubKey
+ }
+
+ return{
+ regenerateKey: optionsOnly(async (userName:string, params:EcKeyParams) => {
+ const p = {
+ ...params,
+ name: "ECDSA",
+ }
+
+ //Generate a new key
+ const key = await window.crypto.subtle.generateKey(p, true, ['sign', 'verify'])
+
+ //Convert to jwk
+ const privateKey = await window.crypto.subtle.exportKey('jwk', key.privateKey) as JWK;
+
+ //Convert to base64 so we can hash it easier
+ const b = btoa(privateKey.x! + privateKey.y!);
+
+ //take sha256 of the binary version of the coords
+ const digest = await crypto.subtle.digest('SHA-256', Base64ToUint8Array(b));
+
+ //Set the kid
+ privateKey.kid = ArrayToHexString(digest);
+
+ //Serial number is random hex
+ const serial = new Uint8Array(32)
+ crypto.getRandomValues(serial)
+ privateKey.serial = ArrayToHexString(serial);
+
+ //Save the key
+ await set({ userName, privateKey })
+ }),
+ getPubKey: optionsOnly(getPubKey),
+ generateOtp: optionsOnly(async () =>{
+ const setting = await get()
+ if (!setting?.privateKey) {
+ throw new Error('No key found')
+ }
+
+ const privKey = await importJWK(setting.privateKey as JWK)
+
+ const random = new Uint8Array(32)
+ crypto.getRandomValues(random)
+
+ const jwt = new SignJWT({
+ 'sub': setting.userName,
+ 'n': ArrayToHexString(random),
+ keyid: setting.privateKey.kid,
+ serial: privKey.serial
+ });
+
+ const token = await jwt.setIssuedAt()
+ .setProtectedHeader({ alg: setting.privateKey.alg! })
+ .setIssuer(state.currentConfig.value.apiUrl)
+ .setExpirationTime('30s')
+ .sign(privKey)
+
+ return token
+ })
+ }
+ },
+ foreground: exportForegroundApi<LocalPkiApi>([
+ 'regenerateKey',
+ 'getPubKey',
+ 'generateOtp'
+ ])
+ }
+}
diff --git a/extension/src/features/auth-api.ts b/extension/src/features/auth-api.ts
new file mode 100644
index 0000000..81bf6ea
--- /dev/null
+++ b/extension/src/features/auth-api.ts
@@ -0,0 +1,123 @@
+// 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 { AxiosInstance } from "axios";
+import { get, watchOnce } from "@vueuse/core";
+import { computed } from "vue";
+import { usePkiAuth, useSession, useUser } from "@vnuge/vnlib.browser";
+import { type FeatureApi, type BgRuntime, type IFeatureExport, exportForegroundApi, popupAndOptionsOnly, popupOnly } from "./framework";
+import type { ClientStatus } from "./types";
+import type { AppSettings } from "./settings";
+import type { JsonObject } from "type-fest";
+
+export interface ProectedHandler<T extends JsonObject> {
+ (message: T): Promise<any>
+}
+
+export interface MessageHandler<T extends JsonObject> {
+ (message: T): Promise<any>
+}
+
+export interface ApiMessageHandler<T extends JsonObject> {
+ (message: T, apiHandle: { axios: AxiosInstance }): Promise<any>
+}
+
+export interface UserApi extends FeatureApi {
+ login: (token: string) => Promise<boolean>
+ logout: () => Promise<void>
+ getProfile: () => Promise<any>
+ getStatus: () => Promise<ClientStatus>
+ waitForChange: () => Promise<void>
+}
+
+export const useAuthApi = (): IFeatureExport<AppSettings, UserApi> => {
+
+ return {
+ background: ({ state, onInstalled }:BgRuntime<AppSettings>): UserApi =>{
+ const { loggedIn, clearLoginState } = useSession();
+ const { currentConfig } = state
+ const { logout, getProfile, heartbeat, userName } = useUser();
+ const currentPkiPath = computed(() => `${currentConfig.value.accountBasePath}/pki`)
+
+ //Use pki login controls
+ const { login } = usePkiAuth(currentPkiPath as any)
+
+ //We can send post messages to the server heartbeat endpoint to get status
+ const runHeartbeat = async () => {
+ //Only run if the api thinks its logged in, and config is enabled
+ if (!loggedIn.value || currentConfig.value.heartbeat !== true) {
+ return
+ }
+
+ try {
+ //Post against the heartbeat endpoint
+ await heartbeat()
+ }
+ catch (error: any) {
+ if (error.response?.status === 401 || error.response?.status === 403) {
+ //If we get a 401, the user is no longer logged in
+ clearLoginState()
+ }
+ }
+ }
+
+ //Install hook for interval
+ onInstalled(() => {
+ //Configure interval to run every 5 minutes to update the status
+ setInterval(runHeartbeat, 60 * 1000);
+
+ //Run immediately
+ runHeartbeat();
+
+ return Promise.resolve();
+ })
+
+ return {
+ login: popupOnly(async (token: string): Promise<boolean> => {
+ //Perform login
+ await login(token)
+ //load profile
+ getProfile()
+ return true;
+ }),
+ logout: popupOnly(async (): Promise<void> => {
+ //Perform logout
+ await logout()
+ //Cleanup after logout
+ clearLoginState()
+ }),
+ getProfile: popupAndOptionsOnly(getProfile),
+ async getStatus (){
+ return {
+ //Logged in if the cookie is set and the api flag is set
+ loggedIn: get(loggedIn),
+ //username
+ userName: get(userName),
+ } as ClientStatus
+ },
+ async waitForChange(){
+ return new Promise((resolve) => watchOnce([currentConfig, loggedIn] as any, () => resolve()))
+ }
+ }
+ },
+ foreground: exportForegroundApi<UserApi>([
+ 'login',
+ 'logout',
+ 'getProfile',
+ 'getStatus',
+ 'waitForChange'
+ ]),
+ }
+} \ No newline at end of file
diff --git a/extension/src/features/framework/index.ts b/extension/src/features/framework/index.ts
new file mode 100644
index 0000000..c58ca68
--- /dev/null
+++ b/extension/src/features/framework/index.ts
@@ -0,0 +1,214 @@
+
+// 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 { 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 { debugLog } from "@vnuge/vnlib.browser";
+
+export interface BgRuntime<T> {
+ readonly state: T;
+ onInstalled(callback: () => Promise<void>): void;
+ onConnected(callback: () => Promise<void>): void;
+}
+
+export type FeatureApi = {
+ [key: string]: (... args: any[]) => Promise<any>
+};
+
+export type SendMessageHandler = <T extends JsonObject | JsonObject[]>(action: string, data: any) => Promise<T>
+export type VarArgsFunction<T> = (...args: any[]) => T
+export type FeatureConstructor<TState, T extends FeatureApi> = () => IFeatureExport<TState, T>
+export type DummyApiExport<T extends FeatureApi> = Array<keyof T>
+
+export interface IFeatureExport<TState, TFeature extends FeatureApi> {
+ /**
+ * Initializes a feature for mapping in the background runtime context
+ * @param bgRuntime The background runtime context
+ * @returns The feature's background api handlers that maps to the foreground
+ */
+ background(bgRuntime: BgRuntime<TState>): TFeature
+ /**
+ * Initializes the feature for mapping in any foreground runtime context
+ * @returns The feature's foreground api stub methods for mapping. They must
+ * match the background api
+ */
+ foreground(): TFeature
+}
+
+export interface IForegroundUnwrapper {
+ /**
+ * Unwraps a foreground feature and builds it's method bindings to
+ * the background handler
+ * @param feature The foreground feature that will be mapped to it's
+ * background handlers
+ * @returns The foreground feature's api stub methods
+ */
+ use: <T extends FeatureApi>(feature: FeatureConstructor<any, T>) => T
+}
+
+export interface IBackgroundWrapper<TState> {
+ register<T extends FeatureApi>(features: FeatureConstructor<TState, T>[]): void
+}
+
+export interface ProtectedFunction extends Function {
+ readonly protection: RuntimeContext[]
+}
+
+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 => {
+ (func as any).protection = protection
+ return func;
+}
+
+/**
+ * Creates a background runtime context for registering background
+ * script feature api handlers
+ */
+export const useBackgroundFeatures = <TState>(state: TState): IBackgroundWrapper<TState> => {
+
+ 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
+ * correspond to the method name. Foreground method calls
+ * are redirected to the send-message of the same unique name
+ */
+
+ return{
+ register: <TFeature extends FeatureApi>(features: FeatureConstructor<TState, TFeature>[]) => {
+ //Loop through features
+ for (const feature of features) {
+
+ //Init feature
+ const f = feature().background(rt)
+
+ //Get all exported function
+ for (const externFuncName in f) {
+
+ //get exported function
+ const func = f[externFuncName] as Function
+
+ const onMessageFuncName = `${feature.name}-${externFuncName}`
+
+ //register method with api
+ onMessage(onMessageFuncName, async (msg: BridgeMessage<any>) => {
+ try {
+
+ //Always an internal endpoint
+ if (!isInternalEndpoint(msg.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(...msg.data)
+ return isObjectLike(res) ? { ...res} : res
+ }
+ catch (e: any) {
+ debugLog(`Error in method ${onMessageFuncName}`, e)
+ const s = serializeError(e)
+ return {
+ bridgeMessageException: JSON.stringify(s),
+ axiosResponseError: JSON.stringify(e.response)
+ }
+ }
+ });
+ }
+ }
+ }
+ }
+}
+
+/**
+ * Creates a foreground runtime context for unwrapping foreground stub
+ * methods and redirecting them to thier background handler
+ */
+export const useForegoundFeatures = (sendMessage: SendMessageHandler): IForegroundUnwrapper => {
+
+ /**
+ * The goal of this function is to get the foreground interface object
+ * that should match the background implementation. All methods are
+ * intercepted and redirected to the background via send-message
+ */
+
+ return{
+ use: <T extends FeatureApi>(feature:FeatureConstructor<any, T>): T => {
+ //Register the feature
+ const api = feature().foreground()
+ const featureName = feature.name
+ const proxied : T = {} as T
+
+ //Loop through all methods
+ for(const funcName in api){
+
+ //Create proxy for each method
+ set(proxied, funcName, async (...args:any) => {
+
+ //Check for exceptions
+ const result = await sendMessage(`${featureName}-${funcName}`, cloneDeep(args)) as any
+
+ if(result?.bridgeMessageException){
+ const str = JSON.parse(result.bridgeMessageException)
+ const err = deserializeError(str)
+ //Recover axios response
+ if(result.axiosResponseError){
+ (err as any).response = JSON.parse(result.axiosResponseError)
+ }
+
+ throw err;
+ }
+
+ return result;
+ })
+ }
+
+ return proxied;
+ }
+ }
+}
+
+export const exportForegroundApi = <T extends FeatureApi>(args: DummyApiExport<T>): () => T => {
+ //Create the type from the array of type properties
+ const type = {} as T
+
+ //Loop through all properties
+ for(const prop of args){
+ //Default the property to an implementation error
+ type[prop] = (async () => {
+ throw new Error(`Method ${prop.toString()} not implemented`)
+ }) as any
+ }
+
+ return () => type
+} \ No newline at end of file
diff --git a/extension/src/features/history.ts b/extension/src/features/history.ts
new file mode 100644
index 0000000..ff7c267
--- /dev/null
+++ b/extension/src/features/history.ts
@@ -0,0 +1,42 @@
+// 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 { ref } from "vue";
+import { } from "./types";
+import { FeatureApi, BgRuntime, IFeatureExport } from "./framework";
+import { AppSettings } from "./settings";
+
+export interface HistoryEvent extends Object{
+
+}
+
+export interface HistoryApi extends FeatureApi{
+
+}
+
+export const useHistoryApi = () : IFeatureExport<AppSettings, HistoryApi> => {
+ return{
+ background: ({ }: BgRuntime<AppSettings>): HistoryApi =>{
+ const evHistory = ref([]);
+
+ return{ }
+ },
+ foreground: (): HistoryApi =>{
+ return { }
+ }
+ }
+}
+
+//Listen for messages \ No newline at end of file
diff --git a/extension/src/features/identity-api.ts b/extension/src/features/identity-api.ts
new file mode 100644
index 0000000..07b6387
--- /dev/null
+++ b/extension/src/features/identity-api.ts
@@ -0,0 +1,112 @@
+// 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 { Endpoints, useServerApi } from "./server-api";
+import { NostrPubKey, Watchable } from "./types";
+import {
+ type FeatureApi,
+ type IFeatureExport,
+ type BgRuntime,
+ optionsOnly,
+ popupAndOptionsOnly,
+ exportForegroundApi
+} from "./framework";
+import { AppSettings } from "./settings";
+import { ref, watch } from "vue";
+import { useSession } from "@vnuge/vnlib.browser";
+import { get, set, useToggle, watchOnce } from "@vueuse/core";
+import { find, isArray } from "lodash";
+
+export interface IdentityApi extends FeatureApi, Watchable {
+ createIdentity: (identity: NostrPubKey) => Promise<NostrPubKey>
+ updateIdentity: (identity: NostrPubKey) => Promise<NostrPubKey>
+ deleteIdentity: (key: NostrPubKey) => Promise<void>
+ getAllKeys: () => Promise<NostrPubKey[]>;
+ getPublicKey: () => Promise<NostrPubKey | undefined>;
+ selectKey: (key: NostrPubKey) => Promise<void>;
+}
+
+export const useIdentityApi = (): IFeatureExport<AppSettings, IdentityApi> => {
+ return{
+ background: ({ state }: BgRuntime<AppSettings>) =>{
+ const { execRequest } = useServerApi(state);
+ const { loggedIn } = useSession();
+
+ //Get the current selected key
+ const selectedKey = ref<NostrPubKey | undefined>();
+ const [ onTriggered , triggerChange ] = useToggle()
+
+ //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)
+ triggerChange()
+ }),
+ updateIdentity: optionsOnly(async (id: NostrPubKey) => {
+ await execRequest<NostrPubKey>(Endpoints.UpdateId, id)
+ triggerChange()
+ }),
+ deleteIdentity: optionsOnly(async (key: NostrPubKey) => {
+ await execRequest<NostrPubKey>(Endpoints.DeleteKey, key);
+ triggerChange()
+ }),
+ selectKey: popupAndOptionsOnly((key: NostrPubKey): Promise<void> => {
+ selectedKey.value = 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]
+ },
+ 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()))
+ }
+ }
+ },
+ foreground: exportForegroundApi([
+ 'createIdentity',
+ 'updateIdentity',
+ 'deleteIdentity',
+ 'getAllKeys',
+ 'getPublicKey',
+ 'selectKey',
+ 'waitForChange'
+ ])
+ }
+} \ No newline at end of file
diff --git a/extension/src/features/index.ts b/extension/src/features/index.ts
new file mode 100644
index 0000000..320ea1c
--- /dev/null
+++ b/extension/src/features/index.ts
@@ -0,0 +1,33 @@
+// 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/>.
+
+//Export all shared types
+export type { NostrPubKey, LoginMessage } from './types'
+export type * from './framework'
+export type { PluginConfig } from './settings'
+export type { PkiPubKey, EcKeyParams, LocalPkiApi as PkiApi } from './account-api'
+export type { NostrApi } from './nostr-api'
+export type { UserApi } from './auth-api'
+export type { IdentityApi } from './identity-api'
+
+export { useBackgroundFeatures, useForegoundFeatures } from './framework'
+export { useLocalPki, usePkiApi } from './account-api'
+export { useAuthApi } from './auth-api'
+export { useIdentityApi } from './identity-api'
+export { useNostrApi } from './nostr-api'
+export { useSettingsApi, useAppSettings } from './settings'
+export { useHistoryApi } from './history'
+export { useEventTagFilterApi } from './tagfilter-api'
+export { useInjectAllowList } from './nip07allow-api' \ No newline at end of file
diff --git a/extension/src/features/nip07allow-api.ts b/extension/src/features/nip07allow-api.ts
new file mode 100644
index 0000000..eff4ff8
--- /dev/null
+++ b/extension/src/features/nip07allow-api.ts
@@ -0,0 +1,181 @@
+// 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 { storage, tabs, type Tabs } from "webextension-polyfill";
+import { Watchable, useSingleSlotStorage } from "./types";
+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";
+
+interface AllowedSites{
+ origins: string[];
+ enabled: boolean;
+}
+export interface AllowedOriginStatus{
+ readonly allowedOrigins: string[];
+ readonly enabled: boolean;
+ readonly currentOrigin?: string;
+ readonly isAllowed: boolean;
+}
+
+export interface InjectAllowlistApi extends FeatureApi, Watchable {
+ addOrigin(origin?: string): Promise<void>;
+ removeOrigin(origin?: string): Promise<void>;
+ getStatus(): Promise<AllowedOriginStatus>;
+ enable(value: boolean): Promise<void>;
+}
+
+export const useInjectAllowList = (): IFeatureExport<AppSettings, InjectAllowlistApi> => {
+ return {
+ background: ({ }: BgRuntime<AppSettings>) => {
+
+ const store = useSingleSlotStorage<AllowedSites>(storage.local, 'nip07-allowlist', { origins: [], enabled: true });
+
+ //watch current tab
+ const allowedOrigins = ref<string[]>([])
+ const protectionEnabled = ref<boolean>(true)
+ const [manullyTriggered, trigger] = useToggle()
+
+ const { currentOrigin, currentTab } = (() => {
+
+ const currentTab = ref<Tabs.Tab | undefined>(undefined)
+ const currentOrigin = computed(() => currentTab.value?.url ? new URL(currentTab.value.url).origin : undefined)
+
+ //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
+ })
+ return { currentTab, currentOrigin }
+ })()
+
+ const writeChanges = async () => {
+ await store.set({ origins: [...get(allowedOrigins)], enabled: get(protectionEnabled) })
+ }
+
+ //Initial load
+ store.get().then((data) => {
+ allowedOrigins.value = data.origins
+ protectionEnabled.value = data.enabled
+ })
+
+ const isOriginAllowed = (origin?: string): boolean => {
+ //If protection is not enabled, allow all
+ if(protectionEnabled.value == false){
+ return true;
+ }
+ //if no origin specified, use current origin
+ origin = defaultTo(origin, currentOrigin.value)
+
+ //If no origin, return false
+ if (!origin) {
+ return false;
+ }
+
+ //Default to origin only
+ const originOnly = new URL(origin).origin
+ return includes(allowedOrigins.value, originOnly)
+ }
+
+ const addOrigin = async (origin?: string): Promise<void> => {
+ //if no origin specified, use current origin
+ const newOrigin = defaultTo(origin, currentOrigin.value)
+ if (!newOrigin) {
+ return;
+ }
+
+ const originOnly = new URL(newOrigin).origin
+
+ //See if origin is already in the list
+ if (!includes(allowedOrigins.value, originOnly)) {
+ //Add to the list
+ allowedOrigins.value.push(originOnly);
+ trigger();
+
+ //Save changes
+ await writeChanges()
+
+ //If current tab was added, reload the tab
+ if (!origin) {
+ await tabs.reload(currentTab.value?.id)
+ }
+ }
+ }
+
+ const removeOrigin = async (origin?: string): Promise<void> => {
+ //Allow undefined to remove current origin
+ const delOrigin = defaultTo(origin, currentOrigin.value)
+ if (!delOrigin) {
+ return;
+ }
+
+ //Get origin part of url
+ const delOriginOnly = new URL(delOrigin).origin
+ const allowList = get(allowedOrigins)
+
+ //Remove the origin
+ allowedOrigins.value = filter(allowList, (o) => !isEqual(o, delOriginOnly));
+ trigger();
+
+ await writeChanges()
+
+ //If current tab was removed, reload the tab
+ if (!origin) {
+ await tabs.reload(currentTab.value?.id)
+ }
+ }
+
+
+ return {
+ addOrigin: popupAndOptionsOnly(addOrigin),
+ removeOrigin: popupAndOptionsOnly(removeOrigin),
+ enable: popupAndOptionsOnly(async (value: boolean): Promise<void> => {
+ set(protectionEnabled, value)
+ await writeChanges()
+ }),
+ async getStatus(): Promise<AllowedOriginStatus> {
+ return{
+ allowedOrigins: [...get(allowedOrigins)],
+ enabled: get(protectionEnabled),
+ currentOrigin: get(currentOrigin),
+ isAllowed: isOriginAllowed()
+ }
+ },
+ waitForChange: () => {
+ //Wait for the trigger to change
+ return new Promise((resolve) => watchOnce([currentTab, protectionEnabled, manullyTriggered] as any, () => resolve()));
+ },
+ }
+ },
+ foreground: exportForegroundApi([
+ 'addOrigin',
+ 'removeOrigin',
+ 'getStatus',
+ 'enable',
+ 'waitForChange'
+ ])
+ }
+} \ No newline at end of file
diff --git a/extension/src/features/nostr-api.ts b/extension/src/features/nostr-api.ts
new file mode 100644
index 0000000..307522d
--- /dev/null
+++ b/extension/src/features/nostr-api.ts
@@ -0,0 +1,79 @@
+// 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 { Endpoints, useServerApi } from "./server-api";
+import { NostrRelay, EventMessage, NostrEvent } from './types'
+import { FeatureApi, BgRuntime, IFeatureExport, optionsOnly, exportForegroundApi } from "./framework";
+import { AppSettings } from "./settings";
+import { useTagFilter } from "./tagfilter-api";
+
+
+/**
+ * The NostrApi is the foreground api for nostr events via
+ * the background script.
+ */
+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>;
+}
+
+export const useNostrApi = (): IFeatureExport<AppSettings, NostrApi> => {
+
+ return{
+ background: ({ state }: BgRuntime<AppSettings>) =>{
+
+ const { execRequest } = useServerApi(state);
+ const { filterTags } = useTagFilter()
+
+ return {
+ getRelays: async (): Promise<NostrRelay[]> => {
+ //Get preferred relays for the current user
+ const data = await execRequest<NostrRelay[]>(Endpoints.GetRelays)
+ return [...data]
+ },
+ signEvent: async (req: NostrEvent): Promise<NostrEvent | undefined> => {
+
+ //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);
+ return event;
+ },
+ nip04Encrypt: async (data: EventMessage): Promise<string> => {
+ return execRequest<string>(Endpoints.Encrypt, data);
+ },
+ nip04Decrypt: (data: EventMessage): Promise<string> => {
+ return execRequest<string>(Endpoints.Decrypt, data);
+ },
+ setRelay: optionsOnly((relay: NostrRelay): Promise<NostrRelay | undefined> => {
+ return execRequest<NostrRelay>(Endpoints.SetRelay, relay)
+ }),
+ }
+ },
+ foreground: exportForegroundApi([
+ 'getRelays',
+ 'signEvent',
+ 'setRelay',
+ 'nip04Encrypt',
+ 'nip04Decrypt'
+ ])
+ }
+}
diff --git a/extension/src/entries/background/permissions.ts b/extension/src/features/permissions.ts
index 4732bb7..c06257b 100644
--- a/extension/src/entries/background/permissions.ts
+++ b/extension/src/features/permissions.ts
@@ -17,7 +17,7 @@ import { useStorageAsync } from "@vueuse/core";
import { find, isEmpty, merge, remove } from "lodash";
import { storage } from "webextension-polyfill";
import { useAuthApi } from "./auth-api";
-import { useSettings } from "./settings";
+import { useSettingsApi } from "./settings";
const permissions = useStorageAsync("permissions", [], storage.local);
@@ -51,7 +51,7 @@ export const removeAutoAllow = async (origin, mKind, keyId) => {
export const useSitePermissions = (() => {
const { apiCall, handleProtectedMessage } = useAuthApi();
- const { currentConfig } = useSettings();
+ const { currentConfig } = useSettingsApi();
const getCurrentPerms = async () => {
diff --git a/extension/src/entries/background/server-api/endpoints.ts b/extension/src/features/server-api/endpoints.ts
index 6da9c71..9c73866 100644
--- a/extension/src/entries/background/server-api/endpoints.ts
+++ b/extension/src/features/server-api/endpoints.ts
@@ -32,15 +32,16 @@ export const initEndponts = () => {
const endpoints = new Map<string, EndpointDefinition>();
+ //Get local axios
+ const axios = useAxios(null);
+
const registerEndpoint = <T extends string>(def: EndpointDefinitionReg<T>) => {
//Store the handler by its id
endpoints.set(def.id, def);
return def;
}
- const getEndpoint = <T extends string>(id: T): EndpointDefinition | undefined => {
- return endpoints.get(id);
- }
+ const getEndpoint = <T extends string>(id: T): EndpointDefinition | undefined => endpoints.get(id);
const execRequest = async <T>(id: string, ...request: any): Promise<T> => {
const endpoint = getEndpoint(id);
@@ -53,12 +54,9 @@ export const initEndponts = () => {
//Execute the request handler
const req = await endpoint.onRequest(...request);
-
- //Get axios
- const axios = useAxios(null);
//Exec the request
- const { data } = await axios({ method: endpoint.method, url: path, data: req });
+ const { data } = await axios.request({ method: endpoint.method, url: path, data: req });
//exec the response handler and return its result
return await endpoint.onResponse(data, request);
diff --git a/extension/src/features/server-api/index.ts b/extension/src/features/server-api/index.ts
new file mode 100644
index 0000000..6aa34da
--- /dev/null
+++ b/extension/src/features/server-api/index.ts
@@ -0,0 +1,137 @@
+// 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 { 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"
+
+export enum Endpoints {
+ GetKeys = 'getKeys',
+ DeleteKey = 'deleteKey',
+ SignEvent = 'signEvent',
+ GetRelays = 'getRelays',
+ SetRelay = 'setRelay',
+ Encrypt = 'encrypt',
+ Decrypt = 'decrypt',
+ CreateId = 'createIdentity',
+ UpdateId = 'updateIdentity',
+ UpdateProfile = 'updateProfile',
+}
+
+export const useServerApi = (settings: AppSettings) => {
+ const { registerEndpoint, execRequest } = initEndponts()
+
+ //ref to nostr endpoint url
+ const nostrUrl = computed(() => settings.currentConfig.value.nostrEndpoint || '/nostr');
+ const accUrl = computed(() => settings.currentConfig.value.accountBasePath || '/account');
+
+ registerEndpoint({
+ id: Endpoints.GetKeys,
+ method: 'GET',
+ path: () => `${get(nostrUrl)}?type=getKeys`,
+ onRequest: () => Promise.resolve(),
+ onResponse: (response) => Promise.resolve(response)
+ })
+
+ registerEndpoint({
+ id: Endpoints.DeleteKey,
+ method: 'DELETE',
+ path: (key:NostrIdentiy) => `${get(nostrUrl)}?type=identity&key_id=${key.Id}`,
+ onRequest: () => Promise.resolve(),
+ onResponse: async (response: WebMessage) => response.getResultOrThrow()
+ })
+
+ registerEndpoint({
+ id: Endpoints.SignEvent,
+ method: 'POST',
+ path: () => `${get(nostrUrl)}?type=signEvent`,
+ onRequest: (event) => Promise.resolve(event),
+ onResponse: async (response: WebMessage<NostrEvent>) => {
+ const res = response.getResultOrThrow()
+ delete (res as any).KeyId;
+ return res;
+ }
+ })
+
+ registerEndpoint({
+ id: Endpoints.GetRelays,
+ method: 'GET',
+ path: () => `${get(nostrUrl)}?type=getRelays`,
+ onRequest: () => Promise.resolve(),
+ onResponse: (response) => Promise.resolve(response)
+ })
+
+ registerEndpoint({
+ id: Endpoints.SetRelay,
+ method: 'POST',
+ path: () => `${get(nostrUrl)}?type=relay`,
+ onRequest: (relay:NostrRelay) => Promise.resolve(relay),
+ onResponse: (response) => Promise.resolve(response)
+ })
+
+ registerEndpoint({
+ id: Endpoints.CreateId,
+ method: 'PUT',
+ path: () => `${get(nostrUrl)}?type=identity`,
+ onRequest: (identity:NostrIdentiy) => Promise.resolve(identity),
+ onResponse: async (response: WebMessage<NostrEvent>) => response.getResultOrThrow()
+ })
+
+ registerEndpoint({
+ id: Endpoints.UpdateId,
+ method: 'PATCH',
+ path: () => `${get(nostrUrl)}?type=identity`,
+ onRequest: (identity:NostrIdentiy) => {
+ const id = cloneDeep(identity) as any;
+ delete id.Created;
+ delete id.LastModified;
+ return Promise.resolve(id)
+ },
+ onResponse: async (response: WebMessage<NostrEvent>) => response.getResultOrThrow()
+ })
+
+ registerEndpoint({
+ id: Endpoints.UpdateProfile,
+ method: 'POST',
+ path: () => `${get(accUrl)}`,
+ onRequest: (profile: UserProfile) => Promise.resolve(cloneDeep(profile)),
+ onResponse: async (response: WebMessage<string>) => response.getResultOrThrow()
+ })
+
+ //Register nip04 events
+ registerEndpoint({
+ id:Endpoints.Encrypt,
+ method:'POST',
+ path: () => `${get(nostrUrl)}?type=encrypt`,
+ onRequest: (data: NostrEvent) => Promise.resolve(data),
+ onResponse: async (response: WebMessage<string>) => response.getResultOrThrow()
+ })
+
+ registerEndpoint({
+ id:Endpoints.Decrypt,
+ method:'POST',
+ path: () => `${get(nostrUrl)}?type=decrypt`,
+ onRequest: (data: NostrEvent) => Promise.resolve(data),
+ onResponse: async (response: WebMessage<string>) => response.getResultOrThrow()
+ })
+
+ return { execRequest }
+} \ No newline at end of file
diff --git a/extension/src/features/settings.ts b/extension/src/features/settings.ts
new file mode 100644
index 0000000..a67957c
--- /dev/null
+++ b/extension/src/features/settings.ts
@@ -0,0 +1,165 @@
+// 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 { storage } from "webextension-polyfill"
+import { isEmpty, merge } from 'lodash'
+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 { get, watchOnce } from "@vueuse/core";
+
+export interface PluginConfig extends JsonObject {
+ readonly apiUrl: string;
+ readonly accountBasePath: string;
+ readonly nostrEndpoint: string;
+ readonly heartbeat: boolean;
+ readonly maxHistory: number;
+ readonly tagFilter: boolean,
+}
+
+//Default storage config
+const defaultConfig : PluginConfig = {
+ apiUrl: import.meta.env.VITE_API_URL,
+ accountBasePath: import.meta.env.VITE_ACCOUNTS_BASE_PATH,
+ nostrEndpoint: import.meta.env.VITE_NOSTR_ENDPOINT,
+ heartbeat: import.meta.env.VITE_HEARTBEAT_ENABLED === 'true',
+ maxHistory: 50,
+ tagFilter: true,
+};
+
+export interface AppSettings{
+ getCurrentConfig: () => Promise<PluginConfig>;
+ restoreApiSettings: () => Promise<void>;
+ saveConfig: (config: PluginConfig) => Promise<void>;
+ readonly currentConfig: Readonly<Ref<PluginConfig>>;
+}
+
+export interface SettingsApi extends FeatureApi, Watchable {
+ getSiteConfig: () => Promise<PluginConfig>;
+ setSiteConfig: (config: PluginConfig) => Promise<PluginConfig>;
+ setDarkMode: (darkMode: boolean) => Promise<void>;
+ getDarkMode: () => Promise<boolean>;
+}
+
+export const useAppSettings = (): AppSettings => {
+ const currentConfig = ref<PluginConfig>({} as PluginConfig);
+ const store = useSingleSlotStorage<PluginConfig>(storage.local, 'siteConfig', defaultConfig);
+
+ const getCurrentConfig = async () => {
+
+ const siteConfig = await store.get()
+
+ //Store a default config if none exists
+ if (isEmpty(siteConfig)) {
+ await store.set(defaultConfig);
+ }
+
+ //Merge the default config with the site config
+ return merge(defaultConfig, siteConfig)
+ }
+
+ const restoreApiSettings = async () => {
+ //Set the current config
+ const current = await getCurrentConfig();
+ currentConfig.value = current;
+
+ //Configure the vnlib api
+ configureApi({
+ session: {
+ cookiesEnabled: false,
+ browserIdSize: 32,
+ },
+ user: {
+ accountBasePath: current.accountBasePath,
+ },
+ axios: {
+ baseURL: current.apiUrl,
+ tokenHeader: import.meta.env.VITE_WEB_TOKEN_HEADER,
+ },
+ storage: localStorage
+ })
+ }
+
+ const saveConfig = async (config: PluginConfig) => {
+ //Save the config and update the current config
+ await store.set(config);
+ currentConfig.value = config;
+ }
+
+ return {
+ getCurrentConfig,
+ restoreApiSettings,
+ saveConfig,
+ currentConfig:readonly(currentConfig)
+ }
+}
+
+export const useSettingsApi = () : IFeatureExport<AppSettings, SettingsApi> =>{
+
+ return{
+ background: ({ state, onConnected, onInstalled }: BgRuntime<AppSettings>) => {
+
+ const _darkMode = ref(false);
+
+ onInstalled(async () => {
+ await state.restoreApiSettings();
+ debugLog('Server settings restored from storage');
+ })
+
+ onConnected(async () => {
+ //refresh the config on connect
+ await state.restoreApiSettings();
+ })
+
+ return {
+
+ getSiteConfig: () => state.getCurrentConfig(),
+
+ setSiteConfig: optionsOnly(async (config: PluginConfig): Promise<PluginConfig> => {
+
+ //Save the config
+ await state.saveConfig(config);
+
+ //Restore the api settings
+ await state.restoreApiSettings();
+
+ debugLog('Config settings saved!');
+
+ //Return the config
+ return state.currentConfig.value
+ }),
+
+ setDarkMode: optionsOnly(async (darkMode: boolean) => {
+ console.log('Setting dark mode to', darkMode, 'from', _darkMode.value)
+ _darkMode.value = darkMode
+ }),
+ getDarkMode: async () => get(_darkMode),
+
+ waitForChange: () => {
+ return new Promise((resolve) => watchOnce([state.currentConfig, _darkMode] as any, () => resolve()))
+ },
+ }
+ },
+ foreground: exportForegroundApi([
+ 'getSiteConfig',
+ 'setSiteConfig',
+ 'setDarkMode',
+ 'getDarkMode',
+ 'waitForChange'
+ ])
+ }
+} \ No newline at end of file
diff --git a/extension/src/features/tagfilter-api.ts b/extension/src/features/tagfilter-api.ts
new file mode 100644
index 0000000..75f4b2a
--- /dev/null
+++ b/extension/src/features/tagfilter-api.ts
@@ -0,0 +1,125 @@
+// 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 { storage } from "webextension-polyfill";
+import { NostrEvent, TaggedNostrEvent, useSingleSlotStorage } from "./types";
+import { filter, isEmpty, isEqual, isRegExp } from "lodash";
+import { BgRuntime, FeatureApi, IFeatureExport, exportForegroundApi } from "./framework";
+import { AppSettings } from "./settings";
+
+interface EventTagFilteStorage {
+ filters: string[];
+}
+
+export interface EventTagFilterApi extends FeatureApi {
+ filterTags(event: TaggedNostrEvent): Promise<void>;
+ addFilter(tag: string): Promise<void>;
+ removeFilter(tag: string): Promise<void>;
+ addFilters(tags: string[]): Promise<void>;
+}
+
+export const useTagFilter = () => {
+ //use storage
+ const { get, set } = useSingleSlotStorage<EventTagFilteStorage>(storage.local, 'tag-filter-struct', { filters: [] });
+
+ return {
+ filterTags: async (event: TaggedNostrEvent): Promise<void> => {
+
+ if(!event.tags){
+ return;
+ }
+
+ //Load latest filter list
+ const data = await get();
+
+ if(isEmpty(event.tags)){
+ return;
+ }
+
+ /*
+ * Nostr events contain a nested array of tags, they may be any
+ * json type. The first element of the array should be the tag name
+ * and the rest of the array is the tag data.
+ */
+ const allowedTags = filter(event.tags, ([tagName]) => {
+ if(!tagName){
+ return false;
+ }
+
+ if(!data.filters.length){
+ return true;
+ }
+
+ const asString = tagName.toString();
+
+ for (const filter of data.filters) {
+ //if the filter is a regex, test it, if it fails, its allowed
+ if (isRegExp(filter)) {
+ if (filter.test(asString)) {
+ return false;
+ }
+ }
+ //If the filter is a string, compare it, if it matches, it's not allowed
+ if (isEqual(filter, asString)) {
+ return false;
+ }
+ }
+
+ //Its allowed
+ return true;
+ })
+
+ //overwrite tags array
+ event.tags = allowedTags;
+ },
+ addFilter: async (tag: string) => {
+ const data = await get();
+ //add new filter to list
+ data.filters.push(tag);
+ //save new config
+ await set(data);
+ },
+ removeFilter: async (tag: string) => {
+ const data = await get();
+ //remove filter from list
+ data.filters = filter(data.filters, (t) => !isEqual(t, tag));
+ //save new config
+ await set(data);
+ },
+ addFilters: async (tags: string[]) => {
+ const data = await get();
+ //add new filters to list
+ data.filters.push(...tags);
+ //save new config
+ await set(data);
+ }
+ }
+}
+
+export const useEventTagFilterApi = (): IFeatureExport<AppSettings, EventTagFilterApi> => {
+ return{
+ background: ({ }: BgRuntime<AppSettings>) => {
+ return{
+ ...useTagFilter()
+ }
+ },
+ foreground: exportForegroundApi([
+ 'filterTags',
+ 'addFilter',
+ 'removeFilter',
+ 'addFilters',
+ ])
+ }
+} \ No newline at end of file
diff --git a/extension/src/features/types.ts b/extension/src/features/types.ts
new file mode 100644
index 0000000..bd0afee
--- /dev/null
+++ b/extension/src/features/types.ts
@@ -0,0 +1,147 @@
+// 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 { defer } from "lodash";
+import { JsonObject } from "type-fest";
+
+export interface NostrPubKey extends JsonObject {
+ readonly Id: string,
+ readonly UserName: string,
+ readonly PublicKey: string,
+ readonly Created: string,
+ readonly LastModified: string
+}
+
+export interface NostrEvent extends JsonObject {
+ KeyId: string,
+ readonly id: string,
+ readonly pubkey: string,
+ readonly content: string,
+}
+
+export interface TaggedNostrEvent extends NostrEvent {
+ tags?: any[][]
+}
+
+export interface EventMessage extends JsonObject {
+ readonly event: NostrEvent
+}
+
+export interface NostrRelay extends JsonObject {
+ readonly Id: string,
+ readonly url: string,
+ readonly flags: number,
+ readonly Created: string,
+ readonly LastModified: string
+}
+
+export interface LoginMessage extends JsonObject {
+ readonly token: string
+}
+
+export interface ClientStatus extends JsonObject {
+ readonly loggedIn: boolean;
+ readonly userName: string | null;
+}
+
+export enum NostrRelayFlags {
+ None = 0,
+ Default = 1,
+ Preferred = 2,
+}
+
+export enum NostrRelayMessageType{
+ updateRelay = 1,
+ addRelay = 2,
+ deleteRelay = 3
+}
+
+export interface NostrRelayMessage extends JsonObject {
+ readonly relay: NostrRelay
+ readonly type: NostrRelayMessageType
+}
+
+export interface LoginMessage extends JsonObject {
+ readonly token: string
+ readonly username: string
+ readonly password: string
+}
+
+export interface Watchable{
+ waitForChange(): Promise<void>;
+}
+
+export const useStorage = (storage: any & chrome.storage.StorageArea) => {
+ const get = async <T>(key: string): Promise<T | undefined> => {
+ const value = await storage.get(key)
+ return value[key] as T;
+ }
+
+ const set = async <T>(key: string, value: T): Promise<void> => {
+ await storage.set({ [key]: value });
+ }
+
+ const remove = async (key: string): Promise<void> => {
+ await storage.remove(key);
+ }
+
+ return { get, set, remove }
+}
+
+export interface SingleSlotStorage<T>{
+ get(): Promise<T | undefined>;
+ set(value: T): Promise<void>;
+ remove(): Promise<void>;
+}
+
+export interface DefaultSingleSlotStorage<T>{
+ get(): Promise<T>;
+ set(value: T): Promise<void>;
+ remove(): Promise<void>;
+}
+
+export interface UseSingleSlotStorage{
+ <T>(storage: any & chrome.storage.StorageArea, key: string): SingleSlotStorage<T>;
+ <T>(storage: any & chrome.storage.StorageArea, key: string, defaultValue: T): DefaultSingleSlotStorage<T>;
+}
+
+const _useSingleSlotStorage = <T>(storage: any & chrome.storage.StorageArea, key: string, defaultValue?: T) => {
+ const s = useStorage(storage);
+
+ const get = async (): Promise<T | undefined> => {
+ return await s.get<T>(key) || defaultValue;
+ }
+
+ const set = (value: T): Promise<void> => s.set(key, value);
+ const remove = (): Promise<void> => s.remove(key);
+
+ return { get, set, remove }
+}
+
+export const useSingleSlotStorage: UseSingleSlotStorage = _useSingleSlotStorage;
+
+export const onWatchableChange = (watchable: Watchable, onChangeCallback: () => Promise<any>, controls? : { immediate: boolean}) => {
+
+ defer(async () => {
+ if (controls?.immediate) {
+ await onChangeCallback();
+ }
+
+ while (true) {
+ await watchable.waitForChange();
+ await onChangeCallback();
+ }
+ })
+} \ No newline at end of file
diff --git a/extension/src/manifest.js b/extension/src/manifest.js
index 19d51b1..6c96f41 100644
--- a/extension/src/manifest.js
+++ b/extension/src/manifest.js
@@ -19,7 +19,7 @@ import pkg from "../package.json";
const sharedManifest = {
content_scripts: [
{
- js: ["src/entries/contentScript/primary/main.js", "src/entries/contentScript/nostr-shim.js"],
+ js: ["src/entries/contentScript/primary/main.js"],
matches: ["*://*/*"]
},
],
@@ -37,7 +37,8 @@ const sharedManifest = {
browser_style:false
},
permissions: [
- 'storage'
+ 'storage',
+ 'activeTab',
],