diff --git a/.env.example b/.env.example
index 1f475fa73..96d0f4f45 100644
--- a/.env.example
+++ b/.env.example
@@ -37,11 +37,19 @@ FORCE_HTTPS_REDIRECT=true
# your DNS. Please refer to the full documentation for more instructions on DNS:
# https://docs.aliasvault.net/installation/install.html#3-email-server-setup
#
-# Set the private email domains below that are allowed to be used (comma separated values).
+# Set the private email domains below that the server should accept incoming mail for (comma separated values).
# Example: PRIVATE_EMAIL_DOMAINS=example.com,example2.org
# To disable the private email domains feature, keep this empty.
PRIVATE_EMAIL_DOMAINS=
+# Set private email domains that should be hidden from UI components (comma separated values).
+# These domains will still function as private email domains for receiving email and claims,
+# but will not appear in domain selection dropdowns or settings. This is useful for deprecating
+# legacy domains while maintaining backwards compatibility.
+# Example: HIDDEN_PRIVATE_EMAIL_DOMAINS=old-domain.com,deprecated.org
+# Note: Domains listed here should ALSO be included in PRIVATE_EMAIL_DOMAINS above.
+HIDDEN_PRIVATE_EMAIL_DOMAINS=
+
# Enable TLS for SMTP.
# ⚠️ Requires valid TLS certificates on your mail server (not provided by the AliasVault installer).
# If set to true without proper certificates, the SMTP service will fail to start.
diff --git a/apps/browser-extension/src/entrypoints/background/VaultMessageHandler.ts b/apps/browser-extension/src/entrypoints/background/VaultMessageHandler.ts
index d4fda43f0..6b3c1189e 100644
--- a/apps/browser-extension/src/entrypoints/background/VaultMessageHandler.ts
+++ b/apps/browser-extension/src/entrypoints/background/VaultMessageHandler.ts
@@ -105,6 +105,10 @@ export async function handleStoreVault(
await storage.setItem('session:privateEmailDomains', vaultRequest.privateEmailDomainList);
}
+ if (vaultRequest.hiddenPrivateEmailDomainList) {
+ await storage.setItem('session:hiddenPrivateEmailDomains', vaultRequest.hiddenPrivateEmailDomainList);
+ }
+
if (vaultRequest.vaultRevisionNumber) {
await storage.setItem('session:vaultRevisionNumber', vaultRequest.vaultRevisionNumber);
}
@@ -168,6 +172,7 @@ export async function handleSyncVault(
{ key: 'session:encryptedVault', value: vaultResponse.vault.blob },
{ key: 'session:publicEmailDomains', value: vaultResponse.vault.publicEmailDomainList },
{ key: 'session:privateEmailDomains', value: vaultResponse.vault.privateEmailDomainList },
+ { key: 'session:hiddenPrivateEmailDomains', value: vaultResponse.vault.hiddenPrivateEmailDomainList },
{ key: 'session:vaultRevisionNumber', value: vaultResponse.vault.currentRevisionNumber }
]);
}
@@ -186,6 +191,7 @@ export async function handleGetVault(
const encryptedVault = await storage.getItem('session:encryptedVault') as string;
const publicEmailDomains = await storage.getItem('session:publicEmailDomains') as string[];
const privateEmailDomains = await storage.getItem('session:privateEmailDomains') as string[];
+ const hiddenPrivateEmailDomains = await storage.getItem('session:hiddenPrivateEmailDomains') as string[] ?? [];
const vaultRevisionNumber = await storage.getItem('session:vaultRevisionNumber') as number;
if (!encryptedVault) {
@@ -208,6 +214,7 @@ export async function handleGetVault(
vault: decryptedVault,
publicEmailDomains: publicEmailDomains ?? [],
privateEmailDomains: privateEmailDomains ?? [],
+ hiddenPrivateEmailDomains: hiddenPrivateEmailDomains ?? [],
vaultRevisionNumber: vaultRevisionNumber ?? 0
};
} catch (error) {
@@ -229,6 +236,7 @@ export function handleClearVault(
'session:encryptionKeyDerivationParams',
'session:publicEmailDomains',
'session:privateEmailDomains',
+ 'session:hiddenPrivateEmailDomains',
'session:vaultRevisionNumber'
]);
@@ -497,13 +505,11 @@ async function uploadNewVaultToServer(sqliteClient: SqliteClient) : Promise {});
diff --git a/apps/browser-extension/src/entrypoints/popup/components/Forms/EmailDomainField.tsx b/apps/browser-extension/src/entrypoints/popup/components/Forms/EmailDomainField.tsx
index 032cc409c..2096b2348 100644
--- a/apps/browser-extension/src/entrypoints/popup/components/Forms/EmailDomainField.tsx
+++ b/apps/browser-extension/src/entrypoints/popup/components/Forms/EmailDomainField.tsx
@@ -45,18 +45,18 @@ const EmailDomainField: React.FC = ({
const [selectedDomain, setSelectedDomain] = useState('');
const [isPopupVisible, setIsPopupVisible] = useState(false);
const [privateEmailDomains, setPrivateEmailDomains] = useState([]);
+ const [hiddenPrivateEmailDomains, setHiddenPrivateEmailDomains] = useState([]);
const popupRef = useRef(null);
// Get private email domains from vault metadata
useEffect(() => {
/**
- * Load private email domains from vault metadata.
+ * Load private email domains from vault metadata, excluding hidden ones.
*/
const loadDomains = async (): Promise => {
const metadata = await dbContext.getVaultMetadata();
- if (metadata?.privateEmailDomains) {
- setPrivateEmailDomains(metadata.privateEmailDomains);
- }
+ setPrivateEmailDomains(metadata?.privateEmailDomains ?? []);
+ setHiddenPrivateEmailDomains(metadata?.hiddenPrivateEmailDomains ?? []);
};
loadDomains();
}, [dbContext]);
@@ -84,9 +84,10 @@ const EmailDomainField: React.FC = ({
setLocalPart(local);
setSelectedDomain(domain);
- // Check if it's a custom domain
+ // Check if it's a custom domain (including hidden private domains as known domains)
const isKnownDomain = PUBLIC_EMAIL_DOMAINS.includes(domain) ||
- privateEmailDomains.includes(domain);
+ privateEmailDomains.includes(domain) ||
+ hiddenPrivateEmailDomains.includes(domain);
setIsCustomDomain(!isKnownDomain);
} else {
setLocalPart(value);
@@ -102,7 +103,7 @@ const EmailDomainField: React.FC = ({
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
- }, [value, privateEmailDomains, showPrivateDomains]);
+ }, [value, privateEmailDomains, hiddenPrivateEmailDomains, showPrivateDomains]);
// Handle local part changes
const handleLocalPartChange = useCallback((e: React.ChangeEvent) => {
@@ -246,20 +247,22 @@ const EmailDomainField: React.FC = ({
{t('credentials.privateEmailDescription')}
- {privateEmailDomains.map((domain) => (
-
- ))}
+ {privateEmailDomains
+ .filter((domain) => !hiddenPrivateEmailDomains.includes(domain))
+ .map((domain) => (
+
+ ))}
)}
diff --git a/apps/browser-extension/src/entrypoints/popup/context/DbContext.tsx b/apps/browser-extension/src/entrypoints/popup/context/DbContext.tsx
index 2e1c34a46..a22cc2b7e 100644
--- a/apps/browser-extension/src/entrypoints/popup/context/DbContext.tsx
+++ b/apps/browser-extension/src/entrypoints/popup/context/DbContext.tsx
@@ -62,8 +62,9 @@ export const DbProvider: React.FC<{ children: React.ReactNode }> = ({ children }
setDbInitialized(true);
setDbAvailable(true);
setVaultMetadata({
- publicEmailDomains: vaultResponse.vault.publicEmailDomainList,
- privateEmailDomains: vaultResponse.vault.privateEmailDomainList,
+ publicEmailDomains: vaultResponse.vault.publicEmailDomainList ?? [],
+ privateEmailDomains: vaultResponse.vault.privateEmailDomainList ?? [],
+ hiddenPrivateEmailDomains: vaultResponse.vault.hiddenPrivateEmailDomainList ?? [],
vaultRevisionNumber: vaultResponse.vault.currentRevisionNumber,
});
@@ -74,6 +75,7 @@ export const DbProvider: React.FC<{ children: React.ReactNode }> = ({ children }
vaultBlob: vaultResponse.vault.blob,
publicEmailDomainList: vaultResponse.vault.publicEmailDomainList,
privateEmailDomainList: vaultResponse.vault.privateEmailDomainList,
+ hiddenPrivateEmailDomainList: vaultResponse.vault.hiddenPrivateEmailDomainList,
vaultRevisionNumber: vaultResponse.vault.currentRevisionNumber,
};
@@ -96,6 +98,7 @@ export const DbProvider: React.FC<{ children: React.ReactNode }> = ({ children }
setVaultMetadata({
publicEmailDomains: response.publicEmailDomains ?? [],
privateEmailDomains: response.privateEmailDomains ?? [],
+ hiddenPrivateEmailDomains: response.hiddenPrivateEmailDomains ?? [],
vaultRevisionNumber: response.vaultRevisionNumber ?? 0,
});
} else {
@@ -123,6 +126,7 @@ export const DbProvider: React.FC<{ children: React.ReactNode }> = ({ children }
setVaultMetadata({
publicEmailDomains: vaultMetadata?.publicEmailDomains ?? [],
privateEmailDomains: vaultMetadata?.privateEmailDomains ?? [],
+ hiddenPrivateEmailDomains: vaultMetadata?.hiddenPrivateEmailDomains ?? [],
vaultRevisionNumber: revisionNumber,
});
}, [vaultMetadata]);
diff --git a/apps/browser-extension/src/utils/dist/shared/models/metadata/index.d.ts b/apps/browser-extension/src/utils/dist/shared/models/metadata/index.d.ts
index e09bb64c7..f15dd4f7c 100644
--- a/apps/browser-extension/src/utils/dist/shared/models/metadata/index.d.ts
+++ b/apps/browser-extension/src/utils/dist/shared/models/metadata/index.d.ts
@@ -1,6 +1,7 @@
type VaultMetadata = {
publicEmailDomains: string[];
privateEmailDomains: string[];
+ hiddenPrivateEmailDomains: string[];
vaultRevisionNumber: number;
};
diff --git a/apps/browser-extension/src/utils/dist/shared/models/webapi/index.d.ts b/apps/browser-extension/src/utils/dist/shared/models/webapi/index.d.ts
index af8a6cbfd..46c8a7754 100644
--- a/apps/browser-extension/src/utils/dist/shared/models/webapi/index.d.ts
+++ b/apps/browser-extension/src/utils/dist/shared/models/webapi/index.d.ts
@@ -28,18 +28,18 @@ type ApiErrorResponse = {
* Vault type.
*/
type Vault = {
- blob: string;
- createdAt: string;
- credentialsCount: number;
- currentRevisionNumber: number;
- emailAddressList: string[];
- privateEmailDomainList: string[];
- publicEmailDomainList: string[];
- encryptionPublicKey: string;
- updatedAt: string;
username: string;
+ blob: string;
version: string;
- client: string;
+ currentRevisionNumber: number;
+ credentialsCount: number;
+ createdAt: string;
+ updatedAt: string;
+ encryptionPublicKey?: string;
+ emailAddressList?: string[];
+ privateEmailDomainList?: string[];
+ hiddenPrivateEmailDomainList?: string[];
+ publicEmailDomainList?: string[];
};
/**
diff --git a/apps/browser-extension/src/utils/types/messaging/StoreVaultRequest.ts b/apps/browser-extension/src/utils/types/messaging/StoreVaultRequest.ts
index 4a1a4421f..830d121fe 100644
--- a/apps/browser-extension/src/utils/types/messaging/StoreVaultRequest.ts
+++ b/apps/browser-extension/src/utils/types/messaging/StoreVaultRequest.ts
@@ -2,5 +2,6 @@ export type StoreVaultRequest = {
vaultBlob: string;
publicEmailDomainList?: string[];
privateEmailDomainList?: string[];
+ hiddenPrivateEmailDomainList?: string[];
vaultRevisionNumber?: number;
}
diff --git a/apps/browser-extension/src/utils/types/messaging/VaultResponse.ts b/apps/browser-extension/src/utils/types/messaging/VaultResponse.ts
index e57e54265..202c0dbd5 100644
--- a/apps/browser-extension/src/utils/types/messaging/VaultResponse.ts
+++ b/apps/browser-extension/src/utils/types/messaging/VaultResponse.ts
@@ -3,5 +3,6 @@ export type VaultResponse = {
vault?: string,
publicEmailDomains?: string[],
privateEmailDomains?: string[],
+ hiddenPrivateEmailDomains?: string[],
vaultRevisionNumber?: number
};
diff --git a/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/vaultstore/VaultMetadataManager.kt b/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/vaultstore/VaultMetadataManager.kt
index 2f71b4ea8..baad47edf 100644
--- a/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/vaultstore/VaultMetadataManager.kt
+++ b/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/vaultstore/VaultMetadataManager.kt
@@ -47,6 +47,7 @@ class VaultMetadataManager(
JSONObject().apply {
put("publicEmailDomains", JSONArray(updatedMetadata.publicEmailDomains))
put("privateEmailDomains", JSONArray(updatedMetadata.privateEmailDomains))
+ put("hiddenPrivateEmailDomains", JSONArray(updatedMetadata.hiddenPrivateEmailDomains))
put("vaultRevisionNumber", updatedMetadata.vaultRevisionNumber)
}.toString(),
)
@@ -158,6 +159,9 @@ class VaultMetadataManager(
privateEmailDomains = json.optJSONArray("privateEmailDomains")?.let { array ->
List(array.length()) { i -> array.getString(i) }
} ?: emptyList(),
+ hiddenPrivateEmailDomains = json.optJSONArray("hiddenPrivateEmailDomains")?.let { array ->
+ List(array.length()) { i -> array.getString(i) }
+ } ?: emptyList(),
vaultRevisionNumber = json.optInt("vaultRevisionNumber", 0),
)
} catch (e: Exception) {
diff --git a/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/vaultstore/VaultMutate.kt b/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/vaultstore/VaultMutate.kt
index daa253321..2c0bdd133 100644
--- a/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/vaultstore/VaultMutate.kt
+++ b/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/vaultstore/VaultMutate.kt
@@ -33,13 +33,10 @@ class VaultMutate(
json.put("credentialsCount", vault.credentialsCount)
json.put("currentRevisionNumber", vault.currentRevisionNumber)
json.put("emailAddressList", JSONArray(vault.emailAddressList))
- json.put("privateEmailDomainList", JSONArray(vault.privateEmailDomainList))
- json.put("publicEmailDomainList", JSONArray(vault.publicEmailDomainList))
json.put("encryptionPublicKey", vault.encryptionPublicKey)
json.put("updatedAt", vault.updatedAt)
json.put("username", vault.username)
json.put("version", vault.version)
- json.put("client", vault.client)
val response = webApiService.executeRequest(
method = "POST",
@@ -124,8 +121,6 @@ class VaultMutate(
} catch (e: Exception) {
"0.0.0"
}
- val baseVersion = version.split("-").firstOrNull() ?: "0.0.0"
- val client = "android-$baseVersion"
val dateFormat = java.text.SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", java.util.Locale.US)
dateFormat.timeZone = java.util.TimeZone.getTimeZone("UTC")
@@ -137,13 +132,11 @@ class VaultMutate(
credentialsCount = credentials.size,
currentRevisionNumber = currentRevision,
emailAddressList = privateEmailAddresses,
- privateEmailDomainList = emptyList(),
- publicEmailDomainList = emptyList(),
+ // TODO: add public RSA encryption key to payload when implementing vault creation from mobile app. Currently only web app does this.
encryptionPublicKey = "",
updatedAt = now,
username = username,
version = dbVersion,
- client = client,
)
}
@@ -157,13 +150,10 @@ class VaultMutate(
val credentialsCount: Int,
val currentRevisionNumber: Int,
val emailAddressList: List,
- val privateEmailDomainList: List,
- val publicEmailDomainList: List,
val encryptionPublicKey: String,
val updatedAt: String,
val username: String,
val version: String,
- val client: String,
)
private data class VaultPostResponse(
diff --git a/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/vaultstore/VaultSync.kt b/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/vaultstore/VaultSync.kt
index f8fac296f..5facc7726 100644
--- a/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/vaultstore/VaultSync.kt
+++ b/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/vaultstore/VaultSync.kt
@@ -173,12 +173,34 @@ class VaultSync(
database.storeEncryptedDatabase(vault.vault.blob)
metadata.setVaultRevisionNumber(newRevision)
+ // Store vault metadata (public/private email domains)
+ val vaultMetadata = net.aliasvault.app.vaultstore.models.VaultMetadata(
+ publicEmailDomains = vault.vault.publicEmailDomainList,
+ privateEmailDomains = vault.vault.privateEmailDomainList,
+ hiddenPrivateEmailDomains = vault.vault.hiddenPrivateEmailDomainList,
+ vaultRevisionNumber = newRevision,
+ )
+ storeVaultMetadata(vaultMetadata)
+
if (database.isVaultUnlocked()) {
// Re-unlock with new data
// Note: This requires auth methods to be passed, handled by VaultStore
}
}
+ /**
+ * Store vault metadata as JSON string.
+ */
+ private fun storeVaultMetadata(vaultMetadata: net.aliasvault.app.vaultstore.models.VaultMetadata) {
+ val json = JSONObject().apply {
+ put("publicEmailDomains", org.json.JSONArray(vaultMetadata.publicEmailDomains))
+ put("privateEmailDomains", org.json.JSONArray(vaultMetadata.privateEmailDomains))
+ put("hiddenPrivateEmailDomains", org.json.JSONArray(vaultMetadata.hiddenPrivateEmailDomains))
+ put("vaultRevisionNumber", vaultMetadata.vaultRevisionNumber)
+ }
+ metadata.storeMetadata(json.toString())
+ }
+
private fun parseVaultResponse(body: String): VaultResponse {
return try {
val json = JSONObject(body)
@@ -196,6 +218,12 @@ class VaultSync(
privateList.add(privateArray.getString(i))
}
+ val hiddenPrivateList = mutableListOf()
+ val hiddenPrivateArray = vaultJson.getJSONArray("hiddenPrivateEmailDomainList")
+ for (i in 0 until hiddenPrivateArray.length()) {
+ hiddenPrivateList.add(hiddenPrivateArray.getString(i))
+ }
+
val publicList = mutableListOf()
val publicArray = vaultJson.getJSONArray("publicEmailDomainList")
for (i in 0 until publicArray.length()) {
@@ -213,6 +241,7 @@ class VaultSync(
credentialsCount = vaultJson.getInt("credentialsCount"),
emailAddressList = emailList,
privateEmailDomainList = privateList,
+ hiddenPrivateEmailDomainList = hiddenPrivateList,
publicEmailDomainList = publicList,
createdAt = vaultJson.getString("createdAt"),
updatedAt = vaultJson.getString("updatedAt"),
@@ -253,6 +282,7 @@ class VaultSync(
val credentialsCount: Int,
val emailAddressList: List,
val privateEmailDomainList: List,
+ val hiddenPrivateEmailDomainList: List,
val publicEmailDomainList: List,
val createdAt: String,
val updatedAt: String,
diff --git a/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/vaultstore/models/VaultMetadata.kt b/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/vaultstore/models/VaultMetadata.kt
index dc5a89759..974572f4e 100644
--- a/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/vaultstore/models/VaultMetadata.kt
+++ b/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/vaultstore/models/VaultMetadata.kt
@@ -14,6 +14,12 @@ data class VaultMetadata(
*/
val privateEmailDomains: List = emptyList(),
+ /**
+ * The hidden private email domains of the vault.
+ * These domains still function as private email domains but are hidden from UI components.
+ */
+ val hiddenPrivateEmailDomains: List = emptyList(),
+
/**
* The revision number of the vault.
*/
diff --git a/apps/mobile-app/android/app/src/test/java/net/aliasvault/app/nativevaultmanager/VaultStoreTest.kt b/apps/mobile-app/android/app/src/test/java/net/aliasvault/app/nativevaultmanager/VaultStoreTest.kt
index 830162677..bd8bb4202 100644
--- a/apps/mobile-app/android/app/src/test/java/net/aliasvault/app/nativevaultmanager/VaultStoreTest.kt
+++ b/apps/mobile-app/android/app/src/test/java/net/aliasvault/app/nativevaultmanager/VaultStoreTest.kt
@@ -35,7 +35,8 @@ class VaultStoreTest {
val metadata = """
{
"publicEmailDomains": ["spamok.com", "spamok.nl"],
- "privateEmailDomains": ["aliasvault.net", "main.aliasvault.net"],
+ "privateEmailDomains": ["aliasvault.net", "main.aliasvault.net", "hidden.aliasvault.net"],
+ "hiddenPrivateEmailDomains": ["hidden.aliasvault.net"],
"vaultRevisionNumber": 1
}
"""
diff --git a/apps/mobile-app/app/initialize.tsx b/apps/mobile-app/app/initialize.tsx
index e215cdd9d..f6da98cd8 100644
--- a/apps/mobile-app/app/initialize.tsx
+++ b/apps/mobile-app/app/initialize.tsx
@@ -261,7 +261,6 @@ export default function Initialize() : React.ReactNode {
// Now perform vault sync (network operations - these are skippable)
await syncVault({
- initialSync: true,
abortSignal: abortControllerRef.current.signal,
/**
* Handle the status update.
diff --git a/apps/mobile-app/app/login.tsx b/apps/mobile-app/app/login.tsx
index 3a261c735..6eba26797 100644
--- a/apps/mobile-app/app/login.tsx
+++ b/apps/mobile-app/app/login.tsx
@@ -10,7 +10,7 @@ import { StyleSheet, View, Text, SafeAreaView, TextInput, ActivityIndicator, Ani
import { useApiUrl } from '@/utils/ApiUrlUtility';
import ConversionUtility from '@/utils/ConversionUtility';
import type { EncryptionKeyDerivationParams } from '@/utils/dist/shared/models/metadata';
-import type { LoginResponse, VaultResponse } from '@/utils/dist/shared/models/webapi';
+import type { LoginResponse } from '@/utils/dist/shared/models/webapi';
import EncryptionUtility from '@/utils/EncryptionUtility';
import { SrpUtility } from '@/utils/SrpUtility';
import { ApiAuthError } from '@/utils/types/errors/ApiAuthError';
@@ -77,14 +77,12 @@ export default function LoginScreen() : React.ReactNode {
* Process the vault response by storing the vault and logging in the user.
* @param token - The token to use for the vault
* @param refreshToken - The refresh token to use for the vault
- * @param vaultResponseJson - The vault response
* @param passwordHashBase64 - The password hash base64
* @param initiateLoginResponse - The initiate login response
*/
const processVaultResponse = async (
token: string,
refreshToken: string,
- vaultResponseJson: VaultResponse,
passwordHashBase64: string,
initiateLoginResponse: LoginResponse
) : Promise => {
@@ -109,7 +107,6 @@ export default function LoginScreen() : React.ReactNode {
await continueProcessVaultResponse(
token,
refreshToken,
- vaultResponseJson,
passwordHashBase64,
initiateLoginResponse
);
@@ -126,7 +123,6 @@ export default function LoginScreen() : React.ReactNode {
await continueProcessVaultResponse(
token,
refreshToken,
- vaultResponseJson,
passwordHashBase64,
initiateLoginResponse
);
@@ -140,7 +136,6 @@ export default function LoginScreen() : React.ReactNode {
await continueProcessVaultResponse(
token,
refreshToken,
- vaultResponseJson,
passwordHashBase64,
initiateLoginResponse
);
@@ -151,7 +146,6 @@ export default function LoginScreen() : React.ReactNode {
* Continue processing the vault response after biometric choice
* @param token - The token to use for the vault
* @param refreshToken - The refresh token to use for the vault
- * @param vaultResponseJson - The vault response
* @param passwordHashBase64 - The password hash base64
* @param initiateLoginResponse - The initiate login response
* @param encryptionKeyDerivationParams - The encryption key derivation parameters
@@ -159,7 +153,6 @@ export default function LoginScreen() : React.ReactNode {
const continueProcessVaultResponse = async (
token: string,
refreshToken: string,
- vaultResponseJson: VaultResponse,
passwordHashBase64: string,
initiateLoginResponse: LoginResponse
) : Promise => {
@@ -169,20 +162,29 @@ export default function LoginScreen() : React.ReactNode {
salt: initiateLoginResponse.salt,
};
- // Set auth tokens, store encryption key and key derivation params, and initialize database
+ /*
+ * Set auth tokens, store encryption key and key derivation params.
+ * Note: We don't call initializeDatabase here anymore - instead, syncVault will download
+ * the vault and store it (including metadata) through native code.
+ */
await authContext.setAuthTokens(ConversionUtility.normalizeUsername(credentials.username), token, refreshToken);
await dbContext.storeEncryptionKey(passwordHashBase64);
await dbContext.storeEncryptionKeyDerivationParams(encryptionKeyDerivationParams);
- await dbContext.initializeDatabase(vaultResponseJson);
let checkSuccess = true;
/**
* After setting auth tokens, execute a server status check immediately
* which takes care of certain sanity checks such as ensuring client/server
- * compatibility.
+ * compatibility. This also downloads the vault and stores it (including metadata)
+ * through native code.
*/
await syncVault({
- initialSync: true,
+ /**
+ * Update login status during sync.
+ */
+ onStatus: (status) => {
+ setLoginStatus(status);
+ },
/**
* Handle the status update.
*/
@@ -218,6 +220,12 @@ export default function LoginScreen() : React.ReactNode {
return;
}
+ /*
+ * After syncVault completes, the vault has been downloaded and stored by native code.
+ * Immediately mark the database as available without file system checks for faster bootstrap.
+ */
+ dbContext.setDatabaseAvailable();
+
await authContext.login();
authContext.setOfflineMode(false);
@@ -286,23 +294,10 @@ export default function LoginScreen() : React.ReactNode {
setLoginStatus(t('auth.syncingVault'));
await new Promise(resolve => requestAnimationFrame(resolve));
- const vaultResponseJson = await webApi.authFetch('Vault', { method: 'GET', headers: {
- 'Authorization': `Bearer ${validationResponse.token.token}`
- } });
-
- const vaultError = webApi.validateVaultResponse(vaultResponseJson);
- if (vaultError) {
- console.error('vaultError', vaultError);
- setError(vaultError);
- setIsLoading(false);
- setLoginStatus(null);
- return;
- }
await processVaultResponse(
validationResponse.token.token,
validationResponse.token.refreshToken,
- vaultResponseJson,
passwordHashBase64,
initiateLoginResponse
);
@@ -357,21 +352,10 @@ export default function LoginScreen() : React.ReactNode {
setLoginStatus(t('auth.syncingVault'));
await new Promise(resolve => requestAnimationFrame(resolve));
- const vaultResponseJson = await webApi.authFetch('Vault', { method: 'GET', headers: {
- 'Authorization': `Bearer ${validationResponse.token.token}`
- } });
-
- const vaultError = webApi.validateVaultResponse(vaultResponseJson);
- if (vaultError) {
- setError(vaultError);
- setIsLoading(false);
- return;
- }
await processVaultResponse(
validationResponse.token.token,
validationResponse.token.refreshToken,
- vaultResponseJson,
passwordHashBase64,
initiateLoginResponse
);
diff --git a/apps/mobile-app/app/reinitialize.tsx b/apps/mobile-app/app/reinitialize.tsx
index 0b2b46b30..c35771c43 100644
--- a/apps/mobile-app/app/reinitialize.tsx
+++ b/apps/mobile-app/app/reinitialize.tsx
@@ -245,7 +245,6 @@ export default function ReinitializeScreen() : React.ReactNode {
// Now perform vault sync (network operations - these are skippable)
await syncVault({
- initialSync: true,
/**
* Handle the status update.
*/
diff --git a/apps/mobile-app/components/credentials/details/EmailPreview.tsx b/apps/mobile-app/components/credentials/details/EmailPreview.tsx
index e24d62a0e..0441c52fb 100644
--- a/apps/mobile-app/components/credentials/details/EmailPreview.tsx
+++ b/apps/mobile-app/components/credentials/details/EmailPreview.tsx
@@ -75,7 +75,7 @@ export const EmailPreview: React.FC = ({ email }) : React.Rea
}, [dbContext]);
/**
- * Check if the email is a private domain.
+ * Check if the email is a private domain (including hidden domains).
*/
const isPrivateDomain = useCallback(async (emailAddress: string): Promise => {
// Get private domains from stored metadata
@@ -84,7 +84,9 @@ export const EmailPreview: React.FC = ({ email }) : React.Rea
return false;
}
- return metadata.privateEmailDomains.includes(emailAddress.split('@')[1]);
+ const domain = emailAddress.split('@')[1];
+ return metadata.privateEmailDomains.includes(domain) ||
+ (metadata.hiddenPrivateEmailDomains || []).includes(domain);
}, [dbContext]);
// Handle app state changes
diff --git a/apps/mobile-app/components/form/EmailDomainField.tsx b/apps/mobile-app/components/form/EmailDomainField.tsx
index e3df6cdca..8d083d3c0 100644
--- a/apps/mobile-app/components/form/EmailDomainField.tsx
+++ b/apps/mobile-app/components/form/EmailDomainField.tsx
@@ -49,18 +49,21 @@ export const EmailDomainField: React.FC = ({
const [selectedDomain, setSelectedDomain] = useState('');
const [isModalVisible, setIsModalVisible] = useState(false);
const [privateEmailDomains, setPrivateEmailDomains] = useState([]);
+ const [hiddenPrivateEmailDomains, setHiddenPrivateEmailDomains] = useState([]);
// Get private email domains from vault metadata
useEffect(() => {
/**
- * Load private email domains from vault metadata.
+ * Load private email domains from vault metadata, excluding hidden ones from UI.
*/
const loadDomains = async (): Promise => {
try {
const metadata = await dbContext.getVaultMetadata();
- if (metadata?.privateEmailDomains) {
- setPrivateEmailDomains(metadata.privateEmailDomains);
- }
+ setPrivateEmailDomains(metadata?.privateEmailDomains ?? []);
+ setHiddenPrivateEmailDomains(metadata?.hiddenPrivateEmailDomains ?? []);
+
+ console.log('privateEmailDomains', metadata?.privateEmailDomains);
+ console.log('hiddenPrivateEmailDomains', metadata?.hiddenPrivateEmailDomains);
} catch (err) {
console.error('Error loading email domains:', err);
}
@@ -91,9 +94,10 @@ export const EmailDomainField: React.FC = ({
setLocalPart(local);
setSelectedDomain(domain);
- // Check if it's a custom domain
+ // Check if it's a custom domain (including hidden private domains as known domains)
const isKnownDomain = PUBLIC_EMAIL_DOMAINS.includes(domain) ||
- privateEmailDomains.includes(domain);
+ privateEmailDomains.includes(domain) ||
+ hiddenPrivateEmailDomains.includes(domain);
setIsCustomDomain(!isKnownDomain);
} else {
setLocalPart(value);
@@ -108,7 +112,7 @@ export const EmailDomainField: React.FC = ({
}
}
}
- }, [value, privateEmailDomains, showPrivateDomains]);
+ }, [value, privateEmailDomains, hiddenPrivateEmailDomains, showPrivateDomains]);
// Handle local part changes
const handleLocalPartChange = useCallback((newText: string) => {
@@ -410,7 +414,7 @@ export const EmailDomainField: React.FC = ({
{t('credentials.privateEmailDescription')}
- {privateEmailDomains.map((domain) => (
+ {privateEmailDomains.filter(domain => !hiddenPrivateEmailDomains.includes(domain)).map((domain) => (
Promise;
testDatabaseConnection: (derivedKey: string) => Promise;
unlockVault: () => Promise;
+ checkStoredVault: () => Promise;
+ setDatabaseAvailable: () => void;
}
const DbContext = createContext(undefined);
@@ -80,13 +82,16 @@ export const DbProvider: React.FC<{ children: React.ReactNode }> = ({ children }
/**
* Initialize the database in the native module.
+ * This is called during initial login/registration to set up the vault.
+ * Note: During sync operations, metadata is stored automatically by native VaultSync.
*
* @param vaultResponse The vault response from the API
*/
const initializeDatabase = useCallback(async (vaultResponse: VaultResponse) => {
const metadata: VaultMetadata = {
- publicEmailDomains: vaultResponse.vault.publicEmailDomainList,
- privateEmailDomains: vaultResponse.vault.privateEmailDomainList,
+ publicEmailDomains: vaultResponse.vault.publicEmailDomainList ?? [],
+ privateEmailDomains: vaultResponse.vault.privateEmailDomainList ?? [],
+ hiddenPrivateEmailDomains: vaultResponse.vault.hiddenPrivateEmailDomainList ?? [],
vaultRevisionNumber: vaultResponse.vault.currentRevisionNumber,
};
@@ -94,7 +99,7 @@ export const DbProvider: React.FC<{ children: React.ReactNode }> = ({ children }
await sqliteClient.storeEncryptedDatabase(vaultResponse.vault.blob);
await sqliteClient.storeMetadata(JSON.stringify(metadata));
- // Initialize the database in the native module
+ // Unlock the vault to make it available for queries
await unlockVault();
setDbInitialized(true);
@@ -156,6 +161,15 @@ export const DbProvider: React.FC<{ children: React.ReactNode }> = ({ children }
NativeVaultManager.clearVault();
}, []);
+ /**
+ * Manually set the database as available. Used after vault sync to immediately
+ * mark the database as ready without file system checks.
+ */
+ const setDatabaseAvailable = useCallback(() : void => {
+ setDbInitialized(true);
+ setDbAvailable(true);
+ }, []);
+
/**
* Get the current vault metadata directly from SQLite client
*/
@@ -199,7 +213,9 @@ export const DbProvider: React.FC<{ children: React.ReactNode }> = ({ children }
unlockVault,
storeEncryptionKey,
storeEncryptionKeyDerivationParams,
- }), [sqliteClient, dbInitialized, dbAvailable, initializeDatabase, hasPendingMigrations, clearDatabase, getVaultMetadata, testDatabaseConnection, unlockVault, storeEncryptionKey, storeEncryptionKeyDerivationParams]);
+ checkStoredVault,
+ setDatabaseAvailable,
+ }), [sqliteClient, dbInitialized, dbAvailable, initializeDatabase, hasPendingMigrations, clearDatabase, getVaultMetadata, testDatabaseConnection, unlockVault, storeEncryptionKey, storeEncryptionKeyDerivationParams, checkStoredVault, setDatabaseAvailable]);
return (
diff --git a/apps/mobile-app/context/NavigationContext.tsx b/apps/mobile-app/context/NavigationContext.tsx
index d274d6b18..7b290690a 100644
--- a/apps/mobile-app/context/NavigationContext.tsx
+++ b/apps/mobile-app/context/NavigationContext.tsx
@@ -186,12 +186,10 @@ export const NavigationProvider: React.FC<{ children: React.ReactNode }> = ({ ch
}
// Database connection failed, navigate to reinitialize flow
- console.log('database connection failed, navigating to reinitialize');
router.replace('/reinitialize');
}
} catch {
// Database query failed, navigate to reinitialize flow
- console.log('database query failed, navigating to reinitialize');
router.replace('/reinitialize');
}
}
diff --git a/apps/mobile-app/hooks/useVaultMutate.ts b/apps/mobile-app/hooks/useVaultMutate.ts
index 91272537f..8d72d9354 100644
--- a/apps/mobile-app/hooks/useVaultMutate.ts
+++ b/apps/mobile-app/hooks/useVaultMutate.ts
@@ -76,6 +76,7 @@ export function useVaultMutate() : {
currentRevisionNumber: currentRevision,
emailAddressList: privateEmailAddresses,
privateEmailDomainList: [],
+ hiddenPrivateEmailDomainList: [],
publicEmailDomainList: [],
encryptionPublicKey: '',
client: '',
diff --git a/apps/mobile-app/hooks/useVaultSync.ts b/apps/mobile-app/hooks/useVaultSync.ts
index b30fed6e2..e15e4064d 100644
--- a/apps/mobile-app/hooks/useVaultSync.ts
+++ b/apps/mobile-app/hooks/useVaultSync.ts
@@ -16,16 +16,7 @@ import { VaultSyncErrorCode, getVaultSyncErrorCode } from '@/utils/types/errors/
/**
* Utility function to ensure a minimum time has elapsed for an operation
*/
-const withMinimumDelay = async (
- operation: () => Promise,
- minDelayMs: number,
- enableDelay: boolean = true
-): Promise => {
- if (!enableDelay) {
- // If delay is disabled, return the result immediately.
- return operation();
- }
-
+const withMinimumDelay = async (operation: () => Promise, minDelayMs: number): Promise => {
const startTime = Date.now();
const result = await operation();
const elapsedTime = Date.now() - startTime;
@@ -38,7 +29,6 @@ const withMinimumDelay = async (
};
type VaultSyncOptions = {
- initialSync?: boolean;
onSuccess?: (hasNewVault: boolean) => void;
onError?: (error: string) => void;
onStatus?: (message: string) => void;
@@ -59,10 +49,7 @@ export const useVaultSync = () : {
const dbContext = useDb();
const syncVault = useCallback(async (options: VaultSyncOptions = {}) => {
- const { initialSync = false, onSuccess, onError, onStatus, onOffline, onUpgradeRequired, abortSignal } = options;
-
- // For the initial sync, we add an artifical delay to various steps which makes it feel more fluid.
- const enableDelay = initialSync;
+ const { onSuccess, onError, onStatus, onOffline, onUpgradeRequired, abortSignal } = options;
try {
// Check if operation was aborted
@@ -87,11 +74,6 @@ export const useVaultSync = () : {
// Update status
onStatus?.(t('vault.checkingVaultUpdates'));
- // Add artificial delay for initial sync UX
- if (enableDelay) {
- await new Promise(resolve => setTimeout(resolve, 300));
- }
-
// Check if operation was aborted
if (abortSignal?.aborted) {
console.debug('VaultSync: Operation aborted after status update');
@@ -128,8 +110,7 @@ export const useVaultSync = () : {
// Run downloadVault with a min delay for UX purposes
await withMinimumDelay(
() => NativeVaultManager.downloadVault(newRevision!),
- enableDelay ? 500 : 300,
- true
+ 300
);
}
} catch (err) {
@@ -183,11 +164,6 @@ export const useVaultSync = () : {
return false;
}
- // Add artificial delay for initial sync UX
- if (enableDelay) {
- await new Promise(resolve => setTimeout(resolve, hasNewVault ? 1000 : 300));
- }
-
onSuccess?.(hasNewVault);
// Register credential identities after sync
diff --git a/apps/mobile-app/ios/VaultModels/VaultMetadata.swift b/apps/mobile-app/ios/VaultModels/VaultMetadata.swift
index 51b8930f4..81f5e0521 100644
--- a/apps/mobile-app/ios/VaultModels/VaultMetadata.swift
+++ b/apps/mobile-app/ios/VaultModels/VaultMetadata.swift
@@ -3,11 +3,13 @@ import Foundation
public struct VaultMetadata: Codable {
public var publicEmailDomains: [String]?
public var privateEmailDomains: [String]?
+ public var hiddenPrivateEmailDomains: [String]?
public var vaultRevisionNumber: Int
- public init(publicEmailDomains: [String]? = nil, privateEmailDomains: [String]? = nil, vaultRevisionNumber: Int) {
+ public init(publicEmailDomains: [String]? = nil, privateEmailDomains: [String]? = nil, hiddenPrivateEmailDomains: [String]? = nil, vaultRevisionNumber: Int) {
self.publicEmailDomains = publicEmailDomains
self.privateEmailDomains = privateEmailDomains
+ self.hiddenPrivateEmailDomains = hiddenPrivateEmailDomains
self.vaultRevisionNumber = vaultRevisionNumber
}
}
diff --git a/apps/mobile-app/ios/VaultStoreKit/VaultStore+Metadata.swift b/apps/mobile-app/ios/VaultStoreKit/VaultStore+Metadata.swift
index 552555158..3d02d00f7 100644
--- a/apps/mobile-app/ios/VaultStoreKit/VaultStore+Metadata.swift
+++ b/apps/mobile-app/ios/VaultStoreKit/VaultStore+Metadata.swift
@@ -42,6 +42,7 @@ extension VaultStore {
metadata = VaultMetadata(
publicEmailDomains: [],
privateEmailDomains: [],
+ hiddenPrivateEmailDomains: [],
vaultRevisionNumber: revisionNumber
)
}
diff --git a/apps/mobile-app/ios/VaultStoreKit/VaultStore+Mutate.swift b/apps/mobile-app/ios/VaultStoreKit/VaultStore+Mutate.swift
index b2c3fe6be..8df2b6790 100644
--- a/apps/mobile-app/ios/VaultStoreKit/VaultStore+Mutate.swift
+++ b/apps/mobile-app/ios/VaultStoreKit/VaultStore+Mutate.swift
@@ -8,13 +8,10 @@ public struct VaultUpload: Codable {
public let credentialsCount: Int
public let currentRevisionNumber: Int
public let emailAddressList: [String]
- public let privateEmailDomainList: [String]
- public let publicEmailDomainList: [String]
public let encryptionPublicKey: String
public let updatedAt: String
public let username: String
public let version: String
- public let client: String
}
/// Vault POST response from API
@@ -86,11 +83,6 @@ extension VaultStore {
// Get database version
let dbVersion = try getDatabaseVersion()
- // Get client version
- let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "0.0.0"
- let baseVersion = version.split(separator: "-").first.map(String.init) ?? "0.0.0"
- let client = "ios-\(baseVersion)"
-
let dateFormatter = ISO8601DateFormatter()
dateFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
let now = dateFormatter.string(from: Date())
@@ -101,13 +93,11 @@ extension VaultStore {
credentialsCount: credentials.count,
currentRevisionNumber: currentRevision,
emailAddressList: privateEmailAddresses,
- privateEmailDomainList: [], // Empty on purpose, API will not use this for vault updates
- publicEmailDomainList: [], // Empty on purpose, API will not use this for vault updates
- encryptionPublicKey: "", // Empty on purpose, only required if new public/private key pair is generated
+ // TODO: add public RSA encryption key to payload when implementing vault creation from mobile app. Currently only web app does this.
+ encryptionPublicKey: "",
updatedAt: now,
username: username,
version: dbVersion,
- client: client
)
}
diff --git a/apps/mobile-app/ios/VaultStoreKit/VaultStore+Sync.swift b/apps/mobile-app/ios/VaultStoreKit/VaultStore+Sync.swift
index aefac0ab0..d18187cd6 100644
--- a/apps/mobile-app/ios/VaultStoreKit/VaultStore+Sync.swift
+++ b/apps/mobile-app/ios/VaultStoreKit/VaultStore+Sync.swift
@@ -19,6 +19,7 @@ public struct VaultData: Codable {
public let credentialsCount: Int
public let emailAddressList: [String]
public let privateEmailDomainList: [String]
+ public let hiddenPrivateEmailDomainList: [String]
public let publicEmailDomainList: [String]
public let createdAt: String
public let updatedAt: String
@@ -177,11 +178,31 @@ extension VaultStore {
try storeEncryptedDatabase(vault.vault.blob)
setCurrentVaultRevisionNumber(newRevision)
+ // Store vault metadata (public/private email domains)
+ let metadata = VaultMetadata(
+ publicEmailDomains: vault.vault.publicEmailDomainList,
+ privateEmailDomains: vault.vault.privateEmailDomainList,
+ hiddenPrivateEmailDomains: vault.vault.hiddenPrivateEmailDomainList,
+ vaultRevisionNumber: newRevision
+ )
+ try storeVaultMetadata(metadata)
+
if isVaultUnlocked {
try unlockVault()
}
}
+ /// Store vault metadata
+ private func storeVaultMetadata(_ metadata: VaultMetadata) throws {
+ let encoder = JSONEncoder()
+ guard let metadataData = try? encoder.encode(metadata),
+ let metadataJson = String(data: metadataData, encoding: .utf8) else {
+ throw VaultSyncError.parseError(message: "Failed to encode vault metadata")
+ }
+
+ try storeMetadata(metadataJson)
+ }
+
/// Parse vault response from JSON
private func parseVaultResponse(_ body: String) throws -> VaultResponse {
guard let vaultData = body.data(using: .utf8) else {
diff --git a/apps/mobile-app/utils/SqliteClient.tsx b/apps/mobile-app/utils/SqliteClient.tsx
index 74c0c093c..3fae7ead2 100644
--- a/apps/mobile-app/utils/SqliteClient.tsx
+++ b/apps/mobile-app/utils/SqliteClient.tsx
@@ -30,7 +30,9 @@ class SqliteClient {
* Store the vault metadata via the native code implementation.
*
* Metadata is stored in plain text in UserDefaults. The metadata consists of the following:
- * - public and private email domains
+ * - public email domains
+ * - private email domains
+ * - hidden private email domains
* - vault revision number
*/
public async storeMetadata(metadata: string): Promise {
@@ -77,7 +79,7 @@ class SqliteClient {
return null;
}
- const { privateEmailDomains, publicEmailDomains } = metadata;
+ const { privateEmailDomains, publicEmailDomains, hiddenPrivateEmailDomains } = metadata;
/**
* Check if a domain is valid (not empty, not 'DISABLED.TLD', and exists in either private or public domains)
@@ -98,7 +100,8 @@ class SqliteClient {
}
// If default domain is not valid, fall back to first available private domain
- const firstPrivate = privateEmailDomains?.find(isValidDomain);
+ // Filter out hidden private domains from the list of private domains
+ const firstPrivate = privateEmailDomains?.filter(domain => !hiddenPrivateEmailDomains?.includes(domain)).find(isValidDomain);
if (firstPrivate) {
return firstPrivate;
}
diff --git a/apps/mobile-app/utils/WebApiService.ts b/apps/mobile-app/utils/WebApiService.ts
index 51af2837e..739712cd2 100644
--- a/apps/mobile-app/utils/WebApiService.ts
+++ b/apps/mobile-app/utils/WebApiService.ts
@@ -314,27 +314,6 @@ export class WebApiService {
return this.get('Security/authlogs');
}
- /**
- * Validates the vault response and returns an error message if validation fails
- */
- public validateVaultResponse(vaultResponseJson: VaultResponse): string | null {
- /**
- * Status 0 = OK, vault is ready.
- * Status 1 = Merge required, which only the web client supports.
- * Status 2 = Outdated, which means the local vault is outdated and the client should fetch the latest vault from the server before saving can continue.
- */
- if (vaultResponseJson.status === 1) {
- // Note: vault merge is no longer allowed by the API as of 0.20.0, updates with the same revision number are rejected. So this check can be removed later.
- return i18n.t('vault.errors.vaultOutdated');
- }
-
- if (vaultResponseJson.status === 2) {
- return i18n.t('vault.errors.vaultOutdated');
- }
-
- return null;
- }
-
/**
* Get the currently configured API URL from native storage.
*/
diff --git a/apps/mobile-app/utils/dist/shared/models/metadata/index.d.ts b/apps/mobile-app/utils/dist/shared/models/metadata/index.d.ts
index e09bb64c7..f15dd4f7c 100644
--- a/apps/mobile-app/utils/dist/shared/models/metadata/index.d.ts
+++ b/apps/mobile-app/utils/dist/shared/models/metadata/index.d.ts
@@ -1,6 +1,7 @@
type VaultMetadata = {
publicEmailDomains: string[];
privateEmailDomains: string[];
+ hiddenPrivateEmailDomains: string[];
vaultRevisionNumber: number;
};
diff --git a/apps/mobile-app/utils/dist/shared/models/webapi/index.d.ts b/apps/mobile-app/utils/dist/shared/models/webapi/index.d.ts
index af8a6cbfd..46c8a7754 100644
--- a/apps/mobile-app/utils/dist/shared/models/webapi/index.d.ts
+++ b/apps/mobile-app/utils/dist/shared/models/webapi/index.d.ts
@@ -28,18 +28,18 @@ type ApiErrorResponse = {
* Vault type.
*/
type Vault = {
- blob: string;
- createdAt: string;
- credentialsCount: number;
- currentRevisionNumber: number;
- emailAddressList: string[];
- privateEmailDomainList: string[];
- publicEmailDomainList: string[];
- encryptionPublicKey: string;
- updatedAt: string;
username: string;
+ blob: string;
version: string;
- client: string;
+ currentRevisionNumber: number;
+ credentialsCount: number;
+ createdAt: string;
+ updatedAt: string;
+ encryptionPublicKey?: string;
+ emailAddressList?: string[];
+ privateEmailDomainList?: string[];
+ hiddenPrivateEmailDomainList?: string[];
+ publicEmailDomainList?: string[];
};
/**
diff --git a/apps/server/AliasVault.Api/Config.cs b/apps/server/AliasVault.Api/Config.cs
index df1759f22..cbad69020 100644
--- a/apps/server/AliasVault.Api/Config.cs
+++ b/apps/server/AliasVault.Api/Config.cs
@@ -23,4 +23,10 @@ public class Config : SharedConfig
/// Gets or sets the list of private email domains that are available.
///
public List PrivateEmailDomains { get; set; } = [];
+
+ ///
+ /// Gets or sets the list of private email domains that should be hidden from UI components.
+ /// These domains still function as private email domains but are not shown in domain selection dropdowns.
+ ///
+ public List HiddenPrivateEmailDomains { get; set; } = [];
}
diff --git a/apps/server/AliasVault.Api/Controllers/VaultController.cs b/apps/server/AliasVault.Api/Controllers/VaultController.cs
index 57efb56d1..f3a95567e 100644
--- a/apps/server/AliasVault.Api/Controllers/VaultController.cs
+++ b/apps/server/AliasVault.Api/Controllers/VaultController.cs
@@ -89,11 +89,7 @@ public class VaultController(ILogger logger, IAliasServerDbCont
Blob = string.Empty,
Version = string.Empty,
CurrentRevisionNumber = 0,
- EncryptionPublicKey = string.Empty,
CredentialsCount = 0,
- EmailAddressList = [],
- PrivateEmailDomainList = [],
- PublicEmailDomainList = [],
CreatedAt = DateTime.MinValue,
UpdatedAt = DateTime.MinValue,
},
@@ -121,6 +117,7 @@ public class VaultController(ILogger logger, IAliasServerDbCont
// Get dynamic list of private email domains from config.
var privateEmailDomainList = config.PrivateEmailDomains;
+ var hiddenPrivateEmailDomainList = config.HiddenPrivateEmailDomains;
// Hardcoded list of public (SpamOK) email domains that are available to the client.
var publicEmailDomainList = new List(["spamok.com", "solarflarecorp.com", "spamok.nl", "3060.nl",
@@ -137,8 +134,8 @@ public class VaultController(ILogger logger, IAliasServerDbCont
CurrentRevisionNumber = vault.RevisionNumber,
EncryptionPublicKey = string.Empty,
CredentialsCount = 0,
- EmailAddressList = [],
PrivateEmailDomainList = privateEmailDomainList,
+ HiddenPrivateEmailDomainList = hiddenPrivateEmailDomainList,
PublicEmailDomainList = publicEmailDomainList,
CreatedAt = vault.CreatedAt,
UpdatedAt = vault.UpdatedAt,
@@ -176,11 +173,7 @@ public class VaultController(ILogger logger, IAliasServerDbCont
Blob = x.VaultBlob,
Version = x.Version,
CurrentRevisionNumber = x.RevisionNumber,
- EncryptionPublicKey = string.Empty,
CredentialsCount = 0,
- EmailAddressList = [],
- PrivateEmailDomainList = [],
- PublicEmailDomainList = [],
CreatedAt = x.CreatedAt,
UpdatedAt = x.UpdatedAt,
}).ToList(),
diff --git a/apps/server/AliasVault.Api/Program.cs b/apps/server/AliasVault.Api/Program.cs
index 54e3ab151..51cb57af5 100644
--- a/apps/server/AliasVault.Api/Program.cs
+++ b/apps/server/AliasVault.Api/Program.cs
@@ -48,6 +48,12 @@ var privateEmailDomains = Environment.GetEnvironmentVariable("PRIVATE_EMAIL_DOMA
.Where(d => !string.IsNullOrWhiteSpace(d));
config.PrivateEmailDomains = privateEmailDomains?.ToList() ?? new List();
+var hiddenPrivateEmailDomains = Environment.GetEnvironmentVariable("HIDDEN_PRIVATE_EMAIL_DOMAINS")?
+ .Split(",", StringSplitOptions.RemoveEmptyEntries)
+ .Select(d => d.Trim())
+ .Where(d => !string.IsNullOrWhiteSpace(d));
+config.HiddenPrivateEmailDomains = hiddenPrivateEmailDomains?.ToList() ?? new List();
+
var ipLoggingEnabled = Environment.GetEnvironmentVariable("IP_LOGGING_ENABLED") ?? "false";
config.IpLoggingEnabled = bool.Parse(ipLoggingEnabled);
diff --git a/apps/server/AliasVault.Api/Properties/launchSettings.json b/apps/server/AliasVault.Api/Properties/launchSettings.json
index 689bbec26..0fe4f8535 100644
--- a/apps/server/AliasVault.Api/Properties/launchSettings.json
+++ b/apps/server/AliasVault.Api/Properties/launchSettings.json
@@ -9,7 +9,8 @@
"JWT_KEY": "12345678901234567890123456789012",
"DATA_PROTECTION_CERT_PASS": "Development",
"PUBLIC_REGISTRATION_ENABLED": "true",
- "PRIVATE_EMAIL_DOMAINS": "example.tld,example2.tld",
+ "PRIVATE_EMAIL_DOMAINS": "example.tld,example2.tld,disabled.tld",
+ "HIDDEN_PRIVATE_EMAIL_DOMAINS": "disabled.tld",
"IP_LOGGING_ENABLED": "true"
},
"dotnetRunMessages": true,
@@ -24,7 +25,8 @@
"JWT_KEY": "12345678901234567890123456789012",
"DATA_PROTECTION_CERT_PASS": "Development",
"PUBLIC_REGISTRATION_ENABLED": "true",
- "PRIVATE_EMAIL_DOMAINS": "example.tld,example2.tld",
+ "PRIVATE_EMAIL_DOMAINS": "example.tld,example2.tld,disabled.tld",
+ "HIDDEN_PRIVATE_EMAIL_DOMAINS": "disabled.tld",
"IP_LOGGING_ENABLED": "true"
},
"dotnetRunMessages": true,
diff --git a/apps/server/AliasVault.Client/Config.cs b/apps/server/AliasVault.Client/Config.cs
index 148e38f53..211f2633e 100644
--- a/apps/server/AliasVault.Client/Config.cs
+++ b/apps/server/AliasVault.Client/Config.cs
@@ -23,6 +23,12 @@ public class Config
///
public List PrivateEmailDomains { get; set; } = [];
+ ///
+ /// Gets or sets the list of private email domains that should be hidden from UI components.
+ /// These domains still function as private email domains but are not shown in domain selection dropdowns.
+ ///
+ public List HiddenPrivateEmailDomains { get; set; } = [];
+
///
/// Gets or sets the list of public email domains that are allowed to be used by the client vault users.
///
diff --git a/apps/server/AliasVault.Client/Main/Components/Forms/EditEmailFormRow.razor b/apps/server/AliasVault.Client/Main/Components/Forms/EditEmailFormRow.razor
index 92513e40a..25d413b9c 100644
--- a/apps/server/AliasVault.Client/Main/Components/Forms/EditEmailFormRow.razor
+++ b/apps/server/AliasVault.Client/Main/Components/Forms/EditEmailFormRow.razor
@@ -99,7 +99,9 @@
private string SelectedDomain = string.Empty;
private bool IsPopupVisible = false;
- private List PrivateDomains => Config.PrivateEmailDomains;
+ private List PrivateDomains => Config.PrivateEmailDomains
+ .Where(d => !Config.HiddenPrivateEmailDomains.Contains(d))
+ .ToList();
private List PublicDomains => Config.PublicEmailDomains;
private bool ShowPrivateDomains => PrivateDomains.Count > 0 && !(PrivateDomains.Count == 1 && PrivateDomains[0] == "DISABLED.TLD");
diff --git a/apps/server/AliasVault.Client/Main/Pages/Settings/General.razor b/apps/server/AliasVault.Client/Main/Pages/Settings/General.razor
index 8a128c555..b2ea49a7d 100644
--- a/apps/server/AliasVault.Client/Main/Pages/Settings/General.razor
+++ b/apps/server/AliasVault.Client/Main/Pages/Settings/General.razor
@@ -125,7 +125,9 @@
@code {
private IStringLocalizer Localizer => LocalizerFactory.Create("Pages.Main.Settings.General", "AliasVault.Client");
- private List PrivateDomains => Config.PrivateEmailDomains;
+ private List PrivateDomains => Config.PrivateEmailDomains
+ .Where(d => !Config.HiddenPrivateEmailDomains.Contains(d))
+ .ToList();
private List PublicDomains => Config.PublicEmailDomains;
private string DefaultEmailDomain { get; set; } = string.Empty;
diff --git a/apps/server/AliasVault.Client/Main/Pages/Settings/Security/ChangePassword.razor b/apps/server/AliasVault.Client/Main/Pages/Settings/Security/ChangePassword.razor
index 9e0a642cf..fde6e4ac6 100644
--- a/apps/server/AliasVault.Client/Main/Pages/Settings/Security/ChangePassword.razor
+++ b/apps/server/AliasVault.Client/Main/Pages/Settings/Security/ChangePassword.razor
@@ -196,6 +196,7 @@ else
CredentialsCount = vault.CredentialsCount,
EmailAddressList = vault.EmailAddressList,
PrivateEmailDomainList = [],
+ HiddenPrivateEmailDomainList = [],
PublicEmailDomainList = [],
CreatedAt = vault.CreatedAt,
UpdatedAt = vault.UpdatedAt,
diff --git a/apps/server/AliasVault.Client/Services/Database/DbService.cs b/apps/server/AliasVault.Client/Services/Database/DbService.cs
index 8c142ac14..17fa89256 100644
--- a/apps/server/AliasVault.Client/Services/Database/DbService.cs
+++ b/apps/server/AliasVault.Client/Services/Database/DbService.cs
@@ -490,6 +490,7 @@ public sealed class DbService : IDisposable
CredentialsCount = credentialsCount,
EmailAddressList = emailAddresses,
PrivateEmailDomainList = [],
+ HiddenPrivateEmailDomainList = [],
PublicEmailDomainList = [],
CreatedAt = currentDateTime,
UpdatedAt = currentDateTime,
diff --git a/apps/server/AliasVault.Client/entrypoint.sh b/apps/server/AliasVault.Client/entrypoint.sh
index 8f94be757..cd740c802 100755
--- a/apps/server/AliasVault.Client/entrypoint.sh
+++ b/apps/server/AliasVault.Client/entrypoint.sh
@@ -1,11 +1,13 @@
#!/bin/sh
# Set the default values
DEFAULT_PRIVATE_EMAIL_DOMAINS=""
+DEFAULT_HIDDEN_PRIVATE_EMAIL_DOMAINS=""
DEFAULT_SUPPORT_EMAIL=""
DEFAULT_PUBLIC_REGISTRATION_ENABLED="true"
# Use the provided environment variables if they exist, otherwise use defaults
PRIVATE_EMAIL_DOMAINS=${PRIVATE_EMAIL_DOMAINS:-$DEFAULT_PRIVATE_EMAIL_DOMAINS}
+HIDDEN_PRIVATE_EMAIL_DOMAINS=${HIDDEN_PRIVATE_EMAIL_DOMAINS:-$DEFAULT_HIDDEN_PRIVATE_EMAIL_DOMAINS}
SUPPORT_EMAIL=${SUPPORT_EMAIL:-$DEFAULT_SUPPORT_EMAIL}
PUBLIC_REGISTRATION_ENABLED=${PUBLIC_REGISTRATION_ENABLED:-$DEFAULT_PUBLIC_REGISTRATION_ENABLED}
@@ -37,9 +39,25 @@ else
json_array=$(echo $PRIVATE_EMAIL_DOMAINS | awk '{split($0,a,","); printf "["; for(i=1;i<=length(a);i++) {printf "\"%s\"", a[i]; if(i
public class Vault
{
+ // ------------------------------------------------------------
+ // Required properties, always part of the vault get/set model.
+ // ------------------------------------------------------------
+
///
/// Gets or sets the username that owns the vault.
///
@@ -34,32 +38,12 @@ public class Vault
///
public required long CurrentRevisionNumber { get; set; }
- ///
- /// Gets or sets the public encryption key that server requires to encrypt user data such as received emails.
- ///
- public required string EncryptionPublicKey { get; set; }
-
///
/// Gets or sets the number of credentials stored in the vault. This anonymous data is used in case a vault back-up
/// needs to be restored to get a better idea of the vault size.
///
public required int CredentialsCount { get; set; }
- ///
- /// Gets or sets the list of email addresses that are used in the vault and should be registered on the server.
- ///
- public required List EmailAddressList { get; set; }
-
- ///
- /// Gets or sets the list of private email domains that are available.
- ///
- public required List PrivateEmailDomainList { get; set; }
-
- ///
- /// Gets or sets the list of public email domains that are available.
- ///
- public required List PublicEmailDomainList { get; set; }
-
///
/// Gets or sets the date and time of creation.
///
@@ -69,4 +53,34 @@ public class Vault
/// Gets or sets the date and time of last update.
///
public required DateTime UpdatedAt { get; set; }
+
+ // ------------------------------------------------------------
+ // Optional properties, only part of the vault get/set model if available and applicable.
+ // ------------------------------------------------------------
+
+ ///
+ /// Gets or sets the public encryption key that server requires to encrypt user data such as received emails.
+ ///
+ public string EncryptionPublicKey { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the list of email addresses that are used in the vault and should be registered on the server.
+ ///
+ public List EmailAddressList { get; set; } = [];
+
+ ///
+ /// Gets or sets the list of private email domains that are available.
+ ///
+ public List PrivateEmailDomainList { get; set; } = [];
+
+ ///
+ /// Gets or sets the list of private email domains that should be hidden from UI components.
+ /// These domains still function as private email domains but are not shown in domain selection dropdowns.
+ ///
+ public List HiddenPrivateEmailDomainList { get; set; } = [];
+
+ ///
+ /// Gets or sets the list of public email domains that are available.
+ ///
+ public List PublicEmailDomainList { get; set; } = [];
}
diff --git a/apps/server/Tests/AliasVault.E2ETests/Common/ClientPlaywrightTest.cs b/apps/server/Tests/AliasVault.E2ETests/Common/ClientPlaywrightTest.cs
index 673b40f8c..aa3555411 100644
--- a/apps/server/Tests/AliasVault.E2ETests/Common/ClientPlaywrightTest.cs
+++ b/apps/server/Tests/AliasVault.E2ETests/Common/ClientPlaywrightTest.cs
@@ -94,8 +94,10 @@ public class ClientPlaywrightTest : PlaywrightTest
ApiBaseUrl = "http://localhost:" + apiPort + "/";
// Set environment variables for the API.
- string[] privateEmailDomains = ["example.tld", "example2.tld"];
+ string[] privateEmailDomains = ["example.tld", "example2.tld","disabled.tld"];
Environment.SetEnvironmentVariable("PRIVATE_EMAIL_DOMAINS", string.Join(",", privateEmailDomains));
+ string[] hiddenPrivateEmailDomains = ["disabled.tld"];
+ Environment.SetEnvironmentVariable("HIDDEN_PRIVATE_EMAIL_DOMAINS", string.Join(",", hiddenPrivateEmailDomains));
// Start WebAPI in-memory.
_apiFactory.Port = apiPort;
diff --git a/dockerfiles/all-in-one/Dockerfile b/dockerfiles/all-in-one/Dockerfile
index 5785b3700..eeaadd18b 100644
--- a/dockerfiles/all-in-one/Dockerfile
+++ b/dockerfiles/all-in-one/Dockerfile
@@ -163,6 +163,7 @@ ENV ALIASVAULT_VERBOSITY=0 \
IP_LOGGING_ENABLED=true \
SUPPORT_EMAIL="" \
PRIVATE_EMAIL_DOMAINS="" \
+ HIDDEN_PRIVATE_EMAIL_DOMAINS="" \
HOSTNAME=localhost \
POSTGRES_HOST=localhost \
POSTGRES_PORT=5432 \
diff --git a/dockerfiles/all-in-one/s6-scripts/api/run b/dockerfiles/all-in-one/s6-scripts/api/run
index 70bdabf0e..7f9334297 100644
--- a/dockerfiles/all-in-one/s6-scripts/api/run
+++ b/dockerfiles/all-in-one/s6-scripts/api/run
@@ -30,6 +30,7 @@ done
export ASPNETCORE_URLS="http://0.0.0.0:3001"
export ASPNETCORE_PATHBASE="/api"
export PRIVATE_EMAIL_DOMAINS="${PRIVATE_EMAIL_DOMAINS:-}"
+export HIDDEN_PRIVATE_EMAIL_DOMAINS="${HIDDEN_PRIVATE_EMAIL_DOMAINS:-}"
export PUBLIC_REGISTRATION_ENABLED="${PUBLIC_REGISTRATION_ENABLED:-true}"
export IP_LOGGING_ENABLED="${IP_LOGGING_ENABLED:-true}"
diff --git a/dockerfiles/all-in-one/s6-scripts/client/run b/dockerfiles/all-in-one/s6-scripts/client/run
index 6fc139060..19f7b8dfa 100644
--- a/dockerfiles/all-in-one/s6-scripts/client/run
+++ b/dockerfiles/all-in-one/s6-scripts/client/run
@@ -27,8 +27,10 @@ done
# Client service entrypoint
DEFAULT_PRIVATE_EMAIL_DOMAINS=""
+DEFAULT_HIDDEN_PRIVATE_EMAIL_DOMAINS=""
DEFAULT_SUPPORT_EMAIL=""
PRIVATE_EMAIL_DOMAINS=${PRIVATE_EMAIL_DOMAINS:-$DEFAULT_PRIVATE_EMAIL_DOMAINS}
+HIDDEN_PRIVATE_EMAIL_DOMAINS=${HIDDEN_PRIVATE_EMAIL_DOMAINS:-$DEFAULT_HIDDEN_PRIVATE_EMAIL_DOMAINS}
SUPPORT_EMAIL=${SUPPORT_EMAIL:-$DEFAULT_SUPPORT_EMAIL}
PUBLIC_REGISTRATION_ENABLED=${PUBLIC_REGISTRATION_ENABLED:-true}
@@ -58,10 +60,19 @@ else
json_array=$(echo "$PRIVATE_EMAIL_DOMAINS" | awk '{split($0,a,","); printf "["; for(i=1;i<=length(a);i++) {printf "\"%s\"", a[i]; if(i /app/client/wwwroot/appsettings.json << EOF
{
"PrivateEmailDomains": $json_array,
+ "HiddenPrivateEmailDomains": $hidden_json_array,
"SupportEmail": "$SUPPORT_EMAIL",
"PublicRegistrationEnabled": "$PUBLIC_REGISTRATION_ENABLED"
}
diff --git a/dockerfiles/all-in-one/s6-scripts/smtp/run b/dockerfiles/all-in-one/s6-scripts/smtp/run
index 65647fba1..a0234a4a2 100644
--- a/dockerfiles/all-in-one/s6-scripts/smtp/run
+++ b/dockerfiles/all-in-one/s6-scripts/smtp/run
@@ -28,6 +28,7 @@ for i in {1..30}; do
done
export PRIVATE_EMAIL_DOMAINS="${PRIVATE_EMAIL_DOMAINS:-}"
+export HIDDEN_PRIVATE_EMAIL_DOMAINS="${HIDDEN_PRIVATE_EMAIL_DOMAINS:-}"
export SMTP_TLS_ENABLED="${SMTP_TLS_ENABLED:-false}"
# Set .NET logging level based on verbosity
diff --git a/dockerfiles/docker-compose.all-in-one.nas.yml b/dockerfiles/docker-compose.all-in-one.nas.yml
index 35a340c20..3f9508c95 100644
--- a/dockerfiles/docker-compose.all-in-one.nas.yml
+++ b/dockerfiles/docker-compose.all-in-one.nas.yml
@@ -29,6 +29,7 @@ services:
FORCE_HTTPS_REDIRECT: "false"
SUPPORT_EMAIL: ""
PRIVATE_EMAIL_DOMAINS: ""
+ HIDDEN_PRIVATE_EMAIL_DOMAINS: ""
volumes:
avdata:
diff --git a/dockerfiles/docker-compose.all-in-one.yml b/dockerfiles/docker-compose.all-in-one.yml
index caf414981..81e45ad96 100644
--- a/dockerfiles/docker-compose.all-in-one.yml
+++ b/dockerfiles/docker-compose.all-in-one.yml
@@ -27,3 +27,4 @@ services:
FORCE_HTTPS_REDIRECT: "false"
SUPPORT_EMAIL: ""
PRIVATE_EMAIL_DOMAINS: ""
+ HIDDEN_PRIVATE_EMAIL_DOMAINS: ""
diff --git a/install.sh b/install.sh
index 60d6e1b1c..a60f11fc9 100755
--- a/install.sh
+++ b/install.sh
@@ -3183,6 +3183,12 @@ check_and_populate_env() {
printf " Set PRIVATE_EMAIL_DOMAINS\n"
fi
+ # HIDDEN_PRIVATE_EMAIL_DOMAINS
+ if ! grep -q "^HIDDEN_PRIVATE_EMAIL_DOMAINS=" "$ENV_FILE"; then
+ update_env_var "HIDDEN_PRIVATE_EMAIL_DOMAINS" ""
+ printf " Set HIDDEN_PRIVATE_EMAIL_DOMAINS\n"
+ fi
+
# HTTP_PORT
if ! grep -q "^HTTP_PORT=" "$ENV_FILE" || [ -z "$(grep "^HTTP_PORT=" "$ENV_FILE" | cut -d '=' -f2)" ]; then
update_env_var "HTTP_PORT" "80"
diff --git a/shared/models/src/metadata/VaultMetadata.ts b/shared/models/src/metadata/VaultMetadata.ts
index 521306858..acb06c9a4 100644
--- a/shared/models/src/metadata/VaultMetadata.ts
+++ b/shared/models/src/metadata/VaultMetadata.ts
@@ -1,5 +1,6 @@
export type VaultMetadata = {
publicEmailDomains: string[],
privateEmailDomains: string[],
+ hiddenPrivateEmailDomains: string[],
vaultRevisionNumber: number
};
diff --git a/shared/models/src/webapi/Vault.ts b/shared/models/src/webapi/Vault.ts
index e318aa196..cb92d0027 100644
--- a/shared/models/src/webapi/Vault.ts
+++ b/shared/models/src/webapi/Vault.ts
@@ -2,16 +2,18 @@
* Vault type.
*/
export type Vault = {
- blob: string;
- createdAt: string;
- credentialsCount: number;
- currentRevisionNumber: number;
- emailAddressList: string[];
- privateEmailDomainList: string[];
- publicEmailDomainList: string[];
- encryptionPublicKey: string;
- updatedAt: string;
+ // Required properties, always part of the vault get/set model.
username: string;
+ blob: string;
version: string;
- client: string;
+ currentRevisionNumber: number;
+ credentialsCount: number;
+ createdAt: string;
+ updatedAt: string;
+ // Optional properties, only part of the vault get/set model if available and applicable.
+ encryptionPublicKey?: string;
+ emailAddressList?: string[];
+ privateEmailDomainList?: string[];
+ hiddenPrivateEmailDomainList?: string[];
+ publicEmailDomainList?: string[];
}
\ No newline at end of file