Refactor browser extension to use shared types, add import order lint rules (#900)

This commit is contained in:
Leendert de Borst
2025-06-08 16:56:41 +02:00
committed by Leendert de Borst
parent c6d7d16b27
commit 22acea0e35
72 changed files with 482 additions and 379 deletions

View File

@@ -105,8 +105,51 @@ export default [
],
"react-hooks/exhaustive-deps": "warn",
"react/jsx-no-constructed-context-values": "error",
"import/no-unresolved": "error",
},
"import/no-unresolved": [
"error",
{
ignore: ['^#imports$'] // Ignore virtual imports from WXT which are not resolved by the typescript compiler
}
],
"import/order": [
"error",
{
"groups": [
"builtin", // Node "fs", "path", etc.
"external", // "react", "lodash", etc.
"internal", // Aliased paths like "@/utils"
"parent", // "../"
"sibling", // "./"
"index", // "./index"
"object", // import 'foo'
"type" // import type ...
],
"pathGroups": [
{
pattern: "@/entrypoints/**",
group: "internal",
position: "before"
},
{
pattern: "@/utils/**",
group: "internal",
position: "before"
},
{
pattern: "@/hooks/**",
group: "internal",
position: "before"
}
],
"pathGroupsExcludedImportTypes": ["builtin"],
"newlines-between": "always",
"alphabetize": {
order: "asc",
caseInsensitive: true
}
}
],
},
settings: {
'import/resolver': {
typescript: {

View File

@@ -1,11 +1,13 @@
import { defineBackground } from '#imports';
import { onMessage, sendMessage } from "webext-bridge/background";
import { setupContextMenus } from '@/entrypoints/background/ContextMenu';
import { handleCheckAuthStatus, handleClearVault, handleCreateIdentity, handleGetCredentials, handleGetDefaultEmailDomain, handleGetDefaultIdentityLanguage, handleGetDerivedKey, handleGetPasswordSettings, handleGetVault, handleStoreVault, handleSyncVault } from '@/entrypoints/background/VaultMessageHandler';
import { handleOpenPopup, handlePopupWithCredential, handleToggleContextMenu } from '@/entrypoints/background/PopupMessageHandler';
import { storage, browser } from '#imports';
import { handleCheckAuthStatus, handleClearVault, handleCreateIdentity, handleGetCredentials, handleGetDefaultEmailDomain, handleGetDefaultIdentityLanguage, handleGetDerivedKey, handleGetPasswordSettings, handleGetVault, handleStoreVault, handleSyncVault } from '@/entrypoints/background/VaultMessageHandler';
import { GLOBAL_CONTEXT_MENU_ENABLED_KEY } from '@/utils/Constants';
import { defineBackground, storage, browser } from '#imports';
export default defineBackground({
/**
* This is the main entry point for the background script.
@@ -32,7 +34,7 @@ export default defineBackground({
if (isContextMenuEnabled) {
setupContextMenus();
}
// Listen for custom commands
try {
browser.commands.onCommand.addListener(async (command) => {

View File

@@ -1,8 +1,10 @@
import { sendMessage } from 'webext-bridge/background';
import { browser } from "#imports";
import { type Browser } from '@wxt-dev/browser';
import { sendMessage } from 'webext-bridge/background';
import { PasswordGenerator } from '@/utils/shared/password-generator';
import { browser } from "#imports";
/**
* Setup the context menus.
*/

View File

@@ -1,8 +1,10 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { browser } from '#imports';
import { BoolResponse } from '@/utils/types/messaging/BoolResponse';
import { setupContextMenus } from './ContextMenu';
import { browser } from '#imports';
/**
* Handle opening the popup.
*/

View File

@@ -1,16 +1,15 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { storage } from 'wxt/utils/storage';
import { EncryptionUtility } from '@/utils/EncryptionUtility';
import type { Vault, VaultResponse, VaultPostResponse } from '@/utils/shared/models';
import { SqliteClient } from '@/utils/SqliteClient';
import { WebApiService } from '@/utils/WebApiService';
import { Vault } from '@/utils/types/webapi/Vault';
import { VaultResponse } from '@/utils/types/webapi/VaultResponse';
import { VaultPostResponse } from '@/utils/types/webapi/VaultPostResponse';
import { BoolResponse as messageBoolResponse } from '@/utils/types/messaging/BoolResponse';
import { VaultResponse as messageVaultResponse } from '@/utils/types/messaging/VaultResponse';
import { CredentialsResponse as messageCredentialsResponse } from '@/utils/types/messaging/CredentialsResponse';
import { StringResponse as stringResponse } from '@/utils/types/messaging/StringResponse';
import { PasswordSettingsResponse as messagePasswordSettingsResponse } from '@/utils/types/messaging/PasswordSettingsResponse';
import { StringResponse as stringResponse } from '@/utils/types/messaging/StringResponse';
import { VaultResponse as messageVaultResponse } from '@/utils/types/messaging/VaultResponse';
import { WebApiService } from '@/utils/WebApiService';
/**
* Check if the user is logged in and if the vault is locked.

View File

@@ -1,9 +1,12 @@
import '@/entrypoints/contentScript/style.css';
import { FormDetector } from '@/utils/formDetector/FormDetector';
import { isAutoShowPopupEnabled, openAutofillPopup, removeExistingPopup } from '@/entrypoints/contentScript/Popup';
import { injectIcon, popupDebounceTimeHasPassed, validateInputField } from '@/entrypoints/contentScript/Form';
import { onMessage } from "webext-bridge/content-script";
import { injectIcon, popupDebounceTimeHasPassed, validateInputField } from '@/entrypoints/contentScript/Form';
import { isAutoShowPopupEnabled, openAutofillPopup, removeExistingPopup } from '@/entrypoints/contentScript/Popup';
import { FormDetector } from '@/utils/formDetector/FormDetector';
import { BoolResponse as messageBoolResponse } from '@/utils/types/messaging/BoolResponse';
import { defineContentScript } from '#imports';
import { createShadowRootUi } from '#imports';

View File

@@ -1,7 +1,8 @@
import { openAutofillPopup } from '@/entrypoints/contentScript/Popup';
import { FormDetector } from '@/utils/formDetector/FormDetector';
import { FormFiller } from '@/utils/formDetector/FormFiller';
import { Credential } from '@/utils/types/Credential';
import { openAutofillPopup } from '@/entrypoints/contentScript/Popup';
/**
* Global timestamp to track popup debounce time.

View File

@@ -1,16 +1,19 @@
import { storage } from '#imports';
import { sendMessage } from 'webext-bridge/content-script';
import { fillCredential } from '@/entrypoints/contentScript/Form';
import { filterCredentials } from '@/entrypoints/contentScript/Filter';
import { fillCredential } from '@/entrypoints/contentScript/Form';
import { DISABLED_SITES_KEY, TEMPORARY_DISABLED_SITES_KEY, GLOBAL_AUTOFILL_POPUP_ENABLED_KEY, VAULT_LOCKED_DISMISS_UNTIL_KEY, LAST_CUSTOM_EMAIL_KEY, LAST_CUSTOM_USERNAME_KEY } from '@/utils/Constants';
import { FormDetector } from '@/utils/formDetector/FormDetector';
import { CreateIdentityGenerator } from '@/utils/shared/identity-generator';
import { CreatePasswordGenerator, PasswordGenerator } from '@/utils/shared/password-generator';
import { SqliteClient } from '@/utils/SqliteClient';
import { Credential } from '@/utils/types/Credential';
import { CredentialsResponse } from '@/utils/types/messaging/CredentialsResponse';
import { PasswordSettingsResponse } from '@/utils/types/messaging/PasswordSettingsResponse';
import { SqliteClient } from '@/utils/SqliteClient';
import { StringResponse } from '@/utils/types/messaging/StringResponse';
import { FormDetector } from '@/utils/formDetector/FormDetector';
import { Credential } from '@/utils/types/Credential';
import { DISABLED_SITES_KEY, TEMPORARY_DISABLED_SITES_KEY, GLOBAL_AUTOFILL_POPUP_ENABLED_KEY, VAULT_LOCKED_DISMISS_UNTIL_KEY, LAST_CUSTOM_EMAIL_KEY, LAST_CUSTOM_USERNAME_KEY } from '@/utils/Constants';
import { storage } from '#imports';
/**
* WeakMap to store event listeners for popup containers

View File

@@ -1,20 +1,22 @@
import React, { useState, useEffect } from 'react';
import { HashRouter as Router, Routes, Route } from 'react-router-dom';
import { useAuth } from '@/entrypoints/popup/context/AuthContext';
import { useMinDurationLoading } from '@/hooks/useMinDurationLoading';
import Header from '@/entrypoints/popup/components/Layout/Header';
import BottomNav from '@/entrypoints/popup/components/Layout/BottomNav';
import AuthSettings from '@/entrypoints/popup/pages/AuthSettings';
import CredentialsList from '@/entrypoints/popup/pages/CredentialsList';
import EmailsList from '@/entrypoints/popup/pages/EmailsList';
import LoadingSpinner from '@/entrypoints/popup/components/LoadingSpinner';
import Home from '@/entrypoints/popup/pages/Home';
import CredentialDetails from '@/entrypoints/popup/pages/CredentialDetails';
import EmailDetails from '@/entrypoints/popup/pages/EmailDetails';
import Settings from '@/entrypoints/popup/pages/Settings';
import GlobalStateChangeHandler from '@/entrypoints/popup/components/GlobalStateChangeHandler';
import BottomNav from '@/entrypoints/popup/components/Layout/BottomNav';
import Header from '@/entrypoints/popup/components/Layout/Header';
import LoadingSpinner from '@/entrypoints/popup/components/LoadingSpinner';
import { useAuth } from '@/entrypoints/popup/context/AuthContext';
import { useLoading } from '@/entrypoints/popup/context/LoadingContext';
import AuthSettings from '@/entrypoints/popup/pages/AuthSettings';
import CredentialDetails from '@/entrypoints/popup/pages/CredentialDetails';
import CredentialsList from '@/entrypoints/popup/pages/CredentialsList';
import EmailDetails from '@/entrypoints/popup/pages/EmailDetails';
import EmailsList from '@/entrypoints/popup/pages/EmailsList';
import Home from '@/entrypoints/popup/pages/Home';
import Logout from '@/entrypoints/popup/pages/Logout';
import Settings from '@/entrypoints/popup/pages/Settings';
import { useMinDurationLoading } from '@/hooks/useMinDurationLoading';
import '@/entrypoints/popup/style.css';
/**

View File

@@ -1,7 +1,8 @@
import React from 'react';
import { useNavigate } from 'react-router-dom';
import { Credential } from '@/utils/types/Credential';
import SqliteClient from '@/utils/SqliteClient';
import { Credential } from '@/utils/types/Credential';
type CredentialCardProps = {
credential: Credential;

View File

@@ -1,7 +1,9 @@
import React from 'react';
import { Credential } from '@/utils/types/Credential';
import { FormInputCopyToClipboard } from '@/entrypoints/popup/components/FormInputCopyToClipboard';
import { IdentityHelperUtils } from '@/utils/shared/identity-generator';
import { Credential } from '@/utils/types/Credential';
type AliasBlockProps = {
credential: Credential;

View File

@@ -1,4 +1,5 @@
import React from 'react';
import { EmailPreview } from '@/entrypoints/popup/components/EmailPreview';
type EmailBlockProps = {

View File

@@ -1,6 +1,7 @@
import React from 'react';
import { Credential } from '@/utils/types/Credential';
import SqliteClient from '@/utils/SqliteClient';
import { Credential } from '@/utils/types/Credential';
type HeaderBlockProps = {
credential: Credential;

View File

@@ -1,7 +1,9 @@
import React from 'react';
import { Credential } from '@/utils/types/Credential';
import { FormInputCopyToClipboard } from '@/entrypoints/popup/components/FormInputCopyToClipboard';
import { Credential } from '@/utils/types/Credential';
type LoginCredentialsBlockProps = {
credential: Credential;
}

View File

@@ -1,7 +1,9 @@
import React, { useState, useEffect } from 'react';
import { useDb } from '@/entrypoints/popup/context/DbContext';
import { TotpCode } from '@/utils/types/TotpCode';
import * as OTPAuth from 'otpauth';
import React, { useState, useEffect } from 'react';
import { useDb } from '@/entrypoints/popup/context/DbContext';
import { TotpCode } from '@/utils/types/TotpCode';
type TotpBlockProps = {
credentialId: string;

View File

@@ -1,9 +1,9 @@
import HeaderBlock from './HeaderBlock';
import EmailBlock from './EmailBlock';
import TotpBlock from './TotpBlock';
import LoginCredentialsBlock from './LoginCredentialsBlock';
import AliasBlock from './AliasBlock';
import EmailBlock from './EmailBlock';
import HeaderBlock from './HeaderBlock';
import LoginCredentialsBlock from './LoginCredentialsBlock';
import NotesBlock from './NotesBlock';
import TotpBlock from './TotpBlock';
export {
HeaderBlock,

View File

@@ -1,11 +1,14 @@
import React, { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
import { storage } from '#imports';
import { useWebApi } from '@/entrypoints/popup/context/WebApiContext';
import { useDb } from '@/entrypoints/popup/context/DbContext';
import { EncryptionUtility } from '@/utils/EncryptionUtility';
import { MailboxEmail } from '@/utils/types/webapi/MailboxEmail';
import { useWebApi } from '@/entrypoints/popup/context/WebApiContext';
import { AppInfo } from '@/utils/AppInfo';
import { EncryptionUtility } from '@/utils/EncryptionUtility';
import type { MailboxEmail } from '@/utils/shared/models';
import { storage } from '#imports';
type EmailPreviewProps = {
email: string;

View File

@@ -1,4 +1,5 @@
import React, { useState, useEffect } from 'react';
import { ClipboardCopyService } from '@/entrypoints/popup/utils/ClipboardCopyService';
/**

View File

@@ -1,5 +1,6 @@
import React, { useEffect, useRef } from 'react';
import { useNavigate } from 'react-router-dom';
import { useAuth } from '@/entrypoints/popup/context/AuthContext';
/**

View File

@@ -1,5 +1,6 @@
import React, { useState, useEffect } from 'react';
import { useNavigate, useLocation } from 'react-router-dom';
import { useAuth } from '@/entrypoints/popup/context/AuthContext';
import { useDb } from '@/entrypoints/popup/context/DbContext';

View File

@@ -1,10 +1,13 @@
import React from 'react';
import { useNavigate, useLocation } from 'react-router-dom';
import { storage } from '#imports';
import { UserMenu } from '@/entrypoints/popup/components/Layout/UserMenu';
import { useAuth } from '@/entrypoints/popup/context/AuthContext';
import { AppInfo } from '@/utils/AppInfo';
import { storage } from '#imports';
/**
* Header props.
*/

View File

@@ -1,5 +1,6 @@
import React, { useState, useRef, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { useAuth } from '@/entrypoints/popup/context/AuthContext';
import { useLoading } from '@/entrypoints/popup/context/LoadingContext';

View File

@@ -1,4 +1,5 @@
import React from 'react';
import { useLoading } from '@/entrypoints/popup/context/LoadingContext';
/**

View File

@@ -1,8 +1,10 @@
import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { storage } from '#imports';
import { AppInfo } from '@/utils/AppInfo';
import { storage } from '#imports';
/**
* Component for displaying the login server information.
*/

View File

@@ -1,9 +1,12 @@
import React, { createContext, useContext, useState, useEffect, useMemo, useCallback } from 'react';
import { storage } from '#imports';
import { sendMessage } from 'webext-bridge/popup';
import { useDb } from '@/entrypoints/popup/context/DbContext';
import { VAULT_LOCKED_DISMISS_UNTIL_KEY } from '@/utils/Constants';
import { storage } from '#imports';
type AuthContextType = {
isLoggedIn: boolean;
isInitialized: boolean;

View File

@@ -1,8 +1,9 @@
import React, { createContext, useContext, useState, useEffect, useCallback, useMemo } from 'react';
import { sendMessage } from 'webext-bridge/popup';
import SqliteClient from '@/utils/SqliteClient';
import { VaultResponse } from '@/utils/types/webapi/VaultResponse';
import EncryptionUtility from '@/utils/EncryptionUtility';
import type { VaultResponse } from '@/utils/shared/models';
import SqliteClient from '@/utils/SqliteClient';
import { VaultResponse as messageVaultResponse } from '@/utils/types/messaging/VaultResponse';
type DbContextType = {

View File

@@ -1,4 +1,5 @@
import React, { createContext, useContext, useState, useMemo } from 'react';
import LoadingSpinnerFullScreen from '@/entrypoints/popup/components/LoadingSpinnerFullScreen';
type LoadingContextType = {

View File

@@ -1,4 +1,5 @@
import React, { createContext, useContext, useState, useMemo, useEffect, useCallback } from 'react';
import { storage } from '#imports';
/**

View File

@@ -1,7 +1,9 @@
import React, { createContext, useContext, useEffect, useState } from 'react';
import { WebApiService } from '@/utils/WebApiService';
import { useAuth } from '@/entrypoints/popup/context/AuthContext';
import { WebApiService } from '@/utils/WebApiService';
const WebApiContext = createContext<WebApiService | null>(null);
/**

View File

@@ -0,0 +1,161 @@
import { useCallback } from 'react';
import { useAuth } from '@/entrypoints/popup/context/AuthContext';
import { useDb } from '@/entrypoints/popup/context/DbContext';
import { useWebApi } from '@/entrypoints/popup/context/WebApiContext';
import { AppInfo } from '@/utils/AppInfo';
import type { VaultResponse } from '@/utils/shared/models';
/**
* Utility function to ensure a minimum time has elapsed for an operation
*/
const withMinimumDelay = async <T>(
operation: () => Promise<T>,
minDelayMs: number,
initialSync: boolean
): Promise<T> => {
if (!initialSync) {
return operation();
}
const startTime = Date.now();
const result = await operation();
const elapsedTime = Date.now() - startTime;
if (elapsedTime < minDelayMs) {
await new Promise(resolve => setTimeout(resolve, minDelayMs - elapsedTime));
}
return result;
};
type VaultSyncOptions = {
initialSync?: boolean;
onSuccess?: (hasNewVault: boolean) => void;
onError?: (error: string) => void;
onStatus?: (message: string) => void;
onOffline?: () => void;
}
/**
* Hook to sync the vault with the server.
*/
export const useVaultSync = () : {
syncVault: (options?: VaultSyncOptions) => Promise<boolean>;
} => {
const authContext = useAuth();
const dbContext = useDb();
const webApi = useWebApi();
const syncVault = useCallback(async (options: VaultSyncOptions = {}) => {
const { initialSync = false, onSuccess, onError, onStatus, onOffline } = options;
try {
const { isLoggedIn } = await authContext.initializeAuth();
if (!isLoggedIn) {
// Not authenticated, return false immediately
return false;
}
// Check app status and vault revision
onStatus?.('Checking vault updates');
const statusResponse = await withMinimumDelay(
() => webApi.getStatus(),
300,
initialSync
);
if (statusResponse.serverVersion === '0.0.0') {
// Server is not available, go into offline mode
onOffline?.();
return false;
}
if (!statusResponse.clientVersionSupported) {
const statusError = 'This version of the AliasVault mobile app is not supported by the server anymore. Please update your app to the latest version.';
onError?.(statusError);
return false;
}
if (!AppInfo.isServerVersionSupported(statusResponse.serverVersion)) {
const statusError = 'The AliasVault server needs to be updated to a newer version in order to use this mobile app. Please contact support if you need help.';
onError?.(statusError);
return false;
}
/*
* If we get here, it means we have a valid connection to the server.
* TODO: browser extension does not support offline mode yet.
* authContext.setOfflineMode(false);
*/
// Compare vault revisions
const vaultMetadata = await dbContext.getVaultMetadata();
const vaultRevisionNumber = vaultMetadata?.vaultRevisionNumber ?? 0;
if (statusResponse.vaultRevision > vaultRevisionNumber) {
onStatus?.('Syncing updated vault');
const vaultResponseJson = await withMinimumDelay(
() => webApi.get<VaultResponse>('Vault'),
1000,
initialSync
);
const vaultError = webApi.validateVaultResponse(vaultResponseJson as VaultResponse);
if (vaultError) {
// Only logout if it's an authentication error, not a network error
if (vaultError.includes('authentication') || vaultError.includes('unauthorized')) {
await webApi.logout(vaultError);
onError?.(vaultError);
return false;
}
/*
* TODO: browser extension does not support offline mode yet.
* For other errors, go into offline mode
* authContext.setOfflineMode(true);
*/
return false;
}
try {
await dbContext.initializeDatabase(vaultResponseJson as VaultResponse);
onSuccess?.(true);
return true;
} catch {
// Vault could not be decrypted, throw an error
throw new Error('Vault could not be decrypted, if problem persists please logout and login again.');
}
}
await withMinimumDelay(
() => Promise.resolve(onSuccess?.(false)),
300,
initialSync
);
return false;
} catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Unknown error during vault sync';
console.error('Vault sync error:', err);
/*
* Check if it's a network error
* TODO: browser extension does not support offline mode yet.
*/
/*
* if (errorMessage.includes('network') || errorMessage.includes('timeout')) {
*authContext.setOfflineMode(true);
*return true;
*}
*/
onError?.(errorMessage);
return false;
}
}, [authContext, dbContext, webApi]);
return { syncVault };
};

View File

@@ -1,10 +1,12 @@
import ReactDOM from 'react-dom/client';
import App from '@/entrypoints/popup/App';
import { AuthProvider } from '@/entrypoints/popup/context/AuthContext';
import { WebApiProvider } from '@/entrypoints/popup/context/WebApiContext';
import { DbProvider } from '@/entrypoints/popup/context/DbContext';
import { LoadingProvider } from '@/entrypoints/popup/context/LoadingContext';
import { ThemeProvider } from '@/entrypoints/popup/context/ThemeContext';
import { WebApiProvider } from '@/entrypoints/popup/context/WebApiContext';
import { setupExpandedMode } from '@/utils/ExpandedMode';
// Run before React initializes to ensure the popup is always a fixed width except for when explicitly expanded.

View File

@@ -1,9 +1,11 @@
import React, { useState, useEffect } from 'react';
import { AppInfo } from '@/utils/AppInfo';
import { storage } from '#imports';
import { GLOBAL_AUTOFILL_POPUP_ENABLED_KEY, DISABLED_SITES_KEY, VAULT_LOCKED_DISMISS_UNTIL_KEY } from '@/utils/Constants';
import * as Yup from 'yup';
import { AppInfo } from '@/utils/AppInfo';
import { GLOBAL_AUTOFILL_POPUP_ENABLED_KEY, DISABLED_SITES_KEY, VAULT_LOCKED_DISMISS_UNTIL_KEY } from '@/utils/Constants';
import { storage } from '#imports';
type ApiOption = {
label: string;
value: string;

View File

@@ -1,8 +1,6 @@
import React, { useState, useEffect } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { useDb } from '@/entrypoints/popup/context/DbContext';
import { Credential } from '@/utils/types/Credential';
import { useLoading } from '@/entrypoints/popup/context/LoadingContext';
import {
HeaderBlock,
EmailBlock,
@@ -11,6 +9,10 @@ import {
AliasBlock,
NotesBlock
} from '@/entrypoints/popup/components/CredentialDetails';
import { useDb } from '@/entrypoints/popup/context/DbContext';
import { useLoading } from '@/entrypoints/popup/context/LoadingContext';
import { Credential } from '@/utils/types/Credential';
/**
* Credential details page.

View File

@@ -1,14 +1,17 @@
import React, { useState, useEffect, useCallback } from 'react';
import { sendMessage } from 'webext-bridge/popup';
import CredentialCard from '@/entrypoints/popup/components/CredentialCard';
import LoadingSpinner from '@/entrypoints/popup/components/LoadingSpinner';
import ReloadButton from '@/entrypoints/popup/components/ReloadButton';
import { useDb } from '@/entrypoints/popup/context/DbContext';
import { Credential } from '@/utils/types/Credential';
import { useLoading } from '@/entrypoints/popup/context/LoadingContext';
import { useWebApi } from '@/entrypoints/popup/context/WebApiContext';
import { VaultResponse } from '@/utils/types/webapi/VaultResponse';
import ReloadButton from '@/entrypoints/popup/components/ReloadButton';
import LoadingSpinner from '@/entrypoints/popup/components/LoadingSpinner';
import type { VaultResponse } from '@/utils/shared/models';
import { Credential } from '@/utils/types/Credential';
import { useMinDurationLoading } from '@/hooks/useMinDurationLoading';
import CredentialCard from '@/entrypoints/popup/components/CredentialCard';
/**
* Credentials list page.

View File

@@ -1,15 +1,17 @@
import React, { useEffect, useState } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { Email } from '@/utils/types/webapi/Email';
import { useDb } from '@/entrypoints/popup/context/DbContext';
import { useWebApi } from '@/entrypoints/popup/context/WebApiContext';
import LoadingSpinner from '@/entrypoints/popup/components/LoadingSpinner';
import { useMinDurationLoading } from '@/hooks/useMinDurationLoading';
import EncryptionUtility from '@/utils/EncryptionUtility';
import { Attachment } from '@/utils/types/webapi/Attachment';
import { useDb } from '@/entrypoints/popup/context/DbContext';
import { useLoading } from '@/entrypoints/popup/context/LoadingContext';
import { useWebApi } from '@/entrypoints/popup/context/WebApiContext';
import ConversionUtility from '@/entrypoints/popup/utils/ConversionUtility';
import EncryptionUtility from '@/utils/EncryptionUtility';
import type { Attachment, Email } from '@/utils/shared/models';
import { useMinDurationLoading } from '@/hooks/useMinDurationLoading';
/**
* Email details page.
*/

View File

@@ -1,13 +1,15 @@
import React, { useEffect, useState, useCallback } from 'react';
import { Link } from 'react-router-dom';
import { MailboxBulkRequest, MailboxBulkResponse } from '@/utils/types/webapi/MailboxBulk';
import { MailboxEmail } from '@/utils/types/webapi/MailboxEmail';
import LoadingSpinner from '@/entrypoints/popup/components/LoadingSpinner';
import ReloadButton from '@/entrypoints/popup/components/ReloadButton';
import { useDb } from '@/entrypoints/popup/context/DbContext';
import { useWebApi } from '@/entrypoints/popup/context/WebApiContext';
import LoadingSpinner from '@/entrypoints/popup/components/LoadingSpinner';
import { useMinDurationLoading } from '@/hooks/useMinDurationLoading';
import EncryptionUtility from '@/utils/EncryptionUtility';
import ReloadButton from '@/entrypoints/popup/components/ReloadButton';
import type { MailboxBulkRequest, MailboxBulkResponse, MailboxEmail } from '@/utils/shared/models';
import { useMinDurationLoading } from '@/hooks/useMinDurationLoading';
/**
* Emails list page.

View File

@@ -1,11 +1,12 @@
import React, { useState, useEffect } from 'react';
import { useAuth } from '@/entrypoints/popup/context/AuthContext';
import Unlock from '@/entrypoints/popup/pages/Unlock';
import Login from '@/entrypoints/popup/pages/Login';
import UnlockSuccess from '@/entrypoints/popup/pages/UnlockSuccess';
import { useNavigate } from 'react-router-dom';
import { useAuth } from '@/entrypoints/popup/context/AuthContext';
import { useDb } from '@/entrypoints/popup/context/DbContext';
import { useLoading } from '@/entrypoints/popup/context/LoadingContext';
import Login from '@/entrypoints/popup/pages/Login';
import Unlock from '@/entrypoints/popup/pages/Unlock';
import UnlockSuccess from '@/entrypoints/popup/pages/UnlockSuccess';
/**
* Home page that shows the correct page based on the user's authentication state.

View File

@@ -1,20 +1,24 @@
import React, { useEffect, useState } from 'react';
import { Buffer } from 'buffer';
import { storage } from '#imports';
import React, { useEffect, useState } from 'react';
import Button from '@/entrypoints/popup/components/Button';
import LoginServerInfo from '@/entrypoints/popup/components/LoginServerInfo';
import { useAuth } from '@/entrypoints/popup/context/AuthContext';
import { useDb } from '@/entrypoints/popup/context/DbContext';
import { useWebApi } from '@/entrypoints/popup/context/WebApiContext';
import { AppInfo } from '@/utils/AppInfo';
import Button from '@/entrypoints/popup/components/Button';
import EncryptionUtility from '@/utils/EncryptionUtility';
import SrpUtility from '@/entrypoints/popup/utils/SrpUtility';
import { useLoading } from '@/entrypoints/popup/context/LoadingContext';
import { VaultResponse } from '@/utils/types/webapi/VaultResponse';
import { LoginResponse } from '@/utils/types/webapi/Login';
import LoginServerInfo from '@/entrypoints/popup/components/LoginServerInfo';
import { useWebApi } from '@/entrypoints/popup/context/WebApiContext';
import SrpUtility from '@/entrypoints/popup/utils/SrpUtility';
import { AppInfo } from '@/utils/AppInfo';
import EncryptionUtility from '@/utils/EncryptionUtility';
import type { VaultResponse, LoginResponse } from '@/utils/shared/models';
import { ApiAuthError } from '@/utils/types/errors/ApiAuthError';
import ConversionUtility from '../utils/ConversionUtility';
import { storage } from '#imports';
/**
* Login page
*/

View File

@@ -1,5 +1,6 @@
import React, { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { useAuth } from '@/entrypoints/popup/context/AuthContext';
import { useWebApi } from '@/entrypoints/popup/context/WebApiContext';

View File

@@ -1,9 +1,12 @@
import React, { useEffect, useState, useCallback } from 'react';
import { storage } from "#imports";
import { sendMessage } from 'webext-bridge/popup';
import { DISABLED_SITES_KEY, GLOBAL_AUTOFILL_POPUP_ENABLED_KEY, GLOBAL_CONTEXT_MENU_ENABLED_KEY, TEMPORARY_DISABLED_SITES_KEY } from '@/utils/Constants';
import { AppInfo } from '@/utils/AppInfo';
import { useTheme } from '@/entrypoints/popup/context/ThemeContext';
import { AppInfo } from '@/utils/AppInfo';
import { DISABLED_SITES_KEY, GLOBAL_AUTOFILL_POPUP_ENABLED_KEY, GLOBAL_CONTEXT_MENU_ENABLED_KEY, TEMPORARY_DISABLED_SITES_KEY } from '@/utils/Constants';
import { storage } from "#imports";
import { browser } from "#imports";
/**

View File

@@ -1,16 +1,20 @@
import { Buffer } from 'buffer';
import React, { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { Buffer } from 'buffer';
import { storage } from '#imports';
import { useDb } from '@/entrypoints/popup/context/DbContext';
import { useAuth } from '@/entrypoints/popup/context/AuthContext';
import { useWebApi } from '@/entrypoints/popup/context/WebApiContext';
import Button from '@/entrypoints/popup/components/Button';
import EncryptionUtility from '@/utils/EncryptionUtility';
import SrpUtility from '@/entrypoints/popup/utils/SrpUtility';
import { VaultResponse } from '@/utils/types/webapi/VaultResponse';
import { useAuth } from '@/entrypoints/popup/context/AuthContext';
import { useDb } from '@/entrypoints/popup/context/DbContext';
import { useLoading } from '@/entrypoints/popup/context/LoadingContext';
import { useWebApi } from '@/entrypoints/popup/context/WebApiContext';
import SrpUtility from '@/entrypoints/popup/utils/SrpUtility';
import { VAULT_LOCKED_DISMISS_UNTIL_KEY } from '@/utils/Constants';
import EncryptionUtility from '@/utils/EncryptionUtility';
import type { VaultResponse } from '@/utils/shared/models';
import { storage } from '#imports';
/**
* Unlock page

View File

@@ -1,9 +1,8 @@
import srp from 'secure-remote-password/client'
import { WebApiService } from '@/utils/WebApiService';
import { LoginRequest, LoginResponse } from '@/utils/types/webapi/Login';
import { ValidateLoginRequest, ValidateLoginRequest2Fa, ValidateLoginResponse } from '@/utils/types/webapi/ValidateLogin';
import BadRequestResponse from '@/utils/types/webapi/BadRequestResponse';
import type { LoginRequest, LoginResponse, ValidateLoginRequest, ValidateLoginRequest2Fa, ValidateLoginResponse, BadRequestResponse } from '@/utils/shared/models';
import { ApiAuthError } from '@/utils/types/errors/ApiAuthError';
import { WebApiService } from '@/utils/WebApiService';
/**
* Utility class for SRP authentication operations.

View File

@@ -1,9 +1,11 @@
import argon2 from 'argon2-browser/dist/argon2-bundled.min.js';
import { Email } from './types/webapi/Email';
import { EncryptionKey } from './types/EncryptionKey';
import { MailboxEmail } from './types/webapi/MailboxEmail';
import { Buffer } from 'buffer';
import argon2 from 'argon2-browser/dist/argon2-bundled.min.js';
import type { Email, MailboxEmail } from '@/utils/shared/models';
import { EncryptionKey } from './types/EncryptionKey';
/**
* Utility class for encryption operations including:
* - Argon2Id key derivation

View File

@@ -1,8 +1,9 @@
import initSqlJs, { Database } from 'sql.js';
import { Credential } from './types/Credential';
import { EncryptionKey } from './types/EncryptionKey';
import { TotpCode } from './types/TotpCode';
import { PasswordSettings } from './types/PasswordSettings';
import { TotpCode } from './types/TotpCode';
/**
* Placeholder base64 image for credentials without a logo.

View File

@@ -1,6 +1,7 @@
import type { StatusResponse, VaultResponse } from '@/utils/shared/models';
import { AppInfo } from "./AppInfo";
import { StatusResponse } from "./types/webapi/StatusResponse";
import { VaultResponse } from "./types/webapi/VaultResponse";
import { storage } from '#imports';
type RequestInit = globalThis.RequestInit;

View File

@@ -1,6 +1,7 @@
import { AppInfo } from '../AppInfo';
import { describe, it, expect } from 'vitest';
import { AppInfo } from '../AppInfo';
describe('AppInfo', () => {
describe('isVersionSupported', () => {
it('should support exact version match', () => {

View File

@@ -1,5 +1,5 @@
import { FormFields } from "./types/FormFields";
import { CombinedFieldPatterns, CombinedGenderOptionPatterns, CombinedStopWords } from "./FieldPatterns";
import { FormFields } from "./types/FormFields";
/**
* Form detector.

View File

@@ -1,7 +1,7 @@
import { Credential } from "@/utils/types/Credential";
import { FormFields } from "@/utils/formDetector/types/FormFields";
import { CombinedDateOptionPatterns, CombinedGenderOptionPatterns } from "@/utils/formDetector/FieldPatterns";
import { FormFields } from "@/utils/formDetector/types/FormFields";
import { Gender, IdentityHelperUtils } from "@/utils/shared/identity-generator";
import { Credential } from "@/utils/types/Credential";
/**
* Class to fill the fields of a form with the given credential.
*/

View File

@@ -1,4 +1,5 @@
import { describe, expect, it } from 'vitest';
import { FormField, testField } from './TestUtils';
describe('FormDetector English tests', () => {

View File

@@ -1,7 +1,9 @@
import { describe, it, expect } from 'vitest';
import { createTestDom } from './TestUtils';
import { FormDetector } from '../FormDetector';
import { createTestDom } from './TestUtils';
describe('FormDetector generic tests', () => {
describe('Invalid form not detected as login form 1', () => {
const htmlFile = 'invalid-form1.html';

View File

@@ -1,4 +1,5 @@
import { describe, it, expect } from 'vitest';
import { FormField, testField, testBirthdateFormat } from './TestUtils';
describe('FormDetector Dutch tests', () => {

View File

@@ -1,7 +1,9 @@
import { describe, it, expect } from 'vitest';
import { createTestDocument } from './TestUtils';
import { FormDetector } from '../FormDetector';
import { createTestDocument } from './TestUtils';
describe('FormDetector.getSuggestedServiceName (English)', () => {
it('should extract service name from title with divider and include domain', () => {
const { document, location } = createTestDocument(

View File

@@ -1,7 +1,9 @@
import { describe, it, expect } from 'vitest';
import { createTestDocument } from './TestUtils';
import { FormDetector } from '../FormDetector';
import { createTestDocument } from './TestUtils';
describe('FormDetector.getSuggestedServiceName (Dutch)', () => {
it('should extract service name from title with divider and include domain', () => {
const { document, location } = createTestDocument(

View File

@@ -1,9 +1,11 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { FormFiller } from '../FormFiller';
import { JSDOM } from 'jsdom';
import { setupTestDOM, createMockFormFields, createMockCredential, wasTriggerCalledFor, createDateSelects } from './TestUtils';
import { FormFields } from '../types/FormFields';
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { Credential } from '../../types/Credential';
import { FormFiller } from '../FormFiller';
import { FormFields } from '../types/FormFields';
import { setupTestDOM, createMockFormFields, createMockCredential, wasTriggerCalledFor, createDateSelects } from './TestUtils';
const { window } = new JSDOM('<!DOCTYPE html>');
global.HTMLSelectElement = window.HTMLSelectElement;

View File

@@ -1,9 +1,11 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { FormFiller } from '../FormFiller';
import { JSDOM } from 'jsdom';
import { setupTestDOM, createMockFormFields, createMockCredential, wasTriggerCalledFor, createDateSelects } from './TestUtils';
import { FormFields } from '../types/FormFields';
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { Credential } from '../../types/Credential';
import { FormFiller } from '../FormFiller';
import { FormFields } from '../types/FormFields';
import { setupTestDOM, createMockFormFields, createMockCredential, wasTriggerCalledFor, createDateSelects } from './TestUtils';
const { window } = new JSDOM('<!DOCTYPE html>');
global.HTMLSelectElement = window.HTMLSelectElement;

View File

@@ -1,9 +1,11 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { FormFiller } from '../FormFiller';
import { JSDOM } from 'jsdom';
import { setupTestDOM, createMockFormFields, createMockCredential, wasTriggerCalledFor, createDateSelects } from './TestUtils';
import { FormFields } from '../types/FormFields';
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { Credential } from '../../types/Credential';
import { FormFiller } from '../FormFiller';
import { FormFields } from '../types/FormFields';
import { setupTestDOM, createMockFormFields, createMockCredential, wasTriggerCalledFor, createDateSelects } from './TestUtils';
const { window } = new JSDOM('<!DOCTYPE html>');
global.HTMLSelectElement = window.HTMLSelectElement;

View File

@@ -1,11 +1,13 @@
import { FormDetector } from '@/utils/formDetector/FormDetector';
import { readFileSync } from 'fs';
import { join } from 'path';
import { it, expect, vi } from 'vitest';
import { JSDOM, DOMWindow } from 'jsdom';
import { it, expect, vi } from 'vitest';
import { FormDetector } from '@/utils/formDetector/FormDetector';
import { FormFields } from '@/utils/formDetector/types/FormFields';
import { Credential } from '@/utils/types/Credential';
import { Gender } from '@/utils/shared/identity-generator';
import { Credential } from '@/utils/types/Credential';
export enum FormField {
Username = 'username',

View File

@@ -299,4 +299,12 @@ type VaultPasswordChangeRequest = Vault & {
newPasswordVerifier: string;
};
export type { Attachment, AuthLogModel, DeleteAccountInitiateRequest, DeleteAccountInitiateResponse, DeleteAccountRequest, Email, FaviconExtractModel, LoginRequest, LoginResponse, MailboxBulkRequest, MailboxBulkResponse, MailboxEmail, PasswordChangeInitiateResponse, RefreshToken, StatusResponse, ValidateLoginRequest, ValidateLoginRequest2Fa, ValidateLoginResponse, Vault, VaultPasswordChangeRequest, VaultPostResponse, VaultResponse };
type BadRequestResponse = {
type: string;
title: string;
status: number;
errors: Record<string, string[]>;
traceId: string;
};
export type { Attachment, AuthLogModel, BadRequestResponse, DeleteAccountInitiateRequest, DeleteAccountInitiateResponse, DeleteAccountRequest, Email, FaviconExtractModel, LoginRequest, LoginResponse, MailboxBulkRequest, MailboxBulkResponse, MailboxEmail, PasswordChangeInitiateResponse, RefreshToken, StatusResponse, ValidateLoginRequest, ValidateLoginRequest2Fa, ValidateLoginResponse, Vault, VaultPasswordChangeRequest, VaultPostResponse, VaultResponse };

View File

@@ -1,19 +0,0 @@
/**
* Email attachment type.
*/
export type Attachment = {
/** The ID of the attachment */
id: number;
/** The ID of the email the attachment belongs to */
emailId: number;
/** The filename of the attachment */
filename: string;
/** The MIME type of the attachment */
mimeType: string;
/** The size of the attachment in bytes */
filesize: number;
}

View File

@@ -1,9 +0,0 @@
type BadRequestResponse = {
type: string;
title: string;
status: number;
errors: Record<string, string[]>;
traceId: string;
};
export default BadRequestResponse;

View File

@@ -1,51 +0,0 @@
import { Attachment } from "./Attachment";
export type Email = {
/** The body of the email message */
messageHtml: string;
/** The plain text body of the email message */
messagePlain: string;
/** The ID of the email */
id: number;
/** The subject of the email */
subject: string;
/** The display name of the sender */
fromDisplay: string;
/** The domain of the sender's email address */
fromDomain: string;
/** The local part of the sender's email address */
fromLocal: string;
/** The domain of the recipient's email address */
toDomain: string;
/** The local part of the recipient's email address */
toLocal: string;
/** The date of the email */
date: string;
/** The system date of the email */
dateSystem: string;
/** The number of seconds ago the email was received */
secondsAgo: number;
/**
* The encrypted symmetric key which was used to encrypt the email message.
* This key is encrypted with the public key of the user.
*/
encryptedSymmetricKey: string;
/** The public key of the user used to encrypt the symmetric key */
encryptionKey: string;
/** The attachments of the email */
attachments: Attachment[];
}

View File

@@ -1,16 +0,0 @@
/**
* Login request type.
*/
export type LoginRequest = {
username: string;
}
/**
* Login response type.
*/
export type LoginResponse = {
salt: string;
serverEphemeral: string;
encryptionType: string;
encryptionSettings: string;
}

View File

@@ -1,21 +0,0 @@
import { MailboxEmail } from "./MailboxEmail";
/**
* Mailbox bulk request type.
*/
export type MailboxBulkRequest = {
addresses: string[];
page: number;
pageSize: number;
}
/**
* Mailbox bulk response type.
*/
export type MailboxBulkResponse = {
addresses: string[];
currentPage: number;
pageSize: number;
totalRecords: number;
mails: MailboxEmail[];
}

View File

@@ -1,46 +0,0 @@
export type MailboxEmail = {
/** The preview of the email message */
messagePreview: string;
/** Indicates whether the email has attachments */
hasAttachments: boolean;
/** The ID of the email */
id: number;
/** The subject of the email */
subject: string;
/** The display name of the sender */
fromDisplay: string;
/** The domain of the sender's email address */
fromDomain: string;
/** The local part of the sender's email address */
fromLocal: string;
/** The domain of the recipient's email address */
toDomain: string;
/** The local part of the recipient's email address */
toLocal: string;
/** The date of the email */
date: string;
/** The system date of the email */
dateSystem: string;
/** The number of seconds ago the email was received */
secondsAgo: number;
/**
* The encrypted symmetric key which was used to encrypt the email message.
* This key is encrypted with the public key of the user.
*/
encryptedSymmetricKey: string;
/** The public key of the user used to encrypt the symmetric key */
encryptionKey: string;
}

View File

@@ -1,8 +0,0 @@
/**
* Status response type.
*/
export type StatusResponse = {
clientVersionSupported: boolean;
serverVersion: string;
vaultRevision: number;
}

View File

@@ -1,32 +0,0 @@
/**
* Validate login request type.
*/
export type ValidateLoginRequest = {
username: string;
rememberMe: boolean;
clientPublicEphemeral: string;
clientSessionProof: string;
}
/**
* Validate login request type for 2FA.
*/
export type ValidateLoginRequest2Fa = {
username: string;
code2Fa: number;
rememberMe: boolean;
clientPublicEphemeral: string;
clientSessionProof: string;
}
/**
* Validate login response type.
*/
export type ValidateLoginResponse = {
requiresTwoFactor: boolean;
token?: {
token: string;
refreshToken: string;
};
serverSessionProof: string;
}

View File

@@ -1,17 +0,0 @@
/**
* Vault type.
*/
export type Vault = {
blob: string;
createdAt: string;
credentialsCount: number;
currentRevisionNumber: number;
emailAddressList: string[];
privateEmailDomainList: string[];
publicEmailDomainList: string[];
encryptionPublicKey: string;
updatedAt: string;
username: string;
version: string;
client: string;
}

View File

@@ -1,7 +0,0 @@
/**
* Vault post response type returned after uploading a new vault to the server.
*/
export type VaultPostResponse = {
status: number;
newRevisionNumber: number;
}

View File

@@ -1,9 +0,0 @@
import { Vault } from "./Vault";
/**
* Vault response type.
*/
export type VaultResponse = {
status: number;
vault: Vault;
}

View File

@@ -94,7 +94,44 @@ module.exports = {
// Import
"import/no-unresolved": "error",
"import/order": ["error", { "newlines-between": "always" }],
"import/order": [
"error",
{
"groups": [
"builtin", // Node "fs", "path", etc.
"external", // "react", "lodash", etc.
"internal", // Aliased paths like "@/utils"
"parent", // "../"
"sibling", // "./"
"index", // "./index"
"object", // import 'foo'
"type" // import type ...
],
"pathGroups": [
{
pattern: "@/entrypoints/**",
group: "internal",
position: "before"
},
{
pattern: "@/utils/**",
group: "internal",
position: "before"
},
{
pattern: "@/hooks/**",
group: "internal",
position: "before"
}
],
"pathGroupsExcludedImportTypes": ["builtin"],
"newlines-between": "always",
"alphabetize": {
order: "asc",
caseInsensitive: true
}
}
],
// JSDoc
"jsdoc/require-jsdoc": [

View File

@@ -16,3 +16,4 @@ export * from './webapi/DeleteAccountInitiate';
export * from './webapi/DeleteAccountRequest';
export * from './webapi/PasswordChangeInitiateResponse';
export * from './webapi/VaultPasswordChangeRequest';
export * from './webapi/BadRequestResponse';

View File

@@ -1,9 +1,7 @@
type BadRequestResponse = {
export type BadRequestResponse = {
type: string;
title: string;
status: number;
errors: Record<string, string[]>;
traceId: string;
};
export default BadRequestResponse;