aboutsummaryrefslogtreecommitdiff
path: root/extension/src/features
diff options
context:
space:
mode:
authorLibravatar vnugent <public@vaughnnugent.com>2024-01-07 20:39:18 -0500
committerLibravatar vnugent <public@vaughnnugent.com>2024-01-07 20:39:18 -0500
commitc438ee90e3be4e5e01ae3d045d6b841a03bd46eb (patch)
tree41c0b2ee815ce979e2af0e79f8fde9b58f5f4627 /extension/src/features
parente87c4b69036e32b4fcf3df89e8158fb52df6a4e0 (diff)
losts of package updates & permissionsHEADmaster
Diffstat (limited to 'extension/src/features')
-rw-r--r--extension/src/features/framework/index.ts2
-rw-r--r--extension/src/features/history.ts2
-rw-r--r--extension/src/features/identity-api.ts2
-rw-r--r--extension/src/features/index.ts7
-rw-r--r--extension/src/features/mfa-api.ts2
-rw-r--r--extension/src/features/nip07allow-api.ts2
-rw-r--r--extension/src/features/nostr-api.ts2
-rw-r--r--extension/src/features/permissions.ts371
-rw-r--r--extension/src/features/pki-api.ts2
-rw-r--r--extension/src/features/server-api/endpoints.ts2
-rw-r--r--extension/src/features/server-api/index.ts2
-rw-r--r--extension/src/features/settings.ts2
-rw-r--r--extension/src/features/tagfilter-api.ts2
-rw-r--r--extension/src/features/types.ts2
-rw-r--r--extension/src/features/util.ts10
15 files changed, 352 insertions, 60 deletions
diff --git a/extension/src/features/framework/index.ts b/extension/src/features/framework/index.ts
index 2d9cad5..b545335 100644
--- a/extension/src/features/framework/index.ts
+++ b/extension/src/features/framework/index.ts
@@ -1,5 +1,5 @@
-// Copyright (C) 2023 Vaughn Nugent
+// Copyright (C) 2024 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
diff --git a/extension/src/features/history.ts b/extension/src/features/history.ts
index ff7c267..82f31c4 100644
--- a/extension/src/features/history.ts
+++ b/extension/src/features/history.ts
@@ -1,4 +1,4 @@
-// Copyright (C) 2023 Vaughn Nugent
+// Copyright (C) 2024 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
diff --git a/extension/src/features/identity-api.ts b/extension/src/features/identity-api.ts
index d909162..0b8973d 100644
--- a/extension/src/features/identity-api.ts
+++ b/extension/src/features/identity-api.ts
@@ -1,4 +1,4 @@
-// Copyright (C) 2023 Vaughn Nugent
+// Copyright (C) 2024 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
diff --git a/extension/src/features/index.ts b/extension/src/features/index.ts
index f05de9b..0a8e182 100644
--- a/extension/src/features/index.ts
+++ b/extension/src/features/index.ts
@@ -1,4 +1,4 @@
-// Copyright (C) 2023 Vaughn Nugent
+// Copyright (C) 2024 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
@@ -21,6 +21,8 @@ export type { PkiPubKey, EcKeyParams, LocalPkiApi as PkiApi } from './pki-api'
export type { NostrApi } from './nostr-api'
export type { UserApi } from './auth-api'
export type { IdentityApi } from './identity-api'
+export type { MfaUpdateResult } from './mfa-api'
+export type { PermissionRequest, PrType, AutoAllowRule } from './permissions'
export { useBackgroundFeatures, useForegoundFeatures } from './framework'
export { useLocalPki, usePkiApi } from './pki-api'
@@ -32,4 +34,5 @@ export { useHistoryApi } from './history'
export { useEventTagFilterApi } from './tagfilter-api'
export { useInjectAllowList } from './nip07allow-api'
export { onWatchableChange } from './util'
-export { useMfaConfigApi, type MfaUpdateResult } from './mfa-api' \ No newline at end of file
+export { useMfaConfigApi } from './mfa-api'
+export { usePermissionApi, PrStatus } from './permissions' \ No newline at end of file
diff --git a/extension/src/features/mfa-api.ts b/extension/src/features/mfa-api.ts
index fc6d51a..85ff49e 100644
--- a/extension/src/features/mfa-api.ts
+++ b/extension/src/features/mfa-api.ts
@@ -1,4 +1,4 @@
-// Copyright (C) 2023 Vaughn Nugent
+// Copyright (C) 2024 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
diff --git a/extension/src/features/nip07allow-api.ts b/extension/src/features/nip07allow-api.ts
index 8c08d5e..4787437 100644
--- a/extension/src/features/nip07allow-api.ts
+++ b/extension/src/features/nip07allow-api.ts
@@ -1,4 +1,4 @@
-// Copyright (C) 2023 Vaughn Nugent
+// Copyright (C) 2024 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
diff --git a/extension/src/features/nostr-api.ts b/extension/src/features/nostr-api.ts
index 046ccea..4aee660 100644
--- a/extension/src/features/nostr-api.ts
+++ b/extension/src/features/nostr-api.ts
@@ -1,4 +1,4 @@
-// Copyright (C) 2023 Vaughn Nugent
+// Copyright (C) 2024 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
diff --git a/extension/src/features/permissions.ts b/extension/src/features/permissions.ts
index c06257b..5473962 100644
--- a/extension/src/features/permissions.ts
+++ b/extension/src/features/permissions.ts
@@ -1,4 +1,4 @@
-// Copyright (C) 2023 Vaughn Nugent
+// Copyright (C) 2024 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
@@ -13,68 +13,353 @@
// 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 { useStorageAsync } from "@vueuse/core";
-import { find, isEmpty, merge, remove } from "lodash";
-import { storage } from "webextension-polyfill";
-import { useAuthApi } from "./auth-api";
-import { useSettingsApi } from "./settings";
+import { Mutable, get, set, toRefs } from "@vueuse/core";
+import { Ref } from "vue";
+import { defaultTo, defaults, defer, filter, find, forEach, isEqual, isNil } from "lodash";
+import { nanoid } from "nanoid";
+import { useSession } from "@vnuge/vnlib.browser";
+import { type FeatureApi, type BgRuntime, type IFeatureExport, exportForegroundApi, optionsOnly } from "./framework";
+import { waitForChangeFn, waitOne } from "./util";
+import { windows, runtime, Windows, tabs } from "webextension-polyfill";
+import type { TotpUpdateMessage, Watchable } from "./types";
+import type { AppSettings } from "./settings";
-const permissions = useStorageAsync("permissions", [], storage.local);
+export interface AutoAllowRule{
+ origin: string
+ type: string
+ readonly timestamp: number
+}
-export const setAutoAllow = async (origin, mKind, keyId) => {
- permissions.value.push({ origin, mKind, keyId, })
+export type PrType = string
+export enum PrStatus{
+ Pending,
+ Approved,
+ Denied
}
-/**
- * Determines if the user has previously allowed the origin to use the key to sign events
- * of the desired kind
- * @param {*} origin The site origin requesting the permission
- * @param {*} mKind The kind of message being signed
- * @param {*} keyId The keyId of the key being used to sign the message
- */
-export const isAutoAllow = async (origin, mKind, keyId) => {
- return find(permissions.value, p => p.origin === origin && p.mKind === mKind && p.keyId === keyId) !== undefined
+export interface PermissionRequest{
+ readonly uuid: string
+ readonly origin: string
+ readonly requestType: PrType
+ readonly timestamp: number
+ readonly status: PrStatus
}
-/**
- * Removes the auto allow permission from the list
- * @param {*} origin The site origin requesting the permission
- * @param {*} mKind The message kind being signed
- * @param {*} keyId The keyId of the key being used to sign the message
- */
-export const removeAutoAllow = async (origin, mKind, keyId) => {
- //Remove the permission from the list
- remove(permissions.value, p => p.origin === origin && p.mKind === mKind && p.keyId === keyId);
+export type MfaUpdateResult = TotpUpdateMessage
+
+export interface PermissionApi extends FeatureApi, Watchable {
+ getRequests(): Promise<PermissionRequest[]>
+ allow(requestId: string, addRule: boolean): Promise<void>
+ deny(requestId: string): Promise<void>
+ clearRequests(): Promise<void>
+ requestAndWaitResult(request: Partial<PermissionRequest>): Promise<PrStatus>
+
+ getRules(): Promise<AutoAllowRule[]>
+ deleteRule(rule: AutoAllowRule): Promise<void>
+ addRule(rule: AutoAllowRule): Promise<void>
}
+interface PermissionSlot {
+ requests: PermissionRequest[]
+}
-export const useSitePermissions = (() => {
+interface RuleSlot{
+ rules: AutoAllowRule[]
+}
- const { apiCall, handleProtectedMessage } = useAuthApi();
- const { currentConfig } = useSettingsApi();
+const useRuleSet = (slot: Ref<RuleSlot>) => {
+ defaults(slot.value, { rules: [] })
+ const { rules } = toRefs(slot)
+
+ return{
+ isAllowed: (request: PermissionRequest): boolean => {
+ //find existing rule
+ const rule = find(get(rules), r => isEqual(r.origin, request.origin) && isEqual(r.type, request.requestType))
+ return !isNil(rule)
+ },
+ addRule: (rule: Partial<AutoAllowRule>) => {
+ const current = defaultTo(get(rules), [])
+
+ //see if rule aready exists
+ if (find(current, r => isEqual(r.origin, rule.origin) && isEqual(r.type, rule.type))) {
+ return;
+ }
+
+ //add rule to head of store
+ current.unshift({
+ ...rule,
+ timestamp: Date.now()
+ } as AutoAllowRule)
+
+ set(rules, current)
+ },
+ deleteRule (rule: AutoAllowRule) {
+ //Filter all non matching rules
+ const wo = filter(get(rules), r => !(isEqual(r.origin, rule.origin) && isEqual(r.type, rule.type)))
+ set(rules, wo)
+ },
+ getRules:(): AutoAllowRule[] =>get(rules)
+ }
+}
+
+const usePermissions = (slot: Ref<PermissionSlot>, rules: ReturnType<typeof useRuleSet>) => {
+
+ const permPopupUrl = runtime.getURL("src/entries/contentScript/auth-popup.html")
+
+ defaults(slot.value, { rules: [] })
+ const { requests } = toRefs(slot)
+
+ const drawWindow = async ({ uuid }: Partial<PermissionRequest>): Promise<Windows.CreateCreateDataType> => {
+ const current = await windows.getCurrent()
+
+ const minWidth = 350
+ const minHeight = 180
+
+ const maxWidth = 500
+ const maxHeight = 250
- const getCurrentPerms = async () => {
- const { permissions } = await storage.local.get('permissions');
+ const width = Math.min(Math.max(current.width! - 100, minWidth), maxWidth)
+ const height = Math.min(Math.max(current.height! - 100, minHeight), maxHeight)
- //Store a default config if none exists
- if (isEmpty(permissions)) {
- await storage.local.set({ siteConfig: defaultConfig });
+ //draw half way across screen minus half its width
+ const left = current.left! + (current.width! / 2) - (width / 2)
+
+ return {
+ url: `${permPopupUrl}?uuid=${uuid}&closeable`,
+ type: "popup",
+ height: height,
+ width: width,
+ focused: true,
+ allowScriptsToClose: true,
+ top: 100,
+ //try to center popup
+ left: left,
}
+ }
+
+ const activePopups = new Map<number, PermissionRequest>()
- //Merge the default config with the site config
- return merge(defaultConfig, siteConfig)
+ const getRequest = (requestId: string): PermissionRequest | undefined => {
+ return find(get(requests), r => r.uuid === requestId)
}
- const onIsSiteEnabled = handleProtectedMessage(async (data) => {
+ const updateRequest = (request: PermissionRequest, addRule: boolean) => {
+ const current = get(requests)
- })
+ const index = current.findIndex(r => r.uuid === request.uuid)
+ if (index === -1) {
+ throw new Error("Request not found")
+ }
+
+ //Set request state
+ current[index] = request
+
+ //Update storage
+ set(requests, current)
- return () => {
+ //Add rule if needed
+ if (addRule) {
+ rules.addRule({ origin: request.origin, type: request.requestType })
+ }
+ }
+
+ const initNewRequest = (request: Partial<PermissionRequest>): PermissionRequest => {
return {
- onCreateIdentity,
- onUpdateIdentity
+ ...request,
+ uuid: nanoid(),
+ status: PrStatus.Pending,
+ timestamp: Date.now()
+ } as PermissionRequest
+ }
+
+ //Listen for popup close to cleanup request
+ windows.onRemoved.addListener(async (id) => {
+ const req = activePopups.get(id)
+ if (req && req.status === PrStatus.Pending) {
+ //set denied
+ (req as Mutable<PermissionRequest>).status = PrStatus.Denied
+ //popup closed, set to denied
+ updateRequest(req, false)
}
+ })
+
+ //Watch for changes to the current tab
+ tabs.onRemoved.addListener(async (tabId) => {
+ const tab = await tabs.get(tabId)
+ const { origin } = new URL(tab.url!)
+
+ //Find ally pending requests for the origin
+ const pending = filter(requests.value, r => r.status == PrStatus.Pending && r.origin == origin)
+
+ //update all pending requests to denied
+ forEach(pending, r => updateRequest(r, false))
+ })
+
+ return{
+ getRequest,
+
+ async showPermsWindow (request: PermissionRequest): Promise<void> {
+ const windowsArgs = await drawWindow(request)
+ const { id } = await windows.create(windowsArgs)
+ activePopups.set(id!, request)
+ },
+
+ pushRequest (request: Partial<PermissionRequest>, showPopup: boolean): PermissionRequest {
+ //Create new request
+ const req = initNewRequest(request)
+
+ //See if allowed
+ if(rules.isAllowed(req)){
+ //Set to approved
+ (req as Mutable<PermissionRequest>).status = PrStatus.Approved
+ //No need to show popup
+ showPopup = false
+ }
+
+ const current = get(requests)
+ current.unshift(req)
+ set(requests, current)
+
+ //Show popup if needed
+ if (showPopup) {
+ this.showPermsWindow(req)
+ }
+
+ return req
+ },
+
+ allow (requestId: string, addRule: boolean): void {
+ const request = getRequest(requestId)
+ if(!request){
+ throw new Error("Request not found")
+ }
+ //set approved
+ (request as Mutable<PermissionRequest>).status = PrStatus.Approved
+ //update request
+ updateRequest(request, addRule)
+ },
+
+ deny(requestId: string): void {
+ const request = getRequest(requestId)
+ if (!request) {
+ throw new Error("Request not found")
+ }
+ //set denied
+ (request as Mutable<PermissionRequest>).status = PrStatus.Denied
+ //update request
+ updateRequest(request, false)
+ },
+
+ clearAll: () => {
+ //notify pending requests
+ forEach(filter(requests.value, r => r.status == PrStatus.Pending), r => {
+ //set denied
+ (r as Mutable<PermissionRequest>).status = PrStatus.Denied
+ //update request
+ updateRequest(r, false)
+ })
+
+ //Then defer clear
+ defer(() => set(requests, []))
+ },
+ getAll: () => get(requests)
}
+}
+
+export const usePermissionApi = (): IFeatureExport<AppSettings, PermissionApi> => {
+
+ return {
+ background: ({ state }: BgRuntime<AppSettings>): PermissionApi => {
+ const { loggedIn } = useSession();
+ const { currentConfig } = state
+
+ //Open storage slot for permissions
+ const reqStore = state.useStorageSlot<PermissionSlot>("permissions", { requests: [] })
+ const ruleStore = state.useStorageSlot<RuleSlot>("rules", { rules: [] })
+
+ //init rules api
+ const ruleSet = useRuleSet(ruleStore)
+ const permissions = usePermissions(reqStore, ruleSet)
-})() \ No newline at end of file
+ return {
+ waitForChange: waitForChangeFn([currentConfig, loggedIn, reqStore, ruleStore]),
+
+ getRequests: () => Promise.resolve(permissions.getAll()),
+
+ deny(requestId: string) {
+ permissions.deny(requestId)
+ return Promise.resolve()
+ },
+
+ allow(requestId: string, addRule: boolean) {
+ permissions.allow(requestId, addRule)
+ return Promise.resolve()
+ },
+
+ clearRequests: optionsOnly(() => {
+ //clear stored requests
+ permissions.clearAll()
+ return Promise.resolve()
+ }),
+
+ async requestAndWaitResult(request: Partial<PermissionRequest>) {
+ //push request
+ const req = permissions.pushRequest(request, true)
+
+ //See if pending
+ if(req.status !== PrStatus.Pending){
+ //completed already, return status
+ return req.status
+ }
+
+ do {
+
+ //wait for a change
+ await waitOne([reqStore])
+
+ //check if request was approved
+ const status = permissions.getRequest(req.uuid);
+
+ switch(status?.status){
+ case PrStatus.Approved:
+ return PrStatus.Approved;
+ case PrStatus.Denied:
+ return PrStatus.Denied;
+ case PrStatus.Pending:
+ //continue waiting
+ break;
+ default:
+ throw new Error("Request was rejected or deleted")
+ }
+
+ //continue to wait for pending status
+ } while(true)
+ },
+
+ getRules: () => Promise.resolve(ruleSet.getRules()),
+
+ deleteRule: optionsOnly((rule: AutoAllowRule) => {
+ ruleSet.deleteRule(rule)
+ return Promise.resolve()
+ }),
+
+ addRule: optionsOnly((rule: AutoAllowRule) => {
+ ruleSet.addRule(rule)
+ return Promise.resolve()
+ }),
+ }
+ },
+ foreground: exportForegroundApi<PermissionApi>([
+ 'waitForChange',
+ 'getRequests',
+ 'clearRequests',
+ 'requestAndWaitResult',
+ 'getRules',
+ 'deleteRule',
+ 'addRule',
+ 'allow',
+ 'deny'
+ ]),
+ }
+} \ No newline at end of file
diff --git a/extension/src/features/pki-api.ts b/extension/src/features/pki-api.ts
index 41fbd48..07fb3df 100644
--- a/extension/src/features/pki-api.ts
+++ b/extension/src/features/pki-api.ts
@@ -1,4 +1,4 @@
-// Copyright (C) 2023 Vaughn Nugent
+// Copyright (C) 2024 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
diff --git a/extension/src/features/server-api/endpoints.ts b/extension/src/features/server-api/endpoints.ts
index 9c73866..5fa1bf4 100644
--- a/extension/src/features/server-api/endpoints.ts
+++ b/extension/src/features/server-api/endpoints.ts
@@ -1,4 +1,4 @@
-// Copyright (C) 2023 Vaughn Nugent
+// Copyright (C) 2024 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
diff --git a/extension/src/features/server-api/index.ts b/extension/src/features/server-api/index.ts
index cd67242..b9524ed 100644
--- a/extension/src/features/server-api/index.ts
+++ b/extension/src/features/server-api/index.ts
@@ -1,4 +1,4 @@
-// Copyright (C) 2023 Vaughn Nugent
+// Copyright (C) 2024 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
diff --git a/extension/src/features/settings.ts b/extension/src/features/settings.ts
index 9a3c32d..ca714a5 100644
--- a/extension/src/features/settings.ts
+++ b/extension/src/features/settings.ts
@@ -1,4 +1,4 @@
-// Copyright (C) 2023 Vaughn Nugent
+// Copyright (C) 2024 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
diff --git a/extension/src/features/tagfilter-api.ts b/extension/src/features/tagfilter-api.ts
index f5f1b6c..22369d0 100644
--- a/extension/src/features/tagfilter-api.ts
+++ b/extension/src/features/tagfilter-api.ts
@@ -1,4 +1,4 @@
-// Copyright (C) 2023 Vaughn Nugent
+// Copyright (C) 2024 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
diff --git a/extension/src/features/types.ts b/extension/src/features/types.ts
index 92cf6cf..fe59011 100644
--- a/extension/src/features/types.ts
+++ b/extension/src/features/types.ts
@@ -1,4 +1,4 @@
-// Copyright (C) 2023 Vaughn Nugent
+// Copyright (C) 2024 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
diff --git a/extension/src/features/util.ts b/extension/src/features/util.ts
index e9147bc..6ec8f15 100644
--- a/extension/src/features/util.ts
+++ b/extension/src/features/util.ts
@@ -1,4 +1,4 @@
-// Copyright (C) 2023 Vaughn Nugent
+// Copyright (C) 2024 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
@@ -20,15 +20,19 @@ import { type MaybeRefOrGetter, type WatchSource, isProxy, toRaw } from "vue";
import type { Watchable } from "./types";
export const waitForChange = <T extends Readonly<WatchSource<unknown>[]>>(source: [...T]):Promise<void> => {
- return new Promise((resolve) => watchOnce(source, () => resolve()))
+ return new Promise((resolve) => watchOnce<any>(source, () => resolve(), { deep: true }))
}
export const waitForChangeFn = <T extends Readonly<WatchSource<unknown>[]>>(source: [...T]) => {
return (): Promise<void> => {
- return new Promise((resolve) => watchOnce(source, () => resolve()))
+ return new Promise((resolve) => watchOnce<any>(source, () => resolve(), {deep: true}))
}
}
+export const waitOne = <T extends Readonly<WatchSource<unknown>[]>>(source: [...T]): Promise<void> => {
+ return new Promise((resolve) => watchOnce<any>(source, () => resolve(), { deep: true }))
+}
+
export const useStorage = <T>(storage: any & chrome.storage.StorageArea, key: string, initialValue: MaybeRefOrGetter<T>): RemovableRef<T> => {
const wrapper: StorageLikeAsync = {