diff --git a/source/background/services/autoLogin.ts b/source/background/services/autoLogin.ts index a21a8b1f..ee31edbb 100644 --- a/source/background/services/autoLogin.ts +++ b/source/background/services/autoLogin.ts @@ -14,7 +14,7 @@ export function getAutoLoginForTab(tabID: number): SearchResult | null { const register = getRegister(); const key = `tab-${tabID}`; if (register.has(key)) { - const item = register.get(key).entry; + const item = (register.get(key) as RegisteredItem).entry; register.delete(key); return item; } diff --git a/source/background/services/config.ts b/source/background/services/config.ts index aff91eda..84abd9eb 100644 --- a/source/background/services/config.ts +++ b/source/background/services/config.ts @@ -1,5 +1,6 @@ import { getSyncValue, setSyncValue } from "./storage.js"; import { Configuration, InputButtonType, SyncStorageItem } from "../types.js"; +import { naiveClone } from "../../shared/library/clone.js"; const DEFAULTS: Configuration = { entryIcons: true, @@ -9,7 +10,7 @@ const DEFAULTS: Configuration = { useSystemTheme: true }; -let __lastConfig: Configuration = null; +let __lastConfig: Configuration | null = null; export function getConfig(): Configuration { if (!__lastConfig) { @@ -24,7 +25,7 @@ export async function initialise() { export async function updateConfigValue(key: T, value: Configuration[T]): Promise { const configRaw = await getSyncValue(SyncStorageItem.Configuration); - const config = JSON.parse(configRaw); + const config = configRaw ? JSON.parse(configRaw) : naiveClone(DEFAULTS); config[key] = value; __lastConfig = config; await setSyncValue(SyncStorageItem.Configuration, JSON.stringify(config)); diff --git a/source/background/services/desktop/actions.ts b/source/background/services/desktop/actions.ts index 411dd2fc..520e8381 100644 --- a/source/background/services/desktop/actions.ts +++ b/source/background/services/desktop/actions.ts @@ -85,7 +85,7 @@ export async function getVaultsTree(): Promise { ...output, [sourceID]: { ...tree[sourceID], - name: names[sourceID] ?? "Untitled vault" + name: names?.[sourceID] ?? "Untitled vault" } }; }, {}); diff --git a/source/background/services/desktop/request.ts b/source/background/services/desktop/request.ts index d4351d32..3784fa19 100644 --- a/source/background/services/desktop/request.ts +++ b/source/background/services/desktop/request.ts @@ -5,7 +5,7 @@ import { decryptPayload, encryptPayload } from "../crypto.js"; import { getLocalValue } from "../storage.js"; import { LocalStorageItem } from "../../types.js"; -type OutputType = "body" | "status"; +type OutputType = "body" | "status" | undefined; interface DesktopRequestConfig { auth?: string | null; @@ -45,12 +45,13 @@ export async function sendDesktopRequest( url = newURL.toString(); } else { requestConfig.body = JSON.stringify(payload); - Object.assign(requestConfig.headers, { + Object.assign(requestConfig.headers as HeadersInit, { "Content-Type": "application/json" }); } } if (auth !== null) { + requestConfig.headers = requestConfig.headers || {}; // Request requires encryption, perform setup now requestConfig.headers["Authorization"] = auth; if (typeof requestConfig.body === "string") { @@ -59,6 +60,12 @@ export async function sendDesktopRequest( // Encrypt const privateKey = await getLocalValue(LocalStorageItem.APIPrivateKey); const publicKey = await getLocalValue(LocalStorageItem.APIServerPublicKey); + if (!privateKey) { + throw new Error("Authenticated request failed: No private key available"); + } + if (!publicKey) { + throw new Error("Authenticated request failed: No public key available"); + } requestConfig.body = await encryptPayload(requestConfig.body, privateKey, publicKey); } } @@ -81,19 +88,25 @@ export async function sendDesktopRequest( } // Handle encrypted response if (resp.headers.get("X-Bcup-API")) { - const components = resp.headers.get("X-Bcup-API").split(","); + const components = resp.headers.get("X-Bcup-API")?.split(",") ?? []; if (components.includes("enc")) { const content = await resp.text(); const contentType = resp.headers.get("X-Content-Type") || resp.headers.get("Content-Type") || "text/plain"; // Decrypt const privateKey = await getLocalValue(LocalStorageItem.APIPrivateKey); const publicKey = await getLocalValue(LocalStorageItem.APIServerPublicKey); + if (!privateKey) { + throw new Error("Decrypting response failed: No private key available"); + } + if (!publicKey) { + throw new Error("Decrypting response failed: No public key available"); + } const rawDecrypted = await decryptPayload(content, publicKey, privateKey); return /application\/json/.test(contentType) ? JSON.parse(rawDecrypted) : rawDecrypted; } } // Standard, unencrypted response - if (/application\/json/.test(resp.headers.get("Content-Type"))) { + if (/application\/json/.test(resp.headers.get("Content-Type") ?? "")) { return resp.json(); } return resp.text(); diff --git a/source/background/services/entry.ts b/source/background/services/entry.ts index 5ce4c59d..b085d334 100644 --- a/source/background/services/entry.ts +++ b/source/background/services/entry.ts @@ -4,5 +4,8 @@ import { formatURL } from "../../shared/library/url.js"; export async function openEntryPageInNewTab(_: SearchResult, url: string): Promise { const tab = await createNewTab(formatURL(url)); + if (typeof tab.id !== "number") { + throw new Error("No tab ID for created tab"); + } return tab.id; } diff --git a/source/background/services/loginMemory.ts b/source/background/services/loginMemory.ts index 7cff093f..1af867e8 100644 --- a/source/background/services/loginMemory.ts +++ b/source/background/services/loginMemory.ts @@ -17,7 +17,7 @@ export function clearCredentials(id: string): void { } if (memory.has("last")) { const last = memory.get("last"); - if (last.credentials.id === id) { + if (last?.credentials.id === id) { memory.delete("last"); } } @@ -35,7 +35,7 @@ export function getAllCredentials(): Array { export function getCredentialsForID(id: string): UsedCredentials | null { const memory = getLoginMemory(); - return memory.has(id) ? memory.get(id).credentials : null; + return memory.has(id) ? (memory.get(id) as LoginMemoryItem).credentials : null; } export function getLastCredentials(tabID: number): UsedCredentials | null { @@ -55,7 +55,7 @@ function getLoginMemory(): ExpiryMap { export function stopPromptForID(id: string): void { const memory = getLoginMemory(); if (memory.has(id)) { - const existing = memory.get(id); + const existing = memory.get(id) as LoginMemoryItem; memory.set(id, { ...existing, credentials: { diff --git a/source/background/services/messaging.ts b/source/background/services/messaging.ts index 43935676..ad260bfd 100644 --- a/source/background/services/messaging.ts +++ b/source/background/services/messaging.ts @@ -53,8 +53,12 @@ async function handleMessage( ) { switch (msg.type) { case BackgroundMessageType.AuthenticateDesktopConnection: { + const { code } = msg; + if (!code) { + throw new Error("No auth code provided"); + } log("complete desktop authentication"); - const publicKey = await authenticateBrowserAccess(msg.code); + const publicKey = await authenticateBrowserAccess(code); await setLocalValue(LocalStorageItem.APIServerPublicKey, publicKey); sendResponse({}); break; @@ -78,6 +82,9 @@ async function handleMessage( } case BackgroundMessageType.ClearSavedCredentials: { const { credentialsID } = msg; + if (!credentialsID) { + throw new Error("No credentials ID provided"); + } log(`clear saved credentials: ${credentialsID}`); clearCredentials(credentialsID); sendResponse({}); @@ -85,6 +92,9 @@ async function handleMessage( } case BackgroundMessageType.ClearSavedCredentialsPrompt: { const { credentialsID } = msg; + if (!credentialsID) { + throw new Error("No credentials ID provided"); + } log(`clear saved credentials prompt: ${credentialsID}`); stopPromptForID(credentialsID); await sendTabsMessage({ @@ -95,6 +105,9 @@ async function handleMessage( } case BackgroundMessageType.DeleteDisabledDomains: { const { domains } = msg; + if (!domains) { + throw new Error("No domains list provided"); + } log(`remove disabled domains: ${domains.join(", ")}`); for (const domain of domains) { await removeDisabledFlagForDomain(domain); @@ -104,6 +117,9 @@ async function handleMessage( } case BackgroundMessageType.DisableSavePromptForCredentials: { const { credentialsID } = msg; + if (!credentialsID) { + throw new Error("No credentials ID provided"); + } log(`disable save prompt for credentials: ${credentialsID}`); try { const credentials = getCredentialsForID(credentialsID); @@ -208,6 +224,9 @@ async function handleMessage( break; } case BackgroundMessageType.GetSavedCredentialsForID: { + if (!msg.credentialsID) { + throw new Error("No credentials ID provided"); + } const credentials = getCredentialsForID(msg.credentialsID); sendResponse({ credentials: [credentials] @@ -223,6 +242,9 @@ async function handleMessage( } case BackgroundMessageType.MarkNotificationRead: { const { notification } = msg; + if (!notification) { + throw new Error("No notification provided"); + } log(`mark notification read: ${notification}`); await markNotificationRead(notification); sendResponse({}); @@ -230,6 +252,9 @@ async function handleMessage( } case BackgroundMessageType.OpenEntryPage: { const { autoLogin, entry } = msg; + if (!entry) { + throw new Error("No entry provided"); + } const [url = null] = getEntryURLs(entry.properties, EntryURLType.Login); if (!url) { sendResponse({ opened: false }); @@ -253,6 +278,9 @@ async function handleMessage( } case BackgroundMessageType.PromptLockSource: { const { sourceID } = msg; + if (!sourceID) { + throw new Error("No source ID provided"); + } log(`request lock source: ${sourceID}`); const locked = await promptSourceLock(sourceID); sendResponse({ @@ -262,6 +290,9 @@ async function handleMessage( } case BackgroundMessageType.PromptUnlockSource: { const { sourceID } = msg; + if (!sourceID) { + throw new Error("No source ID provided"); + } log(`request unlock source: ${sourceID}`); await promptSourceUnlock(sourceID); sendResponse({}); @@ -276,6 +307,15 @@ async function handleMessage( } case BackgroundMessageType.SaveCredentialsToVault: { const { sourceID, groupID, entryID = null, entryProperties, entryType = EntryType.Website } = msg; + if (!sourceID) { + throw new Error("No source ID provided"); + } + if (!groupID) { + throw new Error("No group ID provided"); + } + if (!entryProperties) { + throw new Error("No entry properties provided"); + } if (entryID) { log(`save credentials to existing entry: ${entryID} (source=${sourceID})`); await saveExistingEntry(sourceID, groupID, entryID, entryProperties); @@ -293,31 +333,52 @@ async function handleMessage( } case BackgroundMessageType.SaveUsedCredentials: { const { credentials } = msg; + if (!credentials) { + throw new Error("No source ID provided"); + } + if (!sender.tab?.id) { + throw new Error("No tab ID available for background message"); + } updateUsedCredentials(credentials, sender.tab.id); sendResponse({}); break; } case BackgroundMessageType.SearchEntriesByTerm: { - const searchResults = await searchEntriesByTerm(msg.searchTerm); + const { searchTerm } = msg; + if (!searchTerm) { + throw new Error("No search term provided"); + } + const searchResults = await searchEntriesByTerm(searchTerm); sendResponse({ searchResults }); break; } case BackgroundMessageType.SearchEntriesByURL: { - const searchResults = await searchEntriesByURL(msg.url); + const { url } = msg; + if (!url) { + throw new Error("No URL provided"); + } + const searchResults = await searchEntriesByURL(url); sendResponse({ searchResults }); break; } case BackgroundMessageType.SetConfigurationValue: { - await updateConfigValue(msg.configKey, msg.configValue); + const { configKey, configValue } = msg; + if (!configKey || typeof configValue === "undefined") { + throw new Error("Invalid configuration proivided provided"); + } + await updateConfigValue(configKey, configValue); sendResponse({}); break; } case BackgroundMessageType.TrackRecentEntry: { const { entry } = msg; + if (!entry) { + throw new Error("No entry provided"); + } if (!entry.sourceID) { throw new Error(`No source ID in entry result: ${entry.id}`); } diff --git a/source/background/services/tabs.ts b/source/background/services/tabs.ts index 27eb6b00..7059304f 100644 --- a/source/background/services/tabs.ts +++ b/source/background/services/tabs.ts @@ -9,10 +9,13 @@ export async function sendTabsMessage(payload: TabEvent, tabIDs: Array | await browser.tabs.query({ status: "complete" }) - ).map((tab) => tab.id); + ).reduce((output: Array, tab) => { + if (!tab.id) return output; + return [...output, tab.id]; + }, []); await Promise.all( targetTabIDs.map(async (tabID) => { - await browser.tabs.sendMessage(tabID, payload); + browser.tabs.sendMessage(tabID, payload); }) ); } diff --git a/source/full/components/pages/NotificationsPage.tsx b/source/full/components/pages/NotificationsPage.tsx index bed643b6..313d95cd 100644 --- a/source/full/components/pages/NotificationsPage.tsx +++ b/source/full/components/pages/NotificationsPage.tsx @@ -23,6 +23,7 @@ export function NotificationsPage() { ])]); setCurrentTab(newTabID); const key = Object.keys(NOTIFICATIONS).find(nKey => NOTIFICATIONS[nKey][0] === newTabID); + if (!key) return; updateReadNotifications(key).catch(err => { console.error(err); getToaster().show({ @@ -38,7 +39,7 @@ export function NotificationsPage() { }, [currentTab, handleTabChange, notifications]); return ( - + {notifications.map(([nameKey, Component]) => ( { - if (mode === "new") { + if (mode === "new" && selectedGroupURI) { const [, sourceID, groupID] = selectedGroupURI.split(":"); onSaveNewClick({ ...credentials, groupID, sourceID }); - } else if (mode === "existing") { + } else if (mode === "existing" && selectedEntryURI) { const [, sourceID, groupID, entryID] = selectedEntryURI.split(":"); onSaveNewClick({ ...credentials, @@ -212,12 +212,15 @@ export function CredentialsSaver(props: CredentialsSaverProps) { contents={contents} onNodeClick={handleNodeClick} onNodeCollapse={node => setExpandedNodes( - current => current.filter(id => id !== node.nodeData.id) + current => current.filter(id => id !== node.nodeData?.id) )} - onNodeExpand={node => setExpandedNodes(current => [ - ...current, - node.nodeData.id - ])} + onNodeExpand={node => { + if (!node.nodeData) return; + setExpandedNodes(current => [ + ...current, + (node.nodeData as NodeInfo).id + ]); + }} /> ) || ( )} - {mode === "new" && contents.length > 0 && selectedGroupURI && ( + {mode === "new" && contents.length > 0 && selectedGroupURI && selectedUsedCredentials && ( )} - {mode === "existing" && contents.length > 0 && selectedEntryURI && ( + {mode === "existing" && contents.length > 0 && selectedEntryURI && selectedUsedCredentials && ( { - if (popupSource === "page") { + if (popupSource === "page" && formID) { sendEntryResultToTabForInput(formID, entry); } else if (popupSource === "popup") { openPageForEntry(entry, autoLogin) diff --git a/source/popup/components/pages/OTPsPage.tsx b/source/popup/components/pages/OTPsPage.tsx index a56eaaab..e3bbc4bc 100644 --- a/source/popup/components/pages/OTPsPage.tsx +++ b/source/popup/components/pages/OTPsPage.tsx @@ -87,7 +87,7 @@ function OTPsPageList(props: OTPsPageProps) { const [otps, loadingOTPs] = useOTPs(); const preparedOTPs = usePreparedOTPs(otps); const handleOTPClick = useCallback((otp: OTP) => { - if (popupSource === "page") { + if (popupSource === "page" && formID) { sendOTPToTabForInput(formID, otp); } else if (popupSource === "popup") { if (!otp.loginURL) { diff --git a/source/popup/components/pages/SaveDialogPage.tsx b/source/popup/components/pages/SaveDialogPage.tsx index cd5871eb..32249c11 100644 --- a/source/popup/components/pages/SaveDialogPage.tsx +++ b/source/popup/components/pages/SaveDialogPage.tsx @@ -107,6 +107,7 @@ export function SaveDialogPage() { } }, [loginID]); const handleCloseClick = useCallback(async () => { + if (!loginID) return; try { // Clear prompt and close dialog await clearSavedLoginPrompt(loginID); @@ -120,6 +121,7 @@ export function SaveDialogPage() { } }, [loginID]); const handleDisableClick = useCallback(async () => { + if (!loginID) return; if (!disableConfirm) { setDisableConfirm(true); return; diff --git a/source/popup/hooks/credentials.ts b/source/popup/hooks/credentials.ts index 2e571b78..5b019cec 100644 --- a/source/popup/hooks/credentials.ts +++ b/source/popup/hooks/credentials.ts @@ -4,14 +4,14 @@ import { sendBackgroundMessage } from "../../shared/services/messaging.js"; import { BackgroundMessageType, UsedCredentials } from "../types.js"; import { useCallback } from "react"; -async function getAllCredentials(): Promise> { +async function getAllCredentials(): Promise> { const resp = await sendBackgroundMessage({ type: BackgroundMessageType.GetSavedCredentials }); if (resp.error) { throw new Layerr(resp.error, "Failed fetching saved credentials"); } - return resp.credentials; + return resp.credentials ?? []; } async function getCredentialsForID(id: string): Promise { @@ -22,17 +22,17 @@ async function getCredentialsForID(id: string): Promise if (resp.error) { throw new Layerr(resp.error, "Failed fetching saved credentials"); } - return resp.credentials[0] ?? null; + return resp.credentials?.[0] ?? null; } -export function useAllLoginCredentials(): AsyncResult> { +export function useAllLoginCredentials(): AsyncResult> { const getCredentials = useCallback(() => getAllCredentials(), []); const result = useAsync(getCredentials, [getCredentials]); return result; } -export function useLoginCredentials(loginID: string): AsyncResult { - const getCredentials = useCallback(() => getCredentialsForID(loginID), [loginID]); +export function useLoginCredentials(loginID: string | null): AsyncResult { + const getCredentials = useCallback(async () => (loginID ? await getCredentialsForID(loginID) : null), [loginID]); const result = useAsync(getCredentials, [getCredentials]); return result; } diff --git a/source/popup/hooks/desktop.ts b/source/popup/hooks/desktop.ts index 0dd29c13..9e9d4dea 100644 --- a/source/popup/hooks/desktop.ts +++ b/source/popup/hooks/desktop.ts @@ -48,7 +48,7 @@ export function useDesktopConnectionState(): DesktopConnectionState { return DesktopConnectionState.Pending; } -export function useEntriesForURL(url: string): Array { +export function useEntriesForURL(url: string | null): Array { const performSearch = useCallback(async () => { if (!url) return []; const results = await searchEntriesByURL(url); diff --git a/source/popup/queries/desktop.ts b/source/popup/queries/desktop.ts index f573187a..ab40f5d4 100644 --- a/source/popup/queries/desktop.ts +++ b/source/popup/queries/desktop.ts @@ -19,7 +19,7 @@ export async function getDesktopConnectionAvailable(): Promise { if (resp.error) { throw new Layerr(resp.error, "Failed checking desktop connection availability"); } - return resp.available; + return resp.available ?? false; } export async function getOTPs(): Promise> { @@ -29,7 +29,7 @@ export async function getOTPs(): Promise> { if (resp.error) { throw new Layerr(resp.error, "Failed fetching OTPs from desktop application"); } - return resp.otps; + return resp.otps ?? []; } export async function getRecentEntries(): Promise> { @@ -40,7 +40,7 @@ export async function getRecentEntries(): Promise> { if (resp.error) { throw new Layerr(resp.error, "Failed fetching recent entries from desktop application"); } - return resp.searchResults; + return resp.searchResults ?? []; } export async function getVaultSources(): Promise> { @@ -50,7 +50,7 @@ export async function getVaultSources(): Promise> if (resp.error) { throw new Layerr(resp.error, "Failed fetching vaults from desktop application"); } - return resp.vaultSources; + return resp.vaultSources ?? []; } export async function initiateDesktopConnectionRequest(): Promise { @@ -91,7 +91,7 @@ export async function searchEntriesByTerm(term: string): Promise> { @@ -102,5 +102,5 @@ export async function searchEntriesByURL(url: string): Promise; -} - -export function EntryResultList(props: EntryResultListProps) { - -} diff --git a/source/shared/components/ErrorBoundary.tsx b/source/shared/components/ErrorBoundary.tsx index 099f1be6..2d1fdcc8 100644 --- a/source/shared/components/ErrorBoundary.tsx +++ b/source/shared/components/ErrorBoundary.tsx @@ -26,7 +26,10 @@ export class ErrorBoundary extends Component { return { error }; } - state = { + state: { + error: null | Error; + errorStack: string | null; + } = { error: null, errorStack: null }; diff --git a/source/shared/hooks/async.ts b/source/shared/hooks/async.ts index 6e1a4f1d..53600c2f 100644 --- a/source/shared/hooks/async.ts +++ b/source/shared/hooks/async.ts @@ -17,13 +17,13 @@ export function useAsync( }: { clearOnExec?: boolean; updateInterval?: number | null; - valuesDiffer?: (existingValue: T, newValue: T) => boolean; + valuesDiffer?: (existingValue: T | null, newValue: T | null) => boolean; } = {} ): AsyncResult { const mounted = useRef(false); const executing = useRef(false); - const [value, setValue] = useState(null); - const [error, setError] = useState(null); + const [value, setValue] = useState(null); + const [error, setError] = useState(null); const [loading, setLoading] = useState(null); const [, setTimer] = useState>(null); const execute = useCallback(async () => { @@ -58,7 +58,7 @@ export function useAsync( useEffect(() => { if (updateInterval === null) return; setTimer((existing) => { - clearTimeout(existing); + clearTimeout(existing as any); return null; }); let newTimer: ReturnType; @@ -100,9 +100,9 @@ export function useAsyncWithTimer( value: T | null; } { const mounted = useRef(false); - const allTimers = useRef([]); + const allTimers = useRef>>([]); const [time, setTime] = useState(Date.now()); - const [timer, setTimer] = useState>(null); + const [timer, setTimer] = useState | null>(null); const { error, loading, value } = useAsync(fn, [...deps, time]); const [lastValue, setLastValue] = useState(value); useEffect(() => { @@ -117,7 +117,7 @@ export function useAsyncWithTimer( useEffect(() => { if (time === 0) return; if (error) { - clearInterval(timer); + clearInterval(timer as any); setTime(0); setTimer(null); return; @@ -127,7 +127,7 @@ export function useAsyncWithTimer( if (!mounted.current) return; setTime(Date.now()); }, delay); - allTimers.current.push(thisTimer); + allTimers.current.push(thisTimer as any); setTimer(thisTimer); } }, [time, timer, error, delay]); diff --git a/source/shared/hooks/config.ts b/source/shared/hooks/config.ts index 3c1ce7e8..06d137fa 100644 --- a/source/shared/hooks/config.ts +++ b/source/shared/hooks/config.ts @@ -14,7 +14,7 @@ export function useConfig(): [ const { value, error } = useAsync(getConfig, [ts], { clearOnExec: false }); - const [changeError, setChangeError] = useState(null); + const [changeError, setChangeError] = useState(null); const setConfigValue = useCallback((setKey: T, value: Configuration[T]) => { setChangeError(null); setNewBackgroundValue(setKey, value) diff --git a/source/shared/library/extension.ts b/source/shared/library/extension.ts index 34c953e1..f587a1a8 100644 --- a/source/shared/library/extension.ts +++ b/source/shared/library/extension.ts @@ -2,12 +2,12 @@ import { getExtensionAPI } from "../extension.js"; const NOOP = () => {}; -export async function createNewTab(url: string): Promise { +export async function createNewTab(url: string): Promise { const browser = getExtensionAPI(); if (!browser.tabs) { // Handle non-background scripts browser.runtime.sendMessage({ type: "open-tab", url }); - return; + return null; } return new Promise((resolve) => chrome.tabs.create({ url }, resolve)); } @@ -15,6 +15,7 @@ export async function createNewTab(url: string): Promise { export function closeCurrentTab() { const browser = getExtensionAPI(); browser.tabs.getCurrent((tab) => { + if (!tab?.id) return; browser.tabs.remove(tab.id, NOOP); }); } diff --git a/source/shared/library/version.ts b/source/shared/library/version.ts index 3c772fda..bb427629 100644 --- a/source/shared/library/version.ts +++ b/source/shared/library/version.ts @@ -1,4 +1,4 @@ // Do not edit this file - it is generated automatically at build time -export const BUILD_DATE = "2024-04-06"; +export const BUILD_DATE = "2024-04-07"; export const VERSION = "3.1.0"; diff --git a/source/shared/queries/config.ts b/source/shared/queries/config.ts index 9c693b0c..519ab20b 100644 --- a/source/shared/queries/config.ts +++ b/source/shared/queries/config.ts @@ -9,6 +9,9 @@ export async function getConfig(): Promise { if (resp.error) { throw new Layerr(resp.error, "Failed fetching application configuration"); } + if (!resp.config) { + throw new Error("No config returned from background"); + } return resp.config; } @@ -24,5 +27,8 @@ export async function setConfigValue( if (resp.error) { throw new Layerr(resp.error, "Failed fetching application configuration"); } + if (!resp.config) { + throw new Error("No config returned from background"); + } return resp.config; } diff --git a/source/shared/types.ts b/source/shared/types.ts index 6a5c2eb2..86d12936 100644 --- a/source/shared/types.ts +++ b/source/shared/types.ts @@ -78,7 +78,7 @@ export interface BackgroundResponse { available?: boolean; autoLogin?: SearchResult | null; config?: Configuration; - credentials?: Array; + credentials?: Array; domains?: Array; entryID?: EntryID | null; error?: Error; diff --git a/source/tab/library/dismount.ts b/source/tab/library/dismount.ts index 35420755..cd3085b7 100644 --- a/source/tab/library/dismount.ts +++ b/source/tab/library/dismount.ts @@ -16,6 +16,9 @@ export function onElementDismount(el: HTMLElement, callback: () => void): void { callback(); } }); + if (!el.parentElement) { + throw new Error("No parent element found for target"); + } mutObs.observe(el.parentElement, { childList: true }); timer = setTimeout(() => { if (!el.parentElement) { diff --git a/source/tab/services/LoginTracker.ts b/source/tab/services/LoginTracker.ts index ed3a0ca5..82525f6f 100644 --- a/source/tab/services/LoginTracker.ts +++ b/source/tab/services/LoginTracker.ts @@ -17,7 +17,7 @@ interface LoginTrackerEvents { credentialsChanged: (event: { id: string; username: string; password: string; entry: boolean }) => void; } -let __sharedTracker = null; +let __sharedTracker: LoginTracker | null = null; export class LoginTracker extends EventEmitter { protected _connections: Array = []; @@ -32,9 +32,11 @@ export class LoginTracker extends EventEmitter { return this._url; } - getConnection(loginTarget: LoginTarget) { - return this._connections.find( - (conn) => conn.loginTarget === loginTarget || conn.loginTarget.form === loginTarget.form + getConnection(loginTarget: LoginTarget): Connection | null { + return ( + this._connections.find( + (conn) => conn.loginTarget === loginTarget || conn.loginTarget.form === loginTarget.form + ) || null ); } diff --git a/source/tab/services/config.ts b/source/tab/services/config.ts index 99b7b386..cde17a66 100644 --- a/source/tab/services/config.ts +++ b/source/tab/services/config.ts @@ -9,5 +9,8 @@ export async function getConfig(): Promise { if (resp.error) { throw new Layerr(resp.error, "Failed fetching configuration"); } + if (!resp.config) { + throw new Error("No config returned from background"); + } return resp.config; } diff --git a/source/tab/services/form.ts b/source/tab/services/form.ts index f349e246..45f46671 100644 --- a/source/tab/services/form.ts +++ b/source/tab/services/form.ts @@ -11,6 +11,12 @@ import { FrameEvent, FrameEventType, TabEventType } from "../types.js"; export function fillFormDetails(frameEvent: FrameEvent) { const { currentLoginTarget: loginTarget } = FORM; const { inputDetails } = frameEvent; + if (!inputDetails) { + throw new Error("No input details for form fill action"); + } + if (!loginTarget) { + throw new Error("No login target found"); + } if (inputDetails.username) { loginTarget.fillUsername(inputDetails.username); } @@ -69,6 +75,18 @@ export async function initialise() { throw new Error("Unexpected details input state"); } } else if (tabEvent.type === TabEventType.OpenPopupDialog) { + if (!tabEvent.sourceURL) { + console.error("No source URL provided"); + return; + } + if (!tabEvent.inputPosition) { + console.error("No input position provided"); + return; + } + if (!tabEvent.inputType) { + console.error("No input type provided"); + return; + } // Re-calculate based upon the iframe the message came from const frame = findIframeForWindow(tabEvent.sourceURL); if (!frame) { @@ -78,7 +96,7 @@ export async function initialise() { const newPosition = recalculateRectForIframe(tabEvent.inputPosition, frame); // Show if top, or pass on to the next frame above if (FRAME.isTop) { - FORM.targetFormID = tabEvent.formID; + FORM.targetFormID = tabEvent.formID ?? null; togglePopup(newPosition, tabEvent.inputType); } else { sendTabEvent( diff --git a/source/tab/services/formDetection.ts b/source/tab/services/formDetection.ts index d43ba4e7..91ad3448 100644 --- a/source/tab/services/formDetection.ts +++ b/source/tab/services/formDetection.ts @@ -15,7 +15,7 @@ function filterLoginTarget(_: LoginTargetFeature, element: HTMLElement): boolean } function onIdentifiedTarget(callback: (target: LoginTarget) => void) { - const locatedForms = []; + const locatedForms: Array = []; const findTargets = () => { getLoginTargets(document, filterLoginTarget) .filter((target) => locatedForms.includes(target.form) === false) diff --git a/source/tab/services/logins/disabled.ts b/source/tab/services/logins/disabled.ts index e437c658..bec5fad8 100644 --- a/source/tab/services/logins/disabled.ts +++ b/source/tab/services/logins/disabled.ts @@ -9,5 +9,5 @@ export async function getDisabledDomains(): Promise> { if (resp.error) { throw new Layerr(resp.error, "Failed fetching disabled login domains"); } - return resp.domains; + return resp.domains ?? []; } diff --git a/source/tab/services/logins/saving.ts b/source/tab/services/logins/saving.ts index 934343cb..06976624 100644 --- a/source/tab/services/logins/saving.ts +++ b/source/tab/services/logins/saving.ts @@ -10,7 +10,7 @@ export async function getCredentialsForID(id: string): Promise { @@ -20,7 +20,7 @@ export async function getLastSavedCredentials(): Promise if (resp.error) { throw new Layerr(resp.error, "Failed fetching last saved credentials"); } - return resp.credentials[0] ?? null; + return resp.credentials?.[0] ?? null; } export function transferLoginCredentials(details: UsedCredentials) { diff --git a/source/tab/services/logins/watcher.ts b/source/tab/services/logins/watcher.ts index e268cdb9..a3566b89 100644 --- a/source/tab/services/logins/watcher.ts +++ b/source/tab/services/logins/watcher.ts @@ -48,16 +48,21 @@ export function watchCredentialsOnTarget(loginTarget: LoginTarget): void { loginTarget, (username, source) => { const connection = tracker.getConnection(loginTarget); - connection.entry = source === "fill"; - connection.username = username; + if (connection) { + connection.entry = source === "fill"; + connection.username = username; + } }, (password, source) => { const connection = tracker.getConnection(loginTarget); - connection.entry = source === "fill"; - connection.password = password; + if (connection) { + connection.entry = source === "fill"; + connection.password = password; + } }, () => { const connection = tracker.getConnection(loginTarget); + if (!connection) return; setTimeout(() => { checkForLoginSaveAbility(connection.id); }, 300); diff --git a/source/tab/services/messaging.ts b/source/tab/services/messaging.ts index 87057d6a..07b3cbd1 100644 --- a/source/tab/services/messaging.ts +++ b/source/tab/services/messaging.ts @@ -46,7 +46,7 @@ export function listenForTabEvents(callback: (event: TabEvent) => void) { if (event.data?.type && Object.values(TabEventType).includes(event.data?.type)) { callback({ ...(event.data as TabEvent), - source: event.source + source: event.source ?? undefined }); } }); diff --git a/source/tab/ui/launch.ts b/source/tab/ui/launch.ts index 9636eeb0..42e486fc 100644 --- a/source/tab/ui/launch.ts +++ b/source/tab/ui/launch.ts @@ -128,6 +128,7 @@ function renderButtonStyle(input: HTMLInputElement, onClick: () => void, reattac // toggleInputDialog(input, DIALOG_TYPE_ENTRY_PICKER); onClick(); }; + // @ts-ignore mount(input.offsetParent, button); onElementDismount(button, () => { reattachCB(); diff --git a/source/tab/ui/popup.ts b/source/tab/ui/popup.ts index b3a18766..22c0cc17 100644 --- a/source/tab/ui/popup.ts +++ b/source/tab/ui/popup.ts @@ -57,7 +57,7 @@ function buildNewPopup(inputRect: ElementRect, forInputType: InputType) { frame ); mount(document.body, container); - const removeBodyResizeListener = onBodyResize(() => updatePopupPosition(__popup.inputRect)); + const removeBodyResizeListener = onBodyResize(() => updatePopupPosition((__popup as LastPopup).inputRect)); document.body.addEventListener("click", closePopup, false); __popup = { cleanup: () => { diff --git a/tsconfig.json b/tsconfig.json index 6a90ac4d..70ca63f2 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,14 +1,15 @@ { "compilerOptions": { - "outDir": "./dist", - "module": "esnext", - "moduleResolution": "node", - "target": "ES6", + "allowSyntheticDefaultImports": true, "esModuleInterop": true, - "strict": false, "jsx": "react-jsx", + "module": "esnext", + "moduleResolution": "node", + "outDir": "./dist", "resolveJsonModule": true, - "allowSyntheticDefaultImports": true, + "strict": false, + "strictNullChecks": true, + "target": "ES6", "types": ["chrome", "react", "react-dom"] }, "include": [