diff options
Diffstat (limited to 'extension/src/features')
-rw-r--r-- | extension/src/features/framework/index.ts | 44 | ||||
-rw-r--r-- | extension/src/features/identity-api.ts | 59 | ||||
-rw-r--r-- | extension/src/features/nip07allow-api.ts | 16 | ||||
-rw-r--r-- | extension/src/features/nostr-api.ts | 40 | ||||
-rw-r--r-- | extension/src/features/server-api/index.ts | 28 | ||||
-rw-r--r-- | extension/src/features/settings.ts | 4 | ||||
-rw-r--r-- | extension/src/features/types.ts | 14 |
7 files changed, 125 insertions, 80 deletions
diff --git a/extension/src/features/framework/index.ts b/extension/src/features/framework/index.ts index c58ca68..44ae031 100644 --- a/extension/src/features/framework/index.ts +++ b/extension/src/features/framework/index.ts @@ -15,12 +15,11 @@ // along with this program. If not, see <https://www.gnu.org/licenses/>. import { runtime } from "webextension-polyfill"; -import { createBackgroundPort } from '../../webext-bridge' -import { BridgeMessage, RuntimeContext, isInternalEndpoint } from "../../webext-bridge"; import { serializeError, deserializeError } from 'serialize-error'; import { JsonObject } from "type-fest"; -import { cloneDeep, isObjectLike, set } from "lodash"; +import { cloneDeep, isArray, isObjectLike, set } from "lodash"; import { debugLog } from "@vnuge/vnlib.browser"; +import { ChannelContext, createMessageChannel } from "../../messaging"; export interface BgRuntime<T> { readonly state: T; @@ -68,16 +67,15 @@ export interface IBackgroundWrapper<TState> { } export interface ProtectedFunction extends Function { - readonly protection: RuntimeContext[] + readonly protection: ChannelContext[] } export const optionsOnly = <T extends Function>(func: T): T => protectMethod(func, 'options'); export const popupOnly = <T extends Function>(func: T): T => protectMethod(func, 'popup'); export const contentScriptOnly = <T extends Function>(func: T): T => protectMethod(func, 'content-script'); -export const windowOnly = <T extends Function>(func: T): T => protectMethod(func, 'window'); export const popupAndOptionsOnly = <T extends Function>(func: T): T => protectMethod(func, 'popup', 'options'); -export const protectMethod = <T extends Function>(func: T, ...protection: RuntimeContext[]): T => { +export const protectMethod = <T extends Function>(func: T, ...protection: ChannelContext[]): T => { (func as any).protection = protection return func; } @@ -88,14 +86,16 @@ export const protectMethod = <T extends Function>(func: T, ...protection: Runtim */ export const useBackgroundFeatures = <TState>(state: TState): IBackgroundWrapper<TState> => { + const { openOnMessageChannel } = createMessageChannel('background'); + const { onMessage } = openOnMessageChannel() + + const rt = { state, onConnected: runtime.onConnect.addListener, onInstalled: runtime.onInstalled.addListener, } as BgRuntime<TState> - const { onMessage } = createBackgroundPort() - /** * Each plugin will export named methods. Background methods * are captured and registered as on-message handlers that @@ -120,20 +120,25 @@ export const useBackgroundFeatures = <TState>(state: TState): IBackgroundWrapper const onMessageFuncName = `${feature.name}-${externFuncName}` //register method with api - onMessage(onMessageFuncName, async (msg: BridgeMessage<any>) => { + onMessage<any>(onMessageFuncName, async (sender, payload) => { try { - //Always an internal endpoint - if (!isInternalEndpoint(msg.sender)) { + if ((func as ProtectedFunction).protection + && !(func as ProtectedFunction).protection.includes(sender)) { throw new Error(`Unauthorized external call to ${onMessageFuncName}`) } - if ((func as ProtectedFunction).protection - && !(func as ProtectedFunction).protection.includes(msg.sender.context)) { - throw new Error(`Unauthorized external call to ${onMessageFuncName}`) + const res = await func(...payload) + + if(isArray(res)){ + return [...res] + } + else if(isObjectLike(res)){ + return { ...res } + } + else{ + return res } - const res = await func(...msg.data) - return isObjectLike(res) ? { ...res} : res } catch (e: any) { debugLog(`Error in method ${onMessageFuncName}`, e) @@ -154,8 +159,11 @@ export const useBackgroundFeatures = <TState>(state: TState): IBackgroundWrapper * Creates a foreground runtime context for unwrapping foreground stub * methods and redirecting them to thier background handler */ -export const useForegoundFeatures = (sendMessage: SendMessageHandler): IForegroundUnwrapper => { +export const useForegoundFeatures = (context: ChannelContext): IForegroundUnwrapper => { + const { openChannel } = createMessageChannel(context); + const { sendMessage } = openChannel() + /** * The goal of this function is to get the foreground interface object * that should match the background implementation. All methods are @@ -211,4 +219,4 @@ export const exportForegroundApi = <T extends FeatureApi>(args: DummyApiExport<T } return () => type -}
\ No newline at end of file +} diff --git a/extension/src/features/identity-api.ts b/extension/src/features/identity-api.ts index 07b6387..d7db5ff 100644 --- a/extension/src/features/identity-api.ts +++ b/extension/src/features/identity-api.ts @@ -24,10 +24,10 @@ import { exportForegroundApi } from "./framework"; import { AppSettings } from "./settings"; -import { ref, watch } from "vue"; +import { shallowRef, watch } from "vue"; import { useSession } from "@vnuge/vnlib.browser"; -import { get, set, useToggle, watchOnce } from "@vueuse/core"; -import { find, isArray } from "lodash"; +import { set, useToggle, watchOnce } from "@vueuse/core"; +import { defer, isArray } from "lodash"; export interface IdentityApi extends FeatureApi, Watchable { createIdentity: (identity: NostrPubKey) => Promise<NostrPubKey> @@ -45,56 +45,57 @@ export const useIdentityApi = (): IFeatureExport<AppSettings, IdentityApi> => { const { loggedIn } = useSession(); //Get the current selected key - const selectedKey = ref<NostrPubKey | undefined>(); + const selectedKey = shallowRef<NostrPubKey | undefined>(); + const allKeys = shallowRef<NostrPubKey[]>([]); const [ onTriggered , triggerChange ] = useToggle() + const keyLoadWatchLoop = async () => { + while(true){ + //Load keys from server if logged in + if(loggedIn.value){ + const [...keys] = await execRequest(Endpoints.GetKeys); + allKeys.value = isArray(keys) ? keys : []; + } + else{ + //Clear all keys when logged out + allKeys.value = []; + } + + //Wait for changes to trigger a new key-load + await new Promise((resolve) => watchOnce([onTriggered, loggedIn] as any, () => resolve(null))) + } + } + + defer(keyLoadWatchLoop) + //Clear the selected key if the user logs out watch(loggedIn, (li) => li ? null : selectedKey.value = undefined) return { //Identity is only available in options context createIdentity: optionsOnly(async (id: NostrPubKey) => { - await execRequest<NostrPubKey>(Endpoints.CreateId, id) + await execRequest(Endpoints.CreateId, id) triggerChange() }), updateIdentity: optionsOnly(async (id: NostrPubKey) => { - await execRequest<NostrPubKey>(Endpoints.UpdateId, id) + await execRequest(Endpoints.UpdateId, id) triggerChange() }), deleteIdentity: optionsOnly(async (key: NostrPubKey) => { - await execRequest<NostrPubKey>(Endpoints.DeleteKey, key); + await execRequest(Endpoints.DeleteKey, key); triggerChange() }), selectKey: popupAndOptionsOnly((key: NostrPubKey): Promise<void> => { - selectedKey.value = key; + set(selectedKey, key); return Promise.resolve() }), - getAllKeys: async (): Promise<NostrPubKey[]> => { - if(!get(loggedIn)){ - return [] - } - //Get the keys from the server - const data = await execRequest<NostrPubKey[]>(Endpoints.GetKeys); - - //Response must be an array of key objects - if (!isArray(data)) { - return []; - } - - //Make sure the selected keyid is in the list, otherwise unselect the key - if (data?.length > 0) { - if (!find(data, k => k.Id === selectedKey.value?.Id)) { - set(selectedKey, undefined); - } - } - - return [...data] + getAllKeys: (): Promise<NostrPubKey[]> => { + return Promise.resolve(allKeys.value); }, getPublicKey: (): Promise<NostrPubKey | undefined> => { return Promise.resolve(selectedKey.value); }, waitForChange: () => { - console.log('Waiting for change') return new Promise((resolve) => watchOnce([selectedKey, loggedIn, onTriggered] as any, () => resolve())) } } diff --git a/extension/src/features/nip07allow-api.ts b/extension/src/features/nip07allow-api.ts index eff4ff8..0612b66 100644 --- a/extension/src/features/nip07allow-api.ts +++ b/extension/src/features/nip07allow-api.ts @@ -19,7 +19,7 @@ import { defaultTo, filter, includes, isEqual } from "lodash"; import { BgRuntime, FeatureApi, IFeatureExport, exportForegroundApi, popupAndOptionsOnly } from "./framework"; import { AppSettings } from "./settings"; import { set, get, watchOnce, useToggle } from "@vueuse/core"; -import { computed, ref } from "vue"; +import { computed, shallowRef } from "vue"; interface AllowedSites{ origins: string[]; @@ -46,13 +46,13 @@ export const useInjectAllowList = (): IFeatureExport<AppSettings, InjectAllowlis const store = useSingleSlotStorage<AllowedSites>(storage.local, 'nip07-allowlist', { origins: [], enabled: true }); //watch current tab - const allowedOrigins = ref<string[]>([]) - const protectionEnabled = ref<boolean>(true) + const allowedOrigins = shallowRef<string[]>([]) + const protectionEnabled = shallowRef<boolean>(true) const [manullyTriggered, trigger] = useToggle() const { currentOrigin, currentTab } = (() => { - const currentTab = ref<Tabs.Tab | undefined>(undefined) + const currentTab = shallowRef<Tabs.Tab | undefined>(undefined) const currentOrigin = computed(() => currentTab.value?.url ? new URL(currentTab.value.url).origin : undefined) //Watch for changes to the current tab @@ -73,7 +73,7 @@ export const useInjectAllowList = (): IFeatureExport<AppSettings, InjectAllowlis })() const writeChanges = async () => { - await store.set({ origins: [...get(allowedOrigins)], enabled: get(protectionEnabled) }) + await store.set({ origins: get(allowedOrigins), enabled: get(protectionEnabled) }) } //Initial load @@ -158,15 +158,15 @@ export const useInjectAllowList = (): IFeatureExport<AppSettings, InjectAllowlis }), async getStatus(): Promise<AllowedOriginStatus> { return{ - allowedOrigins: [...get(allowedOrigins)], + allowedOrigins: get(allowedOrigins), enabled: get(protectionEnabled), currentOrigin: get(currentOrigin), isAllowed: isOriginAllowed() } }, - waitForChange: () => { + async waitForChange() { //Wait for the trigger to change - return new Promise((resolve) => watchOnce([currentTab, protectionEnabled, manullyTriggered] as any, () => resolve())); + await new Promise((resolve) => watchOnce([currentTab, protectionEnabled, manullyTriggered] as any, () => resolve(null))); }, } }, diff --git a/extension/src/features/nostr-api.ts b/extension/src/features/nostr-api.ts index 307522d..743f8f1 100644 --- a/extension/src/features/nostr-api.ts +++ b/extension/src/features/nostr-api.ts @@ -14,10 +14,11 @@ // along with this program. If not, see <https://www.gnu.org/licenses/>. import { Endpoints, useServerApi } from "./server-api"; -import { NostrRelay, EventMessage, NostrEvent } from './types' -import { FeatureApi, BgRuntime, IFeatureExport, optionsOnly, exportForegroundApi } from "./framework"; -import { AppSettings } from "./settings"; +import { type FeatureApi, type BgRuntime, type IFeatureExport, optionsOnly, exportForegroundApi } from "./framework"; +import { type AppSettings } from "./settings"; import { useTagFilter } from "./tagfilter-api"; +import type { NostrRelay, EncryptionRequest, NostrEvent } from './types'; +import { cloneDeep } from "lodash"; /** @@ -28,8 +29,8 @@ export interface NostrApi extends FeatureApi { getRelays: () => Promise<NostrRelay[]>; signEvent: (event: NostrEvent) => Promise<NostrEvent | undefined>; setRelay: (relay: NostrRelay) => Promise<NostrRelay | undefined>; - nip04Encrypt: (data: EventMessage) => Promise<string>; - nip04Decrypt: (data: EventMessage) => Promise<string>; + nip04Encrypt: (data: EncryptionRequest) => Promise<string>; + nip04Decrypt: (data: EncryptionRequest) => Promise<string>; } export const useNostrApi = (): IFeatureExport<AppSettings, NostrApi> => { @@ -43,28 +44,41 @@ export const useNostrApi = (): IFeatureExport<AppSettings, NostrApi> => { return { getRelays: async (): Promise<NostrRelay[]> => { //Get preferred relays for the current user - const data = await execRequest<NostrRelay[]>(Endpoints.GetRelays) - return [...data] + const [...relays] = await execRequest(Endpoints.GetRelays) + return relays; }, signEvent: async (req: NostrEvent): Promise<NostrEvent | undefined> => { + //Store copy to prevent mutation + req = cloneDeep(req) + //If tag filter is enabled, filter before continuing if(state.currentConfig.value.tagFilter){ await filterTags(req) } //Sign the event - const event = await execRequest<NostrEvent>(Endpoints.SignEvent, req); + const event = await execRequest(Endpoints.SignEvent, req); return event; }, - nip04Encrypt: async (data: EventMessage): Promise<string> => { - return execRequest<string>(Endpoints.Encrypt, data); + nip04Encrypt: async (data: EncryptionRequest): Promise<string> => { + const message: EncryptionRequest = { + content: data.content, + KeyId: data.KeyId, + pubkey: data.pubkey + } + return execRequest(Endpoints.Encrypt, message); }, - nip04Decrypt: (data: EventMessage): Promise<string> => { - return execRequest<string>(Endpoints.Decrypt, data); + nip04Decrypt: (data: EncryptionRequest): Promise<string> => { + const message: EncryptionRequest = { + content: data.content, + KeyId: data.KeyId, + pubkey: data.pubkey + } + return execRequest(Endpoints.Decrypt, message); }, setRelay: optionsOnly((relay: NostrRelay): Promise<NostrRelay | undefined> => { - return execRequest<NostrRelay>(Endpoints.SetRelay, relay) + return execRequest(Endpoints.SetRelay, relay) }), } }, diff --git a/extension/src/features/server-api/index.ts b/extension/src/features/server-api/index.ts index 6aa34da..6637aaa 100644 --- a/extension/src/features/server-api/index.ts +++ b/extension/src/features/server-api/index.ts @@ -18,10 +18,9 @@ import { computed } from "vue" import { get } from '@vueuse/core' import { type WebMessage, type UserProfile } from "@vnuge/vnlib.browser" import { initEndponts } from "./endpoints" -import { type NostrIdentiy } from "../foreground/types" import { cloneDeep } from "lodash" import { type AppSettings } from "../settings" -import type { NostrEvent, NostrRelay } from "../types" +import type { EncryptionRequest, NostrEvent, NostrPubKey, NostrRelay } from "../types" export enum Endpoints { GetKeys = 'getKeys', @@ -36,7 +35,20 @@ export enum Endpoints { UpdateProfile = 'updateProfile', } -export const useServerApi = (settings: AppSettings) => { +export interface ExecRequestHandler{ + (id: Endpoints.GetKeys):Promise<NostrPubKey[]> + (id: Endpoints.DeleteKey, key: NostrPubKey):Promise<void> + (id: Endpoints.SignEvent, event: NostrEvent):Promise<NostrEvent> + (id: Endpoints.GetRelays):Promise<NostrRelay[]> + (id: Endpoints.SetRelay, relay: NostrRelay):Promise<NostrRelay> + (id: Endpoints.Encrypt, data: EncryptionRequest):Promise<string> + (id: Endpoints.Decrypt, data: EncryptionRequest):Promise<string> + (id: Endpoints.CreateId, identity: NostrPubKey):Promise<NostrPubKey> + (id: Endpoints.UpdateId, identity: NostrPubKey):Promise<NostrPubKey> + (id: Endpoints.UpdateProfile, profile: UserProfile):Promise<string> +} + +export const useServerApi = (settings: AppSettings): { execRequest: ExecRequestHandler } => { const { registerEndpoint, execRequest } = initEndponts() //ref to nostr endpoint url @@ -54,7 +66,7 @@ export const useServerApi = (settings: AppSettings) => { registerEndpoint({ id: Endpoints.DeleteKey, method: 'DELETE', - path: (key:NostrIdentiy) => `${get(nostrUrl)}?type=identity&key_id=${key.Id}`, + path: (key: NostrPubKey) => `${get(nostrUrl)}?type=identity&key_id=${key.Id}`, onRequest: () => Promise.resolve(), onResponse: async (response: WebMessage) => response.getResultOrThrow() }) @@ -91,7 +103,7 @@ export const useServerApi = (settings: AppSettings) => { id: Endpoints.CreateId, method: 'PUT', path: () => `${get(nostrUrl)}?type=identity`, - onRequest: (identity:NostrIdentiy) => Promise.resolve(identity), + onRequest: (identity: NostrPubKey) => Promise.resolve(identity), onResponse: async (response: WebMessage<NostrEvent>) => response.getResultOrThrow() }) @@ -99,7 +111,7 @@ export const useServerApi = (settings: AppSettings) => { id: Endpoints.UpdateId, method: 'PATCH', path: () => `${get(nostrUrl)}?type=identity`, - onRequest: (identity:NostrIdentiy) => { + onRequest: (identity:NostrPubKey) => { const id = cloneDeep(identity) as any; delete id.Created; delete id.LastModified; @@ -121,7 +133,7 @@ export const useServerApi = (settings: AppSettings) => { id:Endpoints.Encrypt, method:'POST', path: () => `${get(nostrUrl)}?type=encrypt`, - onRequest: (data: NostrEvent) => Promise.resolve(data), + onRequest: (data: EncryptionRequest) => Promise.resolve(data), onResponse: async (response: WebMessage<string>) => response.getResultOrThrow() }) @@ -129,7 +141,7 @@ export const useServerApi = (settings: AppSettings) => { id:Endpoints.Decrypt, method:'POST', path: () => `${get(nostrUrl)}?type=decrypt`, - onRequest: (data: NostrEvent) => Promise.resolve(data), + onRequest: (data: EncryptionRequest) => Promise.resolve(data), onResponse: async (response: WebMessage<string>) => response.getResultOrThrow() }) diff --git a/extension/src/features/settings.ts b/extension/src/features/settings.ts index a67957c..059c2d2 100644 --- a/extension/src/features/settings.ts +++ b/extension/src/features/settings.ts @@ -19,7 +19,7 @@ import { configureApi, debugLog } from '@vnuge/vnlib.browser' import { readonly, ref, Ref } from "vue"; import { JsonObject } from "type-fest"; import { Watchable, useSingleSlotStorage } from "./types"; -import { BgRuntime, FeatureApi, optionsOnly, IFeatureExport, exportForegroundApi } from './framework' +import { BgRuntime, FeatureApi, optionsOnly, IFeatureExport, exportForegroundApi, popupAndOptionsOnly } from './framework' import { get, watchOnce } from "@vueuse/core"; export interface PluginConfig extends JsonObject { @@ -143,7 +143,7 @@ export const useSettingsApi = () : IFeatureExport<AppSettings, SettingsApi> =>{ return state.currentConfig.value }), - setDarkMode: optionsOnly(async (darkMode: boolean) => { + setDarkMode: popupAndOptionsOnly(async (darkMode: boolean) => { console.log('Setting dark mode to', darkMode, 'from', _darkMode.value) _darkMode.value = darkMode }), diff --git a/extension/src/features/types.ts b/extension/src/features/types.ts index bd0afee..a64be93 100644 --- a/extension/src/features/types.ts +++ b/extension/src/features/types.ts @@ -35,8 +35,18 @@ export interface TaggedNostrEvent extends NostrEvent { tags?: any[][] } -export interface EventMessage extends JsonObject { - readonly event: NostrEvent +export interface EncryptionRequest extends JsonObject { + readonly KeyId: string + /** + * The plaintext to encrypt or ciphertext + * to decrypt + */ + readonly content: string + /** + * The other peer's public key used + * for encryption + */ + readonly pubkey: string } export interface NostrRelay extends JsonObject { |