diff --git a/public/src/config.js b/public/src/config.js
index 7375b18..bb4d2bc 100644
--- a/public/src/config.js
+++ b/public/src/config.js
@@ -190,14 +190,29 @@ export const VLLM_ENDPOINTS = [
},
];
+// Helper to get vault value or localStorage fallback
+function getVaultOrStorage(key, defaultValue = null) {
+ // Try vault first if available
+ if (typeof window !== 'undefined' && window.UncloseVault && window.UncloseVault.isUnlocked()) {
+ return window.UncloseVault.get(key, defaultValue);
+ }
+ // Fallback to localStorage
+ const stored = localStorage.getItem(key);
+ if (stored === null) return defaultValue;
+ // Parse booleans
+ if (stored === 'true') return true;
+ if (stored === 'false') return false;
+ return stored;
+}
+
// Function to get API configuration (custom or default)
export async function getAPIConfig() {
try {
- const useCustomAPI = localStorage.getItem("useCustomAPI") === "true";
-
+ const useCustomAPI = getVaultOrStorage("useCustomAPI", false) === true;
+
if (useCustomAPI) {
- const customBaseURL = localStorage.getItem("customBaseURL");
- const customAPIKey = localStorage.getItem("customAPIKey");
+ const customBaseURL = getVaultOrStorage("customBaseURL", "");
+ const customAPIKey = getVaultOrStorage("customAPIKey", "");
// Import model functions to get the currently selected model
const { getSelectedModel } = await import("./models.js");
diff --git a/public/src/languages/en.js b/public/src/languages/en.js
index 7030d6e..1a02ef7 100644
--- a/public/src/languages/en.js
+++ b/public/src/languages/en.js
@@ -117,4 +117,35 @@ export const en = {
useUnsandbox: "Use unsandbox.com API keys for code execution.",
unsandboxPublicKey: "Public Key (unsb-pk-...)",
unsandboxSecretKey: "Secret Key (unsb-sk-...)",
+
+ // Vault settings
+ vaultLoggedIn: "Logged in",
+ vaultLocked: "Settings Locked",
+ vaultUnlocked: "Settings Unlocked",
+ vaultCreate: "Save Settings",
+ vaultUnlock: "Unlock",
+ vaultLock: "Lock",
+ vaultLockTooltip: "Lock your settings (log out)",
+ vaultPassword: "Password",
+ vaultPasswordPlaceholder: "Choose a password...",
+ vaultConfirmPassword: "Confirm password",
+ vaultConfirmPlaceholder: "Confirm your password...",
+ vaultWrongPassword: "That password didn't work. Please try again.",
+ vaultPasswordMismatch: "Passwords don't match.",
+ vaultPasswordTooShort: "Password needs at least 8 characters.",
+ vaultCreated: "Settings saved!",
+ vaultUnlockedMsg: "Welcome back!",
+ vaultLockedMsg: "Settings locked.",
+ vaultRequired: "Enter your password to access settings.",
+ vaultWelcome: "Your settings (model, voice, API keys) are saved safely behind a password of your choice. This keeps them private on this device.",
+ vaultWelcomeBack: "Welcome back! Enter your password to access your saved settings.",
+ vaultCreateInfo: "Choose a password to protect your settings. Your preferences and API keys will be encrypted and stored locally.",
+ vaultSessionInfo: "You'll stay logged in for {days} days.",
+ vaultChangePassword: "Change Password",
+ vaultDeleteVault: "Delete Settings",
+ vaultDeleteConfirm: "Are you sure? This will remove all your saved settings from this device.",
+ vaultCurrentPassword: "Current password",
+ vaultNewPassword: "New password",
+ vaultPasswordChanged: "Password changed!",
+ vaultDeleted: "Settings deleted.",
};
\ No newline at end of file
diff --git a/public/src/un.js b/public/src/un.js
index b891f1b..a2384c1 100644
--- a/public/src/un.js
+++ b/public/src/un.js
@@ -10,9 +10,24 @@ const API_BASE = "https://api.unsandbox.com";
const POLL_DELAYS_MS = [300, 450, 700, 900, 650, 1600, 2000];
/**
- * Get credentials from localStorage
+ * Get credentials from vault (preferred) or localStorage (fallback)
*/
export function getCredentials() {
+ // Try to get from vault first
+ if (typeof window !== 'undefined' && window.UncloseVault && window.UncloseVault.isUnlocked()) {
+ const useUnsandbox = window.UncloseVault.get("useUnsandbox", false);
+ if (!useUnsandbox) return null;
+
+ const publicKey = window.UncloseVault.get("unsandboxPublicKey");
+ const secretKey = window.UncloseVault.get("unsandboxSecretKey");
+
+ if (publicKey && secretKey) {
+ return { publicKey, secretKey };
+ }
+ return null;
+ }
+
+ // Fallback to localStorage (for migration or if vault not available)
const useUnsandbox = localStorage.getItem("useUnsandbox") === "true";
if (!useUnsandbox) return null;
diff --git a/public/src/uncloseai-embed-modal.js b/public/src/uncloseai-embed-modal.js
index 036c692..f0c69e9 100644
--- a/public/src/uncloseai-embed-modal.js
+++ b/public/src/uncloseai-embed-modal.js
@@ -15,6 +15,23 @@ import {
setUserLanguagePreference,
} from "./ui-translations.js";
import { NATIVE_LANGUAGE_NAMES } from "./translation.js";
+import { UncloseVault } from "./vault.js";
+
+// Load CryptoJS if not already loaded
+async function ensureCryptoJS() {
+ if (typeof CryptoJS !== 'undefined') return true;
+
+ return new Promise((resolve) => {
+ const script = document.createElement('script');
+ script.src = 'https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.2.0/crypto-js.min.js';
+ script.onload = () => resolve(true);
+ script.onerror = () => {
+ console.warn('Failed to load CryptoJS - vault features disabled');
+ resolve(false);
+ };
+ document.head.appendChild(script);
+ });
+}
// Get USE_CUSTOM_STYLING from window or default
const USE_CUSTOM_STYLING = window.UNCLOSEAI_CUSTOM_STYLING !== false;
@@ -179,6 +196,12 @@ function refreshModalUI() {
async function openUncloseaiEmbeddedModalNew() {
try {
+ // Load CryptoJS and initialize vault
+ await ensureCryptoJS();
+ if (UncloseVault.isAvailable()) {
+ UncloseVault.init();
+ }
+
// Detect if PicoCSS is actually present on the page
const hasPicoCSS = document.querySelector('link[href*="pico"]') !== null;
@@ -277,6 +300,311 @@ async function openUncloseaiEmbeddedModalNew() {
const settingsPanel = document.createElement("div");
settingsPanel.className = "uncloseai-modal-settings";
+ // Vault UI section
+ const vaultSection = document.createElement("div");
+ vaultSection.className = "uncloseai-section uncloseai-vault-section";
+ vaultSection.style.gridColumn = "1 / -1"; // Full width
+ vaultSection.style.marginBottom = "16px";
+ vaultSection.style.padding = "12px";
+ vaultSection.style.borderRadius = "8px";
+ vaultSection.style.border = "1px solid var(--uncloseai-border-color, #ccc)";
+
+ // Container for the rest of settings (will be hidden when vault locked)
+ const settingsContent = document.createElement("div");
+ settingsContent.className = "uncloseai-settings-content";
+
+ // Helper to get vault value or localStorage fallback (for migration)
+ function getVaultOrStorage(key, defaultValue = null) {
+ if (UncloseVault.isAvailable() && UncloseVault.isUnlocked()) {
+ return UncloseVault.get(key, defaultValue);
+ }
+ // Fallback for when vault not available
+ const stored = localStorage.getItem(key);
+ return stored !== null ? stored : defaultValue;
+ }
+
+ // Helper to set vault value
+ function setVaultValue(key, value) {
+ if (UncloseVault.isAvailable() && UncloseVault.isUnlocked()) {
+ UncloseVault.set(key, value);
+ } else {
+ // If vault not unlocked, store temporarily (will be lost)
+ console.warn('Vault locked - setting not persisted:', key);
+ }
+ }
+
+ // Build vault UI based on current state
+ function buildVaultUI() {
+ vaultSection.innerHTML = '';
+
+ if (!UncloseVault.isAvailable()) {
+ // CryptoJS not loaded - settings work but won't persist across sessions
+ vaultSection.style.display = 'none';
+ settingsContent.style.display = 'block';
+ return;
+ }
+
+ if (UncloseVault.isUnlocked()) {
+ // Vault unlocked - show simple "Logged in" status with lock button
+ vaultSection.style.display = 'block';
+ const unlockedUI = document.createElement("div");
+ unlockedUI.innerHTML = `
+
+
+ ✓ ${getUIText("vaultLoggedIn")}
+
+
+
+
+
+
+ `;
+
+ const lockBtn = unlockedUI.querySelector('.vault-lock-btn');
+ const settingsBtn = unlockedUI.querySelector('.vault-settings-btn');
+
+ lockBtn.onclick = () => {
+ UncloseVault.lock();
+ buildVaultUI();
+ // Close settings panel after locking
+ settingsOpen = false;
+ settingsPanel.classList.remove("open");
+ };
+
+ settingsBtn.onclick = () => {
+ showVaultSettings();
+ };
+
+ vaultSection.appendChild(unlockedUI);
+ settingsContent.style.display = 'block'; // Show settings when unlocked
+ } else {
+ // Vault locked or doesn't exist - show prompt
+ vaultSection.style.display = 'block';
+ const hasVault = UncloseVault.hasVault();
+
+ const promptUI = document.createElement("div");
+ promptUI.innerHTML = `
+
+
+ ${hasVault ? getUIText("vaultWelcomeBack") : getUIText("vaultWelcome")}
+
+
+ ${!hasVault ? `
` : ''}
+
+
+
+ `;
+
+ const passwordInput = promptUI.querySelector('.vault-password');
+ const confirmInput = promptUI.querySelector('.vault-confirm');
+ const submitBtn = promptUI.querySelector('.vault-submit-btn');
+ const errorDiv = promptUI.querySelector('.vault-error');
+
+ submitBtn.onclick = () => {
+ const password = passwordInput.value;
+
+ if (hasVault) {
+ // Unlock existing vault
+ const result = UncloseVault.unlock(password);
+ if (result.success) {
+ buildVaultUI();
+ refreshSettingsFromVault();
+ } else {
+ errorDiv.textContent = getUIText("vaultWrongPassword");
+ errorDiv.style.display = 'block';
+ passwordInput.value = '';
+ passwordInput.focus();
+ }
+ } else {
+ // Create new vault
+ const confirm = confirmInput ? confirmInput.value : '';
+
+ if (password.length < 8) {
+ errorDiv.textContent = getUIText("vaultPasswordTooShort");
+ errorDiv.style.display = 'block';
+ return;
+ }
+
+ if (password !== confirm) {
+ errorDiv.textContent = getUIText("vaultPasswordMismatch");
+ errorDiv.style.display = 'block';
+ return;
+ }
+
+ const result = UncloseVault.create(password);
+ if (result.success) {
+ migrateSettingsToVault();
+ buildVaultUI();
+ } else {
+ errorDiv.textContent = result.error;
+ errorDiv.style.display = 'block';
+ }
+ }
+ };
+
+ // Allow Enter key to submit
+ const lastInput = confirmInput || passwordInput;
+ lastInput.onkeydown = (e) => {
+ if (e.key === 'Enter') submitBtn.click();
+ };
+
+ vaultSection.appendChild(promptUI);
+ settingsContent.style.display = 'none'; // Hide settings until unlocked
+ }
+ }
+
+ // Show vault settings dialog (change password, delete)
+ function showVaultSettings() {
+ const existingDialog = document.getElementById('vault-settings-dialog');
+ if (existingDialog) existingDialog.remove();
+
+ const dialog = document.createElement('dialog');
+ dialog.id = 'vault-settings-dialog';
+ dialog.style.cssText = 'padding: 20px; border-radius: 8px; max-width: 400px; border: 1px solid var(--uncloseai-border-color, #ccc);';
+ dialog.innerHTML = `
+ 🔑 ${getUIText("vaultChangePassword")}
+
+
+
+
+
+
+
+
+ ${getUIText("vaultDeleteVault")}
+
+
${getUIText("vaultDeleteConfirm")}
+
+
+
+
+
+
+
+
+ `;
+
+ const changeBtn = dialog.querySelector('.change-password-btn');
+ const deleteBtn = dialog.querySelector('.delete-vault-btn');
+ const closeBtn = dialog.querySelector('.close-dialog-btn');
+ const errorDiv = dialog.querySelector('.vault-dialog-error');
+
+ changeBtn.onclick = () => {
+ const current = dialog.querySelector('.current-password').value;
+ const newPass = dialog.querySelector('.new-password').value;
+ const confirm = dialog.querySelector('.confirm-password').value;
+
+ if (newPass.length < 8) {
+ errorDiv.textContent = getUIText("vaultPasswordTooShort");
+ errorDiv.style.display = 'block';
+ return;
+ }
+
+ if (newPass !== confirm) {
+ errorDiv.textContent = getUIText("vaultPasswordMismatch");
+ errorDiv.style.display = 'block';
+ return;
+ }
+
+ const result = UncloseVault.changePassword(current, newPass);
+ if (result.success) {
+ alert(getUIText("vaultPasswordChanged"));
+ dialog.close();
+ dialog.remove();
+ } else {
+ errorDiv.textContent = result.error;
+ errorDiv.style.display = 'block';
+ }
+ };
+
+ deleteBtn.onclick = () => {
+ const password = dialog.querySelector('.delete-password').value;
+ const result = UncloseVault.deleteVault(password);
+
+ if (result.success) {
+ alert(getUIText("vaultDeleted"));
+ dialog.close();
+ dialog.remove();
+ buildVaultUI();
+ } else {
+ errorDiv.textContent = result.error;
+ errorDiv.style.display = 'block';
+ }
+ };
+
+ closeBtn.onclick = () => {
+ dialog.close();
+ dialog.remove();
+ };
+
+ document.body.appendChild(dialog);
+ dialog.showModal();
+ }
+
+ // Migrate existing localStorage settings to vault
+ function migrateSettingsToVault() {
+ if (!UncloseVault.isUnlocked()) return;
+
+ const keysToMigrate = [
+ 'selectedModel', 'selectedEndpoint', 'selectedVoice',
+ 'useCustomAPI', 'customBaseURL', 'customAPIKey',
+ 'useUnsandbox', 'unsandboxPublicKey', 'unsandboxSecretKey',
+ 'hermesSettingsOpen', 'uncloseai_language'
+ ];
+
+ keysToMigrate.forEach(key => {
+ const value = localStorage.getItem(key);
+ if (value !== null) {
+ // Parse booleans
+ let parsedValue = value;
+ if (value === 'true') parsedValue = true;
+ else if (value === 'false') parsedValue = false;
+
+ UncloseVault.set(key, parsedValue);
+ // Remove from plain localStorage after migration
+ localStorage.removeItem(key);
+ }
+ });
+
+ console.log('Settings migrated to vault');
+ }
+
+ // Refresh UI inputs from vault values
+ function refreshSettingsFromVault() {
+ // Model selection will be loaded from vault when loadModels is called
+ // Voice selection will be loaded from vault when loadVoices is called
+ // Language will be loaded from vault
+
+ // Refresh API config
+ if (customAPICheckbox) {
+ customAPICheckbox.checked = getVaultOrStorage('useCustomAPI') === true || getVaultOrStorage('useCustomAPI') === 'true';
+ apiConfigInputs.style.display = customAPICheckbox.checked ? 'block' : 'none';
+ }
+ if (baseURLInput) {
+ baseURLInput.value = getVaultOrStorage('customBaseURL', '');
+ }
+ if (apiKeyInput) {
+ apiKeyInput.value = getVaultOrStorage('customAPIKey', '');
+ }
+
+ // Refresh unsandbox config
+ if (unsandboxCheckbox) {
+ unsandboxCheckbox.checked = getVaultOrStorage('useUnsandbox') === true || getVaultOrStorage('useUnsandbox') === 'true';
+ unsandboxInputs.style.display = unsandboxCheckbox.checked ? 'block' : 'none';
+ }
+ if (unsandboxPublicKeyInput) {
+ unsandboxPublicKeyInput.value = getVaultOrStorage('unsandboxPublicKey', '');
+ }
+ if (unsandboxSecretKeyInput) {
+ unsandboxSecretKeyInput.value = getVaultOrStorage('unsandboxSecretKey', '');
+ }
+ }
+
+ // Initialize vault UI
+ buildVaultUI();
+
// Model selection
const modelSection = document.createElement("div");
modelSection.className = "uncloseai-section";
@@ -306,9 +634,9 @@ async function openUncloseaiEmbeddedModalNew() {
modelSelect.appendChild(option);
});
- // Load saved model from localStorage or use current selection
- const savedModel = localStorage.getItem("selectedModel");
- const savedEndpoint = localStorage.getItem("selectedEndpoint");
+ // Load saved model from vault or use current selection
+ const savedModel = getVaultOrStorage("selectedModel");
+ const savedEndpoint = getVaultOrStorage("selectedEndpoint");
const currentModel = getSelectedModel();
if (savedModel && savedEndpoint) {
@@ -356,9 +684,9 @@ async function openUncloseaiEmbeddedModalNew() {
(model) => model.uniqueId === selectedUniqueId,
);
if (selectedModel) {
- // Update the global model selection
- localStorage.setItem("selectedModel", selectedModel.modelName);
- localStorage.setItem("selectedEndpoint", selectedModel.endpointId);
+ // Update the model selection in vault
+ setVaultValue("selectedModel", selectedModel.modelName);
+ setVaultValue("selectedEndpoint", selectedModel.endpointId);
console.log("Model changed to:", selectedModel);
}
};
@@ -416,8 +744,8 @@ async function openUncloseaiEmbeddedModalNew() {
voiceSelect.appendChild(optgroup);
});
- // Load saved voice from localStorage or default to first voice
- const savedVoice = localStorage.getItem("selectedVoice");
+ // Load saved voice from vault or default to first voice
+ const savedVoice = getVaultOrStorage("selectedVoice");
if (savedVoice && voiceSelect.querySelector(`option[value="${savedVoice}"]`)) {
voiceSelect.value = savedVoice;
}
@@ -425,9 +753,9 @@ async function openUncloseaiEmbeddedModalNew() {
console.log("Voices loaded:", loadedVoicesCount, "voices from", models.length, "models");
};
- // Save voice selection to localStorage when changed
+ // Save voice selection to vault when changed
voiceSelect.onchange = () => {
- localStorage.setItem("selectedVoice", voiceSelect.value);
+ setVaultValue("selectedVoice", voiceSelect.value);
console.log("Voice changed to:", voiceSelect.value);
};
@@ -511,7 +839,7 @@ async function openUncloseaiEmbeddedModalNew() {
const customAPICheckbox = document.createElement("input");
customAPICheckbox.type = "checkbox";
customAPICheckbox.id = "custom-api-toggle";
- customAPICheckbox.checked = localStorage.getItem("useCustomAPI") === "true";
+ customAPICheckbox.checked = getVaultOrStorage("useCustomAPI") === true || getVaultOrStorage("useCustomAPI") === "true";
const customAPIToggleLabel = document.createElement("label");
customAPIToggleLabel.htmlFor = "custom-api-toggle";
@@ -528,7 +856,7 @@ async function openUncloseaiEmbeddedModalNew() {
const baseURLInput = document.createElement("input");
baseURLInput.type = "text";
baseURLInput.placeholder = "https://api.openai.com/v1";
- baseURLInput.value = localStorage.getItem("customBaseURL") || "";
+ baseURLInput.value = getVaultOrStorage("customBaseURL", "");
baseURLInput.className = "uncloseai-input";
baseURLContainer.appendChild(baseURLInput);
@@ -540,7 +868,7 @@ async function openUncloseaiEmbeddedModalNew() {
const apiKeyInput = document.createElement("input");
apiKeyInput.type = "password";
apiKeyInput.placeholder = "API Key";
- apiKeyInput.value = localStorage.getItem("customAPIKey") || "";
+ apiKeyInput.value = getVaultOrStorage("customAPIKey", "");
apiKeyInput.className = "uncloseai-input";
apiKeyContainer.appendChild(apiKeyInput);
@@ -556,16 +884,16 @@ async function openUncloseaiEmbeddedModalNew() {
// Event handlers
customAPICheckbox.onchange = () => {
const useCustom = customAPICheckbox.checked;
- localStorage.setItem("useCustomAPI", useCustom.toString());
+ setVaultValue("useCustomAPI", useCustom);
apiConfigInputs.style.display = useCustom ? "block" : "none";
};
baseURLInput.onchange = () => {
- localStorage.setItem("customBaseURL", baseURLInput.value);
+ setVaultValue("customBaseURL", baseURLInput.value);
};
apiKeyInput.onchange = () => {
- localStorage.setItem("customAPIKey", apiKeyInput.value);
+ setVaultValue("customAPIKey", apiKeyInput.value);
};
@@ -583,7 +911,7 @@ async function openUncloseaiEmbeddedModalNew() {
const unsandboxCheckbox = document.createElement("input");
unsandboxCheckbox.type = "checkbox";
unsandboxCheckbox.id = "unsandbox-toggle";
- unsandboxCheckbox.checked = localStorage.getItem("useUnsandbox") === "true";
+ unsandboxCheckbox.checked = getVaultOrStorage("useUnsandbox") === true || getVaultOrStorage("useUnsandbox") === "true";
const unsandboxToggleLabel = document.createElement("label");
unsandboxToggleLabel.htmlFor = "unsandbox-toggle";
@@ -600,7 +928,7 @@ async function openUncloseaiEmbeddedModalNew() {
const unsandboxPublicKeyInput = document.createElement("input");
unsandboxPublicKeyInput.type = "text";
unsandboxPublicKeyInput.placeholder = getUIText("unsandboxPublicKey");
- unsandboxPublicKeyInput.value = localStorage.getItem("unsandboxPublicKey") || "";
+ unsandboxPublicKeyInput.value = getVaultOrStorage("unsandboxPublicKey", "");
unsandboxPublicKeyInput.className = "uncloseai-input";
unsandboxPublicKeyContainer.appendChild(unsandboxPublicKeyInput);
@@ -612,7 +940,7 @@ async function openUncloseaiEmbeddedModalNew() {
const unsandboxSecretKeyInput = document.createElement("input");
unsandboxSecretKeyInput.type = "password";
unsandboxSecretKeyInput.placeholder = getUIText("unsandboxSecretKey");
- unsandboxSecretKeyInput.value = localStorage.getItem("unsandboxSecretKey") || "";
+ unsandboxSecretKeyInput.value = getVaultOrStorage("unsandboxSecretKey", "");
unsandboxSecretKeyInput.className = "uncloseai-input";
unsandboxSecretKeyContainer.appendChild(unsandboxSecretKeyInput);
@@ -627,16 +955,16 @@ async function openUncloseaiEmbeddedModalNew() {
// Event handlers for unsandbox
unsandboxCheckbox.onchange = () => {
const useUnsandbox = unsandboxCheckbox.checked;
- localStorage.setItem("useUnsandbox", useUnsandbox.toString());
+ setVaultValue("useUnsandbox", useUnsandbox);
unsandboxInputs.style.display = useUnsandbox ? "block" : "none";
};
unsandboxPublicKeyInput.onchange = () => {
- localStorage.setItem("unsandboxPublicKey", unsandboxPublicKeyInput.value);
+ setVaultValue("unsandboxPublicKey", unsandboxPublicKeyInput.value);
};
unsandboxSecretKeyInput.onchange = () => {
- localStorage.setItem("unsandboxSecretKey", unsandboxSecretKeyInput.value);
+ setVaultValue("unsandboxSecretKey", unsandboxSecretKeyInput.value);
};
unsandboxSection.appendChild(unsandboxToggle);
@@ -725,12 +1053,20 @@ async function openUncloseaiEmbeddedModalNew() {
// Quick actions span both columns
actionsSection.className = "uncloseai-section uncloseai-actions-full-width";
- settingsPanel.appendChild(actionsSection);
- settingsPanel.appendChild(leftColumn);
- settingsPanel.appendChild(rightColumn);
+ // Wrap settings in settingsContent container (shown/hidden based on vault state)
+ settingsContent.appendChild(actionsSection);
+ settingsContent.appendChild(leftColumn);
+ settingsContent.appendChild(rightColumn);
+ settingsContent.style.display = 'grid';
+ settingsContent.style.gridTemplateColumns = 'repeat(auto-fit, minmax(200px, 1fr))';
+ settingsContent.style.gap = '16px';
+
+ // Add vault section first, then settings content
+ settingsPanel.appendChild(vaultSection);
+ settingsPanel.appendChild(settingsContent);
// Load and save settings panel state
- let settingsOpen = localStorage.getItem("hermesSettingsOpen") === "true";
+ let settingsOpen = getVaultOrStorage("hermesSettingsOpen") === true || getVaultOrStorage("hermesSettingsOpen") === "true";
if (settingsOpen) {
settingsPanel.classList.add("open");
}
@@ -738,7 +1074,7 @@ async function openUncloseaiEmbeddedModalNew() {
menuBtn.onclick = () => {
settingsOpen = !settingsOpen;
settingsPanel.classList.toggle("open", settingsOpen);
- localStorage.setItem("hermesSettingsOpen", settingsOpen.toString());
+ setVaultValue("hermesSettingsOpen", settingsOpen);
};
// Set close button handler now that settings panel is available
@@ -747,10 +1083,10 @@ async function openUncloseaiEmbeddedModalNew() {
if (settingsOpen) {
settingsOpen = false;
settingsPanel.classList.toggle("open", settingsOpen);
- localStorage.setItem("hermesSettingsOpen", settingsOpen.toString());
+ setVaultValue("hermesSettingsOpen", settingsOpen);
return; // Don't close modal yet
}
-
+
// Settings closed or not open, close the modal
modal.close();
document.body.removeChild(modal);
diff --git a/public/src/vault.js b/public/src/vault.js
new file mode 100644
index 0000000..428735e
--- /dev/null
+++ b/public/src/vault.js
@@ -0,0 +1,527 @@
+/**
+ * UncloseAI Vault - Encrypted Settings Storage
+ *
+ * All settings are encrypted with a user-chosen password using AES-256.
+ * Session persistence allows staying unlocked across page navigations.
+ *
+ * Requires CryptoJS library.
+ *
+ * Flow:
+ * 1. User creates vault with password (first time)
+ * 2. Settings encrypted with password, stored in localStorage
+ * 3. Session key derived and stored (encrypted) for auto-unlock
+ * 4. On page load, session key auto-unlocks if not expired
+ * 5. User can explicitly lock or session expires after TTL
+ */
+
+const UncloseVault = {
+ // localStorage keys
+ VAULT_KEY: 'uncloseai_vault',
+ SALT_KEY: 'uncloseai_device_salt',
+ SESSION_KEY: 'uncloseai_session',
+
+ // Session TTL (7 days in milliseconds)
+ SESSION_TTL_MS: 7 * 24 * 60 * 60 * 1000,
+
+ // In-memory state
+ _settings: null,
+ _password: null,
+
+ /**
+ * Check if CryptoJS is available
+ */
+ isAvailable() {
+ return typeof CryptoJS !== 'undefined';
+ },
+
+ /**
+ * Get or create device-specific salt (32 bytes)
+ */
+ getDeviceSalt() {
+ let salt = localStorage.getItem(this.SALT_KEY);
+ if (!salt) {
+ if (!this.isAvailable()) return null;
+ const randomBytes = CryptoJS.lib.WordArray.random(32);
+ salt = randomBytes.toString();
+ localStorage.setItem(this.SALT_KEY, salt);
+ }
+ return salt;
+ },
+
+ /**
+ * Generate vault ID from password (deterministic per device)
+ */
+ getVaultId(password) {
+ if (!this.isAvailable()) return null;
+ const salt = this.getDeviceSalt();
+ return CryptoJS.SHA256(password + salt).toString();
+ },
+
+ /**
+ * Encrypt data with key
+ */
+ encrypt(data, key) {
+ if (!this.isAvailable()) return null;
+ try {
+ return CryptoJS.AES.encrypt(JSON.stringify(data), key).toString();
+ } catch (e) {
+ console.error('Vault encryption failed:', e);
+ return null;
+ }
+ },
+
+ /**
+ * Decrypt data with key
+ */
+ decrypt(encryptedData, key) {
+ if (!this.isAvailable()) return null;
+ try {
+ const bytes = CryptoJS.AES.decrypt(encryptedData, key);
+ const decryptedStr = bytes.toString(CryptoJS.enc.Utf8);
+ if (!decryptedStr) return null;
+ return JSON.parse(decryptedStr);
+ } catch (e) {
+ return null;
+ }
+ },
+
+ /**
+ * Check if a vault exists
+ */
+ hasVault() {
+ const vaultData = localStorage.getItem(this.VAULT_KEY);
+ if (!vaultData) return false;
+ try {
+ const vault = JSON.parse(vaultData);
+ return !!vault.encrypted_settings;
+ } catch (e) {
+ return false;
+ }
+ },
+
+ /**
+ * Check if vault is currently unlocked (in memory)
+ */
+ isUnlocked() {
+ return this._settings !== null;
+ },
+
+ /**
+ * Get vault data from localStorage
+ */
+ getVault() {
+ try {
+ const vaultJson = localStorage.getItem(this.VAULT_KEY);
+ return vaultJson ? JSON.parse(vaultJson) : null;
+ } catch (e) {
+ return null;
+ }
+ },
+
+ /**
+ * Save vault data to localStorage
+ */
+ saveVault(vault) {
+ localStorage.setItem(this.VAULT_KEY, JSON.stringify(vault));
+ },
+
+ /**
+ * Create session for auto-unlock persistence
+ * Stores the password encrypted with a random session key
+ */
+ createSession(password) {
+ if (!this.isAvailable()) return;
+
+ try {
+ // Generate random session key
+ const sessionKey = CryptoJS.lib.WordArray.random(32).toString();
+
+ // Encrypt password with session key
+ const encryptedPassword = CryptoJS.AES.encrypt(password, sessionKey).toString();
+
+ // Store session data
+ const sessionData = {
+ key: sessionKey,
+ data: encryptedPassword,
+ expires: Date.now() + this.SESSION_TTL_MS,
+ created: Date.now()
+ };
+
+ localStorage.setItem(this.SESSION_KEY, JSON.stringify(sessionData));
+ } catch (e) {
+ console.error('Failed to create session:', e);
+ }
+ },
+
+ /**
+ * Try to restore session (auto-unlock)
+ * Returns true if session was valid and vault unlocked
+ */
+ tryRestoreSession() {
+ if (!this.isAvailable()) return false;
+ if (this.isUnlocked()) return true; // Already unlocked
+
+ try {
+ const sessionJson = localStorage.getItem(this.SESSION_KEY);
+ if (!sessionJson) return false;
+
+ const session = JSON.parse(sessionJson);
+
+ // Check expiry
+ if (Date.now() > session.expires) {
+ this.clearSession();
+ return false;
+ }
+
+ // Decrypt password from session
+ const bytes = CryptoJS.AES.decrypt(session.data, session.key);
+ const password = bytes.toString(CryptoJS.enc.Utf8);
+
+ if (!password) {
+ this.clearSession();
+ return false;
+ }
+
+ // Try to unlock with recovered password
+ const result = this._unlockWithPassword(password, false); // Don't recreate session
+
+ if (result.success) {
+ // Refresh session expiry on successful restore
+ session.expires = Date.now() + this.SESSION_TTL_MS;
+ localStorage.setItem(this.SESSION_KEY, JSON.stringify(session));
+ return true;
+ }
+
+ this.clearSession();
+ return false;
+ } catch (e) {
+ console.error('Session restore failed:', e);
+ this.clearSession();
+ return false;
+ }
+ },
+
+ /**
+ * Clear session data
+ */
+ clearSession() {
+ localStorage.removeItem(this.SESSION_KEY);
+ },
+
+ /**
+ * Get session info (for UI display)
+ */
+ getSessionInfo() {
+ try {
+ const sessionJson = localStorage.getItem(this.SESSION_KEY);
+ if (!sessionJson) return null;
+
+ const session = JSON.parse(sessionJson);
+ const now = Date.now();
+
+ if (now > session.expires) return null;
+
+ return {
+ expiresAt: new Date(session.expires),
+ expiresIn: session.expires - now,
+ createdAt: new Date(session.created)
+ };
+ } catch (e) {
+ return null;
+ }
+ },
+
+ /**
+ * Internal unlock with password
+ */
+ _unlockWithPassword(password, createNewSession = true) {
+ if (!this.isAvailable()) {
+ return { success: false, error: 'CryptoJS not available' };
+ }
+
+ const vault = this.getVault();
+ if (!vault || !vault.encrypted_settings) {
+ return { success: false, error: 'No vault found' };
+ }
+
+ const decrypted = this.decrypt(vault.encrypted_settings, password);
+ if (!decrypted) {
+ return { success: false, error: 'Wrong password' };
+ }
+
+ // Store in memory
+ this._settings = decrypted;
+ this._password = password;
+
+ // Create session for persistence
+ if (createNewSession) {
+ this.createSession(password);
+ }
+
+ // Dispatch event
+ window.dispatchEvent(new CustomEvent('uncloseai-vault-unlocked'));
+
+ return { success: true };
+ },
+
+ /**
+ * Create a new vault with password
+ */
+ create(password) {
+ if (!this.isAvailable()) {
+ return { success: false, error: 'CryptoJS not available. Include crypto-js library.' };
+ }
+ if (!password || password.length < 8) {
+ return { success: false, error: 'Password must be at least 8 characters' };
+ }
+
+ if (this.hasVault()) {
+ return { success: false, error: 'Vault already exists. Use unlock() instead.' };
+ }
+
+ // Create with default settings
+ const defaultSettings = this.getDefaultSettings();
+ const encrypted = this.encrypt(defaultSettings, password);
+
+ if (!encrypted) {
+ return { success: false, error: 'Encryption failed' };
+ }
+
+ const vault = {
+ encrypted_settings: encrypted,
+ created_at: new Date().toISOString(),
+ updated_at: new Date().toISOString()
+ };
+
+ this.saveVault(vault);
+
+ // Unlock in memory
+ this._settings = defaultSettings;
+ this._password = password;
+
+ // Create session
+ this.createSession(password);
+
+ window.dispatchEvent(new CustomEvent('uncloseai-vault-created'));
+ window.dispatchEvent(new CustomEvent('uncloseai-vault-unlocked'));
+
+ return { success: true };
+ },
+
+ /**
+ * Unlock vault with password
+ */
+ unlock(password) {
+ if (!password) {
+ return { success: false, error: 'Password required' };
+ }
+ return this._unlockWithPassword(password, true);
+ },
+
+ /**
+ * Lock the vault (clear memory and session)
+ */
+ lock() {
+ this._settings = null;
+ this._password = null;
+ this.clearSession();
+ window.dispatchEvent(new CustomEvent('uncloseai-vault-locked'));
+ },
+
+ /**
+ * Save current settings to vault
+ */
+ save() {
+ if (!this.isUnlocked()) {
+ console.error('Cannot save: vault not unlocked');
+ return false;
+ }
+
+ const vault = this.getVault() || {};
+ const encrypted = this.encrypt(this._settings, this._password);
+
+ if (!encrypted) {
+ console.error('Failed to encrypt settings');
+ return false;
+ }
+
+ vault.encrypted_settings = encrypted;
+ vault.updated_at = new Date().toISOString();
+ this.saveVault(vault);
+
+ return true;
+ },
+
+ /**
+ * Get a setting value (returns default if vault locked)
+ */
+ get(key, defaultValue = null) {
+ if (!this.isUnlocked()) {
+ // Return from defaults if not unlocked
+ const defaults = this.getDefaultSettings();
+ return defaults[key] !== undefined ? defaults[key] : defaultValue;
+ }
+ return this._settings[key] !== undefined ? this._settings[key] : defaultValue;
+ },
+
+ /**
+ * Set a setting value (requires unlocked vault)
+ */
+ set(key, value) {
+ if (!this.isUnlocked()) {
+ console.warn('Vault locked - setting not saved');
+ return false;
+ }
+ this._settings[key] = value;
+ return this.save();
+ },
+
+ /**
+ * Set multiple settings at once
+ */
+ setMultiple(settingsObj) {
+ if (!this.isUnlocked()) {
+ console.warn('Vault locked - settings not saved');
+ return false;
+ }
+ Object.assign(this._settings, settingsObj);
+ return this.save();
+ },
+
+ /**
+ * Get all settings
+ */
+ getAll() {
+ if (!this.isUnlocked()) return this.getDefaultSettings();
+ return { ...this._settings };
+ },
+
+ /**
+ * Default settings (used when vault doesn't exist or is locked)
+ */
+ getDefaultSettings() {
+ return {
+ // Language
+ language: 'en',
+
+ // Model selection
+ selectedModel: null,
+ selectedEndpoint: null,
+ selectedVoice: 'tts-1:onyx',
+
+ // Custom API
+ useCustomAPI: false,
+ customBaseURL: '',
+ customAPIKey: '',
+
+ // Unsandbox code execution
+ useUnsandbox: false,
+ unsandboxPublicKey: '',
+ unsandboxSecretKey: '',
+
+ // UI preferences
+ settingsOpen: false
+ };
+ },
+
+ /**
+ * Delete vault entirely (requires password confirmation)
+ */
+ deleteVault(password) {
+ if (!this.isAvailable()) {
+ return { success: false, error: 'CryptoJS not available' };
+ }
+
+ const vault = this.getVault();
+ if (!vault) {
+ return { success: false, error: 'No vault exists' };
+ }
+
+ // Verify password
+ const decrypted = this.decrypt(vault.encrypted_settings, password);
+ if (!decrypted) {
+ return { success: false, error: 'Wrong password' };
+ }
+
+ // Clear everything
+ localStorage.removeItem(this.VAULT_KEY);
+ this.clearSession();
+ this._settings = null;
+ this._password = null;
+
+ window.dispatchEvent(new CustomEvent('uncloseai-vault-deleted'));
+
+ return { success: true };
+ },
+
+ /**
+ * Change vault password
+ */
+ changePassword(currentPassword, newPassword) {
+ if (!this.isAvailable()) {
+ return { success: false, error: 'CryptoJS not available' };
+ }
+ if (!newPassword || newPassword.length < 8) {
+ return { success: false, error: 'New password must be at least 8 characters' };
+ }
+
+ const vault = this.getVault();
+ if (!vault) {
+ return { success: false, error: 'No vault exists' };
+ }
+
+ // Decrypt with current password
+ const decrypted = this.decrypt(vault.encrypted_settings, currentPassword);
+ if (!decrypted) {
+ return { success: false, error: 'Wrong current password' };
+ }
+
+ // Re-encrypt with new password
+ const encrypted = this.encrypt(decrypted, newPassword);
+ if (!encrypted) {
+ return { success: false, error: 'Re-encryption failed' };
+ }
+
+ vault.encrypted_settings = encrypted;
+ vault.updated_at = new Date().toISOString();
+ this.saveVault(vault);
+
+ // Update in-memory and session
+ this._password = newPassword;
+ this.createSession(newPassword);
+
+ return { success: true };
+ },
+
+ /**
+ * Initialize - try to restore session on page load
+ * Call this when the page loads
+ */
+ init() {
+ if (!this.isAvailable()) {
+ console.warn('UncloseVault: CryptoJS not available');
+ return false;
+ }
+
+ if (this.hasVault()) {
+ // Try auto-unlock from session
+ if (this.tryRestoreSession()) {
+ console.log('UncloseVault: Session restored, vault unlocked');
+ return true;
+ }
+ console.log('UncloseVault: Vault exists but locked (no valid session)');
+ return false;
+ }
+
+ console.log('UncloseVault: No vault exists yet');
+ return false;
+ }
+};
+
+// Export for ES modules
+export { UncloseVault };
+
+// Also make available globally
+if (typeof window !== 'undefined') {
+ window.UncloseVault = UncloseVault;
+}