From b5f950c9a27a032594ae6318bc48f35bb96de292 Mon Sep 17 00:00:00 2001 From: fainashalts Date: Mon, 5 Jan 2026 16:50:38 -0800 Subject: [PATCH 01/12] add passphrase encryption for wallet export in iframe --- export/index.template.html | 314 +++++++++++++++++++++++++++++++++++++ export/index.test.js | 97 ++++++++++++ 2 files changed, 411 insertions(+) diff --git a/export/index.template.html b/export/index.template.html index e9d2324..32a3eba 100644 --- a/export/index.template.html +++ b/export/index.template.html @@ -75,6 +75,64 @@ .hidden { display: none; } + #passphrase-form-div { + text-align: center; + max-width: 500px; + margin: 2em auto; + padding: 1.5em; + border: 1px solid #ddd; + border-radius: 8px; + background-color: #fafafa; + } + #passphrase-form-div h2 { + margin-top: 0; + color: #333; + } + #passphrase-form-div p { + color: #555; + font-size: 0.9em; + margin-bottom: 1.5em; + } + #passphrase-form-div label { + display: block; + text-align: left; + margin: 0.5em 0 0.25em 0; + font-weight: bold; + color: #444; + } + #passphrase-form-div input[type="password"] { + width: 100%; + padding: 0.6em; + font-size: 1em; + border: 1px solid #ccc; + border-radius: 4px; + box-sizing: border-box; + margin-bottom: 0.5em; + } + #passphrase-form-div input[type="password"]:focus { + outline: none; + border-color: #666; + } + #encrypt-and-export { + color: white; + width: 100%; + font-size: 1em; + padding: 0.75em; + margin-top: 1em; + border-radius: 4px; + background-color: rgb(50, 44, 44); + border: 1px rgb(33, 33, 33) solid; + cursor: pointer; + } + #encrypt-and-export:hover { + background-color: rgb(70, 64, 64); + } + #passphrase-error { + color: #c0392b; + font-size: 0.9em; + margin: 0.5em 0; + text-align: left; + } @@ -1191,6 +1249,102 @@

Message log

return JSON.stringify(validSettings); } + /** + * Encrypts a Uint8Array using PBKDF2 key derivation and AES-GCM-256 encryption. + * @param {Uint8Array} buf - The data to encrypt + * @param {string} passphrase - The passphrase to derive the key from + * @returns {Promise} - Concatenated salt || iv || ciphertext + */ + async function encryptWithPassphrase(buf, passphrase) { + // Generate random 16-byte salt and 12-byte IV + const salt = crypto.getRandomValues(new Uint8Array(16)); + const iv = crypto.getRandomValues(new Uint8Array(12)); + + // Import passphrase as PBKDF2 key material + const keyMaterial = await crypto.subtle.importKey( + "raw", + new TextEncoder().encode(passphrase), + "PBKDF2", + false, + ["deriveBits", "deriveKey"] + ); + + // Derive AES-256 key using PBKDF2 (100,000 iterations, SHA-256) + const aesKey = await crypto.subtle.deriveKey( + { + name: "PBKDF2", + salt: salt, + iterations: 100000, + hash: "SHA-256", + }, + keyMaterial, + { name: "AES-GCM", length: 256 }, + false, + ["encrypt"] + ); + + // Encrypt using AES-GCM + const ciphertext = await crypto.subtle.encrypt( + { name: "AES-GCM", iv: iv }, + aesKey, + buf + ); + + // Return concatenated salt || iv || ciphertext + const result = new Uint8Array( + salt.length + iv.length + ciphertext.byteLength + ); + result.set(salt, 0); + result.set(iv, salt.length); + result.set(new Uint8Array(ciphertext), salt.length + iv.length); + return result; + } + + /** + * Decrypts a buffer encrypted by encryptWithPassphrase. + * @param {Uint8Array} encryptedBuf - The encrypted data (salt || iv || ciphertext) + * @param {string} passphrase - The passphrase to derive the key from + * @returns {Promise} - The decrypted data + */ + async function decryptWithPassphrase(encryptedBuf, passphrase) { + // Extract salt (bytes 0-16), iv (bytes 16-28), ciphertext (bytes 28+) + const salt = encryptedBuf.slice(0, 16); + const iv = encryptedBuf.slice(16, 28); + const ciphertext = encryptedBuf.slice(28); + + // Import passphrase as PBKDF2 key material + const keyMaterial = await crypto.subtle.importKey( + "raw", + new TextEncoder().encode(passphrase), + "PBKDF2", + false, + ["deriveBits", "deriveKey"] + ); + + // Derive same AES key using PBKDF2 + const aesKey = await crypto.subtle.deriveKey( + { + name: "PBKDF2", + salt: salt, + iterations: 100000, + hash: "SHA-256", + }, + keyMaterial, + { name: "AES-GCM", length: 256 }, + false, + ["decrypt"] + ); + + // Decrypt using AES-GCM + const decrypted = await crypto.subtle.decrypt( + { name: "AES-GCM", iv: iv }, + aesKey, + ciphertext + ); + + return new Uint8Array(decrypted); + } + return { initEmbeddedKey, generateTargetKey, @@ -1221,6 +1375,8 @@

Message log

validateStyles, getSettings, setSettings, + encryptWithPassphrase, + decryptWithPassphrase, }; })(); @@ -1312,6 +1468,20 @@

Message log

TKHQ.sendMessageUp("ERROR", e.toString(), event.data["requestId"]); } } + if (event.data && event.data["type"] == "INJECT_WALLET_EXPORT_BUNDLE_ENCRYPTED") { + TKHQ.logMessage( + `⬇️ Received message ${event.data["type"]}: ${event.data["value"]}, ${event.data["organizationId"]}` + ); + try { + await onInjectWalletBundleEncrypted( + event.data["value"], + event.data["organizationId"], + event.data["requestId"] + ); + } catch (e) { + TKHQ.sendMessageUp("ERROR", e.toString(), event.data["requestId"]); + } + } if (event.data && event.data["type"] == "APPLY_SETTINGS") { try { await onApplySettings(event.data["value"], event.data["requestId"]); @@ -1432,6 +1602,126 @@

Message log

TKHQ.applySettings(TKHQ.getSettings()); } + /** + * Display a passphrase form to encrypt the mnemonic before exporting. + * @param {string} mnemonic - The wallet mnemonic to encrypt + * @param {string} requestId - The request ID for message correlation + */ + function displayPassphraseForm(mnemonic, requestId) { + // Hide all existing DOM elements except scripts + Array.from(document.body.children).forEach((child) => { + if (child.tagName !== "SCRIPT") { + child.style.display = "none"; + } + }); + + // Create the passphrase form container + const formDiv = document.createElement("div"); + formDiv.id = "passphrase-form-div"; + + // Create heading + const heading = document.createElement("h2"); + heading.innerText = "Encrypt Your Wallet Export"; + formDiv.appendChild(heading); + + // Create description + const description = document.createElement("p"); + description.innerText = + "Enter a passphrase to encrypt your wallet mnemonic. You will need this passphrase to decrypt your wallet later."; + formDiv.appendChild(description); + + // Create passphrase input + const passphraseLabel = document.createElement("label"); + passphraseLabel.setAttribute("for", "export-passphrase"); + passphraseLabel.innerText = "Passphrase"; + formDiv.appendChild(passphraseLabel); + + const passphraseInput = document.createElement("input"); + passphraseInput.type = "password"; + passphraseInput.id = "export-passphrase"; + passphraseInput.placeholder = "Enter passphrase (min 8 characters)"; + formDiv.appendChild(passphraseInput); + + // Create confirmation input + const confirmLabel = document.createElement("label"); + confirmLabel.setAttribute("for", "export-passphrase-confirm"); + confirmLabel.innerText = "Confirm Passphrase"; + formDiv.appendChild(confirmLabel); + + const confirmInput = document.createElement("input"); + confirmInput.type = "password"; + confirmInput.id = "export-passphrase-confirm"; + confirmInput.placeholder = "Confirm passphrase"; + formDiv.appendChild(confirmInput); + + // Create error message paragraph + const errorMsg = document.createElement("p"); + errorMsg.id = "passphrase-error"; + errorMsg.style.display = "none"; + formDiv.appendChild(errorMsg); + + // Create submit button + const submitButton = document.createElement("button"); + submitButton.type = "button"; + submitButton.id = "encrypt-and-export"; + submitButton.innerText = "Encrypt & Export"; + formDiv.appendChild(submitButton); + + // Append the form to the body + document.body.appendChild(formDiv); + + // Add click event listener to the submit button + submitButton.addEventListener("click", async () => { + const passphrase = passphraseInput.value; + const confirmPassphrase = confirmInput.value; + + // Validate minimum passphrase length (8 characters) + if (passphrase.length < 8) { + errorMsg.innerText = + "Passphrase must be at least 8 characters long."; + errorMsg.style.display = "block"; + return; + } + + // Validate passphrases match + if (passphrase !== confirmPassphrase) { + errorMsg.innerText = "Passphrases do not match."; + errorMsg.style.display = "block"; + return; + } + + // Hide error message + errorMsg.style.display = "none"; + + try { + // Encode mnemonic to Uint8Array + const encoder = new TextEncoder(); + const mnemonicBytes = encoder.encode(mnemonic); + + // Encrypt with passphrase + const encryptedBytes = await TKHQ.encryptWithPassphrase( + mnemonicBytes, + passphrase + ); + + // Convert to base64 + const encryptedBase64 = btoa( + String.fromCharCode.apply(null, encryptedBytes) + ); + + // Send message up + TKHQ.sendMessageUp( + "ENCRYPTED_WALLET_EXPORT", + encryptedBase64, + requestId + ); + } catch (e) { + errorMsg.innerText = "Encryption failed: " + e.toString(); + errorMsg.style.display = "block"; + } + }); + } + /** * Parse and decrypt the export bundle. * The `bundle` param is a JSON string of the encapsulated public @@ -1585,6 +1875,30 @@

Message log

TKHQ.sendMessageUp("BUNDLE_INJECTED", true, requestId); } + /** + * Function triggered when INJECT_WALLET_EXPORT_BUNDLE_ENCRYPTED event is received. + * @param {string} bundle + * @param {string} organizationId + * @param {string} requestId + */ + async function onInjectWalletBundleEncrypted( + bundle, + organizationId, + requestId + ) { + // Decrypt the export bundle + const walletBytes = await decryptBundle(bundle, organizationId); + + // Reset embedded key after using for decryption + TKHQ.onResetEmbeddedKey(); + + // Parse the decrypted wallet bytes + const wallet = TKHQ.encodeWallet(new Uint8Array(walletBytes)); + + // Display passphrase form instead of showing the key directly + displayPassphraseForm(wallet.mnemonic, requestId); + } + /** * Function triggered when APPLY_SETTINGS event is received. * For now, the only settings that can be applied are for "styles". diff --git a/export/index.test.js b/export/index.test.js index 79648fe..f6ab366 100644 --- a/export/index.test.js +++ b/export/index.test.js @@ -446,4 +446,101 @@ describe("TKHQ", () => { }; expect(TKHQ.validateStyles(allStylesValid)).toEqual(allStylesValid); }); + + it("encrypts data with passphrase correctly", async () => { + const plaintext = new TextEncoder().encode("test mnemonic phrase"); + const passphrase = "securepassword123"; + + const encrypted = await TKHQ.encryptWithPassphrase(plaintext, passphrase); + + // Result should be salt (16) + iv (12) + ciphertext (at least as long as plaintext + auth tag) + // Note: Using ArrayBuffer check due to JSDOM cross-realm Uint8Array difference + expect(encrypted.buffer).toBeDefined(); + expect(encrypted.length).toBeGreaterThanOrEqual(16 + 12 + plaintext.length); + + // Salt and IV should be present at the beginning + const salt = encrypted.slice(0, 16); + const iv = encrypted.slice(16, 28); + expect(salt.length).toBe(16); + expect(iv.length).toBe(12); + }); + + it("decrypts data encrypted by encryptWithPassphrase correctly", async () => { + const originalText = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; + const plaintext = new TextEncoder().encode(originalText); + const passphrase = "mySecurePassphrase!"; + + // Encrypt + const encrypted = await TKHQ.encryptWithPassphrase(plaintext, passphrase); + + // Decrypt + const decrypted = await TKHQ.decryptWithPassphrase(encrypted, passphrase); + + // Verify + const decryptedText = new TextDecoder().decode(decrypted); + expect(decryptedText).toBe(originalText); + }); + + it("fails to decrypt with wrong passphrase", async () => { + const plaintext = new TextEncoder().encode("secret data"); + const correctPassphrase = "correctPassphrase"; + const wrongPassphrase = "wrongPassphrase"; + + const encrypted = await TKHQ.encryptWithPassphrase(plaintext, correctPassphrase); + + // Attempting to decrypt with wrong passphrase should throw + await expect( + TKHQ.decryptWithPassphrase(encrypted, wrongPassphrase) + ).rejects.toThrow(); + }); + + it("produces different ciphertext for same plaintext (due to random salt/IV)", async () => { + const plaintext = new TextEncoder().encode("same plaintext"); + const passphrase = "samePassphrase"; + + const encrypted1 = await TKHQ.encryptWithPassphrase(plaintext, passphrase); + const encrypted2 = await TKHQ.encryptWithPassphrase(plaintext, passphrase); + + // Encrypted results should be different due to random salt and IV + expect(TKHQ.uint8arrayToHexString(encrypted1)).not.toBe( + TKHQ.uint8arrayToHexString(encrypted2) + ); + + // But both should decrypt to the same plaintext + const decrypted1 = await TKHQ.decryptWithPassphrase(encrypted1, passphrase); + const decrypted2 = await TKHQ.decryptWithPassphrase(encrypted2, passphrase); + + expect(new TextDecoder().decode(decrypted1)).toBe("same plaintext"); + expect(new TextDecoder().decode(decrypted2)).toBe("same plaintext"); + }); + + it("handles encryption of wallet mnemonic end-to-end", async () => { + const mnemonic = + "suffer surround soup duck goose patrol add unveil appear eye neglect hurry alpha project tomorrow embody hen wish twenty join notable amused burden treat"; + const passphrase = "strongPassphrase123!"; + + // Encode mnemonic to bytes + const mnemonicBytes = new TextEncoder().encode(mnemonic); + + // Encrypt + const encrypted = await TKHQ.encryptWithPassphrase(mnemonicBytes, passphrase); + + // Convert to base64 (as would be done in displayPassphraseForm) + const encryptedBase64 = btoa(String.fromCharCode.apply(null, encrypted)); + expect(typeof encryptedBase64).toBe("string"); + expect(encryptedBase64.length).toBeGreaterThan(0); + + // Convert back from base64 + const encryptedFromBase64 = new Uint8Array( + atob(encryptedBase64) + .split("") + .map((c) => c.charCodeAt(0)) + ); + + // Decrypt + const decrypted = await TKHQ.decryptWithPassphrase(encryptedFromBase64, passphrase); + const decryptedMnemonic = new TextDecoder().decode(decrypted); + + expect(decryptedMnemonic).toBe(mnemonic); + }); }); From 6af2ee2e1af319635ffc0b197b89d24e18ca555a Mon Sep 17 00:00:00 2001 From: Faina Shalts Date: Mon, 5 Jan 2026 21:14:45 -0800 Subject: [PATCH 02/12] Update export/index.template.html Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- export/index.template.html | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/export/index.template.html b/export/index.template.html index 32a3eba..f84dbd0 100644 --- a/export/index.template.html +++ b/export/index.template.html @@ -1269,12 +1269,14 @@

Message log

["deriveBits", "deriveKey"] ); - // Derive AES-256 key using PBKDF2 (100,000 iterations, SHA-256) + // Derive AES-256 key using PBKDF2 (600,000 iterations, SHA-256). + // NOTE: The iteration count must match during decryption; changing it + // affects compatibility with data encrypted using a different value. const aesKey = await crypto.subtle.deriveKey( { name: "PBKDF2", salt: salt, - iterations: 100000, + iterations: 600000, hash: "SHA-256", }, keyMaterial, From 26665e7407b5a0b645257769b5162dd6ebd8871e Mon Sep 17 00:00:00 2001 From: fainashalts Date: Tue, 6 Jan 2026 07:04:43 -0800 Subject: [PATCH 03/12] make encrypt & decrypt iterations match --- export/index.template.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/export/index.template.html b/export/index.template.html index f84dbd0..37d3a95 100644 --- a/export/index.template.html +++ b/export/index.template.html @@ -1328,7 +1328,7 @@

Message log

{ name: "PBKDF2", salt: salt, - iterations: 100000, + iterations: 600000, hash: "SHA-256", }, keyMaterial, From e698e3791569dd138c8b5c126ee6ace34b78e2ae Mon Sep 17 00:00:00 2001 From: fainashalts Date: Tue, 6 Jan 2026 07:12:18 -0800 Subject: [PATCH 04/12] require passphrase when exporting with encryption --- export/index.template.html | 5 + export/index.test.js | 269 +++++++++++++++++++++++++++++++++++++ 2 files changed, 274 insertions(+) diff --git a/export/index.template.html b/export/index.template.html index 37d3a95..edf40b5 100644 --- a/export/index.template.html +++ b/export/index.template.html @@ -1642,6 +1642,9 @@

Message log

passphraseInput.type = "password"; passphraseInput.id = "export-passphrase"; passphraseInput.placeholder = "Enter passphrase (min 8 characters)"; + passphraseInput.required = true; + passphraseInput.setAttribute("aria-required", "true"); + passphraseInput.minLength = 8; formDiv.appendChild(passphraseInput); // Create confirmation input @@ -1654,6 +1657,8 @@

Message log

confirmInput.type = "password"; confirmInput.id = "export-passphrase-confirm"; confirmInput.placeholder = "Confirm passphrase"; + confirmInput.required = true; + confirmInput.setAttribute("aria-required", "true"); formDiv.appendChild(confirmInput); // Create error message paragraph diff --git a/export/index.test.js b/export/index.test.js index f6ab366..c0d1df7 100644 --- a/export/index.test.js +++ b/export/index.test.js @@ -544,3 +544,272 @@ describe("TKHQ", () => { expect(decryptedMnemonic).toBe(mnemonic); }); }); + +/** + * Tests for passphrase form validation + * These tests create the form elements manually and test the validation logic + */ +describe("Passphrase Form Validation", () => { + let dom; + let document; + let TKHQ; + + // Helper to create the passphrase form elements (mimics displayPassphraseForm) + function createPassphraseForm() { + const formDiv = document.createElement("div"); + formDiv.id = "passphrase-form-div"; + + const passphraseInput = document.createElement("input"); + passphraseInput.type = "password"; + passphraseInput.id = "export-passphrase"; + formDiv.appendChild(passphraseInput); + + const confirmInput = document.createElement("input"); + confirmInput.type = "password"; + confirmInput.id = "export-passphrase-confirm"; + formDiv.appendChild(confirmInput); + + const errorMsg = document.createElement("p"); + errorMsg.id = "passphrase-error"; + errorMsg.style.display = "none"; + formDiv.appendChild(errorMsg); + + const submitButton = document.createElement("button"); + submitButton.type = "button"; + submitButton.id = "encrypt-and-export"; + formDiv.appendChild(submitButton); + + document.body.appendChild(formDiv); + + return { formDiv, passphraseInput, confirmInput, errorMsg, submitButton }; + } + + // Helper to create click handler that mimics displayPassphraseForm logic + function addValidationHandler(elements, mnemonic, onSuccess) { + const { passphraseInput, confirmInput, errorMsg, submitButton } = elements; + + submitButton.addEventListener("click", async () => { + const passphrase = passphraseInput.value; + const confirmPassphrase = confirmInput.value; + + // Validate minimum passphrase length (8 characters) + if (passphrase.length < 8) { + errorMsg.innerText = "Passphrase must be at least 8 characters long."; + errorMsg.style.display = "block"; + return; + } + + // Validate passphrases match + if (passphrase !== confirmPassphrase) { + errorMsg.innerText = "Passphrases do not match."; + errorMsg.style.display = "block"; + return; + } + + // Hide error message + errorMsg.style.display = "none"; + + if (onSuccess) { + await onSuccess(passphrase); + } + }); + } + + beforeEach(() => { + dom = new JSDOM(html, { + runScripts: "dangerously", + url: "http://localhost", + beforeParse(window) { + window.TextDecoder = TextDecoder; + window.TextEncoder = TextEncoder; + }, + }); + + Object.defineProperty(dom.window, "crypto", { + value: crypto.webcrypto, + }); + + document = dom.window.document; + TKHQ = dom.window.TKHQ; + }); + + it("shows error when passphrase is too short", () => { + const elements = createPassphraseForm(); + addValidationHandler(elements, "test mnemonic"); + + // Set a passphrase that's too short (< 8 chars) + elements.passphraseInput.value = "short"; + elements.confirmInput.value = "short"; + + // Click submit + elements.submitButton.click(); + + // Error should be displayed + expect(elements.errorMsg.style.display).toBe("block"); + expect(elements.errorMsg.innerText).toBe( + "Passphrase must be at least 8 characters long." + ); + }); + + it("shows error when passphrase is exactly 7 characters", () => { + const elements = createPassphraseForm(); + addValidationHandler(elements, "test mnemonic"); + + // Set a passphrase that's exactly 7 chars (boundary case) + elements.passphraseInput.value = "1234567"; + elements.confirmInput.value = "1234567"; + + elements.submitButton.click(); + + expect(elements.errorMsg.style.display).toBe("block"); + expect(elements.errorMsg.innerText).toBe( + "Passphrase must be at least 8 characters long." + ); + }); + + it("accepts passphrase with exactly 8 characters", async () => { + const elements = createPassphraseForm(); + let successCalled = false; + + addValidationHandler(elements, "test mnemonic", async () => { + successCalled = true; + }); + + // Set a passphrase that's exactly 8 chars (boundary case - should pass) + elements.passphraseInput.value = "12345678"; + elements.confirmInput.value = "12345678"; + + elements.submitButton.click(); + + // Allow async handler to complete + await new Promise((resolve) => setTimeout(resolve, 10)); + + expect(elements.errorMsg.style.display).toBe("none"); + expect(successCalled).toBe(true); + }); + + it("shows error when passphrases do not match", () => { + const elements = createPassphraseForm(); + addValidationHandler(elements, "test mnemonic"); + + // Set mismatched passphrases (both >= 8 chars) + elements.passphraseInput.value = "password123"; + elements.confirmInput.value = "password456"; + + elements.submitButton.click(); + + expect(elements.errorMsg.style.display).toBe("block"); + expect(elements.errorMsg.innerText).toBe("Passphrases do not match."); + }); + + it("shows length error before mismatch error", () => { + const elements = createPassphraseForm(); + addValidationHandler(elements, "test mnemonic"); + + // Set short AND mismatched passphrases + elements.passphraseInput.value = "short"; + elements.confirmInput.value = "diff"; + + elements.submitButton.click(); + + // Length error should take precedence + expect(elements.errorMsg.style.display).toBe("block"); + expect(elements.errorMsg.innerText).toBe( + "Passphrase must be at least 8 characters long." + ); + }); + + it("hides error message on successful validation", async () => { + const elements = createPassphraseForm(); + addValidationHandler(elements, "test mnemonic", async () => {}); + + // First trigger an error + elements.passphraseInput.value = "short"; + elements.confirmInput.value = "short"; + elements.submitButton.click(); + expect(elements.errorMsg.style.display).toBe("block"); + + // Now enter valid passphrases + elements.passphraseInput.value = "validpassword123"; + elements.confirmInput.value = "validpassword123"; + elements.submitButton.click(); + + // Allow async handler to complete + await new Promise((resolve) => setTimeout(resolve, 10)); + + // Error should be hidden + expect(elements.errorMsg.style.display).toBe("none"); + }); + + it("accepts empty confirmation when passphrase is too short (length check first)", () => { + const elements = createPassphraseForm(); + addValidationHandler(elements, "test mnemonic"); + + // Short passphrase with empty confirmation + elements.passphraseInput.value = "short"; + elements.confirmInput.value = ""; + + elements.submitButton.click(); + + // Should show length error, not mismatch + expect(elements.errorMsg.innerText).toBe( + "Passphrase must be at least 8 characters long." + ); + }); + + it("validates with special characters in passphrase", async () => { + const elements = createPassphraseForm(); + let receivedPassphrase = null; + + addValidationHandler(elements, "test mnemonic", async (passphrase) => { + receivedPassphrase = passphrase; + }); + + // Passphrase with special characters + const specialPass = "p@$$w0rd!#%^&*()"; + elements.passphraseInput.value = specialPass; + elements.confirmInput.value = specialPass; + + elements.submitButton.click(); + + await new Promise((resolve) => setTimeout(resolve, 10)); + + expect(elements.errorMsg.style.display).toBe("none"); + expect(receivedPassphrase).toBe(specialPass); + }); + + it("validates with unicode characters in passphrase", async () => { + const elements = createPassphraseForm(); + let receivedPassphrase = null; + + addValidationHandler(elements, "test mnemonic", async (passphrase) => { + receivedPassphrase = passphrase; + }); + + // Passphrase with unicode + const unicodePass = "密码🔐secure"; + elements.passphraseInput.value = unicodePass; + elements.confirmInput.value = unicodePass; + + elements.submitButton.click(); + + await new Promise((resolve) => setTimeout(resolve, 10)); + + expect(elements.errorMsg.style.display).toBe("none"); + expect(receivedPassphrase).toBe(unicodePass); + }); + + it("is case-sensitive when comparing passphrases", () => { + const elements = createPassphraseForm(); + addValidationHandler(elements, "test mnemonic"); + + // Same passphrase but different case + elements.passphraseInput.value = "Password123"; + elements.confirmInput.value = "password123"; + + elements.submitButton.click(); + + expect(elements.errorMsg.style.display).toBe("block"); + expect(elements.errorMsg.innerText).toBe("Passphrases do not match."); + }); +}); From 847504c4cd6cad566d7d4052c05aac6cc67e8af6 Mon Sep 17 00:00:00 2001 From: fainashalts Date: Tue, 6 Jan 2026 07:31:55 -0800 Subject: [PATCH 05/12] add button state management --- export/index.template.html | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/export/index.template.html b/export/index.template.html index edf40b5..5c1f0fe 100644 --- a/export/index.template.html +++ b/export/index.template.html @@ -1697,8 +1697,9 @@

Message log

return; } - // Hide error message + // Hide error message and disable button to prevent duplicate submissions errorMsg.style.display = "none"; + submitButton.disabled = true; try { // Encode mnemonic to Uint8Array @@ -1722,9 +1723,12 @@

Message log

encryptedBase64, requestId ); + + // Keep button disabled after success (operation complete) } catch (e) { errorMsg.innerText = "Encryption failed: " + e.toString(); errorMsg.style.display = "block"; + submitButton.disabled = false; } }); } From 19c9c3c441d43811491232b23272737bce4ec376 Mon Sep 17 00:00:00 2001 From: fainashalts Date: Wed, 4 Feb 2026 10:56:54 -0800 Subject: [PATCH 06/12] fix: resolve bugs and clean up unused code --- export-and-sign/package-lock.json | 42 ++++++++++--------------------- export/package-lock.json | 8 ++++++ import/package-lock.json | 12 ++++++++- 3 files changed, 32 insertions(+), 30 deletions(-) diff --git a/export-and-sign/package-lock.json b/export-and-sign/package-lock.json index d57b981..9165523 100644 --- a/export-and-sign/package-lock.json +++ b/export-and-sign/package-lock.json @@ -92,6 +92,7 @@ "integrity": "sha512-97z/ju/Jy1rZmDxybphrBuI+jtJjFVoz7Mr9yUQVVVi+DNZE333uFQeMOqcCIy1x3WYBIbWftUSLmbNXNT7qFQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.22.13", @@ -2162,6 +2163,7 @@ "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@jest/environment": "^29.7.0", "@jest/expect": "^29.7.0", @@ -3865,6 +3867,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3949,6 +3952,7 @@ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -4666,6 +4670,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -4755,20 +4760,6 @@ "dev": true, "license": "MIT" }, - "node_modules/bufferutil": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.1.0.tgz", - "integrity": "sha512-ZMANVnAixE6AWWnPzlW2KpUrxhm9woycYvPOo67jWHyFowASTEd9s+QN1EIMsSDtwhIxN4sWE1jotpuDUIgyIw==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "dependencies": { - "node-gyp-build": "^4.3.0" - }, - "engines": { - "node": ">=6.14.2" - } - }, "node_modules/bundle-name": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", @@ -7457,6 +7448,7 @@ "integrity": "sha512-6YrDKTuqaP/TquFH7h4srYWsZx+x6k6+FbsTm0ziCwGHDP78Unr1r9F/H4+sGmMbX08GQcJ+K64x55b+7VM/jg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/html-minifier-terser": "^6.0.0", "html-minifier-terser": "^6.0.2", @@ -8462,6 +8454,7 @@ "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", @@ -11099,6 +11092,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -13004,7 +12998,8 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" + "license": "0BSD", + "peer": true }, "node_modules/tsyringe": { "version": "4.10.0", @@ -13220,20 +13215,6 @@ "requires-port": "^1.0.0" } }, - "node_modules/utf-8-validate": { - "version": "5.0.10", - "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz", - "integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "dependencies": { - "node-gyp-build": "^4.3.0" - }, - "engines": { - "node": ">=6.14.2" - } - }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -13355,6 +13336,7 @@ "integrity": "sha512-7h/weGm9d/ywQ6qzJ+Xy+r9n/3qgp/thalBbpOi5i223dPXKi04IBtqPN9nTd+jBc7QKfvDbaBnFipYp4sJAUQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", @@ -13404,6 +13386,7 @@ "integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@discoveryjs/json-ext": "^0.5.0", "@webpack-cli/configtest": "^2.1.1", @@ -13926,6 +13909,7 @@ "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=8.3.0" }, diff --git a/export/package-lock.json b/export/package-lock.json index 782df70..e34d4fe 100644 --- a/export/package-lock.json +++ b/export/package-lock.json @@ -72,6 +72,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.2.tgz", "integrity": "sha512-n7s51eWdaWZ3vGT2tD4T7J6eJs3QoBXydv7vkUM06Bf1cbVD2Kc2UrkzhiQwobfV7NwOnQXYL7UBJ5VPU+RGoQ==", "dev": true, + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.22.13", @@ -2079,6 +2080,7 @@ "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", "dev": true, + "peer": true, "dependencies": { "@jest/environment": "^29.7.0", "@jest/expect": "^29.7.0", @@ -3083,6 +3085,7 @@ "url": "https://github.com/sponsors/ai" } ], + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001541", "electron-to-chromium": "^1.4.535", @@ -5216,6 +5219,7 @@ "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "dev": true, + "peer": true, "dependencies": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", @@ -8376,6 +8380,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.2.tgz", "integrity": "sha512-n7s51eWdaWZ3vGT2tD4T7J6eJs3QoBXydv7vkUM06Bf1cbVD2Kc2UrkzhiQwobfV7NwOnQXYL7UBJ5VPU+RGoQ==", "dev": true, + "peer": true, "requires": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.22.13", @@ -9760,6 +9765,7 @@ "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", "dev": true, + "peer": true, "requires": { "@jest/environment": "^29.7.0", "@jest/expect": "^29.7.0", @@ -10533,6 +10539,7 @@ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.1.tgz", "integrity": "sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ==", "dev": true, + "peer": true, "requires": { "caniuse-lite": "^1.0.30001541", "electron-to-chromium": "^1.4.535", @@ -12029,6 +12036,7 @@ "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "dev": true, + "peer": true, "requires": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", diff --git a/import/package-lock.json b/import/package-lock.json index a8ad0f5..6851efa 100644 --- a/import/package-lock.json +++ b/import/package-lock.json @@ -84,6 +84,7 @@ "integrity": "sha512-97z/ju/Jy1rZmDxybphrBuI+jtJjFVoz7Mr9yUQVVVi+DNZE333uFQeMOqcCIy1x3WYBIbWftUSLmbNXNT7qFQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.22.13", @@ -2013,6 +2014,7 @@ "version": "29.7.0", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@jest/environment": "^29.7.0", "@jest/expect": "^29.7.0", @@ -3140,6 +3142,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3204,6 +3207,7 @@ "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", @@ -3789,6 +3793,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", @@ -7193,6 +7198,7 @@ "version": "29.7.0", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", @@ -9112,6 +9118,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -10830,7 +10837,8 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "dev": true, - "license": "0BSD" + "license": "0BSD", + "peer": true }, "node_modules/type-check": { "version": "0.4.0", @@ -11110,6 +11118,7 @@ "integrity": "sha512-7h/weGm9d/ywQ6qzJ+Xy+r9n/3qgp/thalBbpOi5i223dPXKi04IBtqPN9nTd+jBc7QKfvDbaBnFipYp4sJAUQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", @@ -11159,6 +11168,7 @@ "integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@discoveryjs/json-ext": "^0.5.0", "@webpack-cli/configtest": "^2.1.1", From 8270f1f39086c390aecb43b19e957f7af092642f Mon Sep 17 00:00:00 2001 From: fainashalts Date: Wed, 4 Feb 2026 13:31:12 -0800 Subject: [PATCH 07/12] add new-password autocomplete --- export/index.template.html | 2 ++ 1 file changed, 2 insertions(+) diff --git a/export/index.template.html b/export/index.template.html index 5c1f0fe..629a894 100644 --- a/export/index.template.html +++ b/export/index.template.html @@ -1644,6 +1644,7 @@

Message log

passphraseInput.placeholder = "Enter passphrase (min 8 characters)"; passphraseInput.required = true; passphraseInput.setAttribute("aria-required", "true"); + passphraseInput.setAttribute("autocomplete", "new-password"); passphraseInput.minLength = 8; formDiv.appendChild(passphraseInput); @@ -1659,6 +1660,7 @@

Message log

confirmInput.placeholder = "Confirm passphrase"; confirmInput.required = true; confirmInput.setAttribute("aria-required", "true"); + confirmInput.setAttribute("autocomplete", "new-password"); formDiv.appendChild(confirmInput); // Create error message paragraph From d7209f18299d96344f77b45d0381664df0c089c7 Mon Sep 17 00:00:00 2001 From: fainashalts Date: Wed, 4 Feb 2026 17:20:36 -0800 Subject: [PATCH 08/12] refactor: use form element for passphrase input --- export/index.template.html | 9 +++++---- export/index.test.js | 41 ++++++++++++++++++++++---------------- 2 files changed, 29 insertions(+), 21 deletions(-) diff --git a/export/index.template.html b/export/index.template.html index 629a894..366f516 100644 --- a/export/index.template.html +++ b/export/index.template.html @@ -1618,7 +1618,7 @@

Message log

}); // Create the passphrase form container - const formDiv = document.createElement("div"); + const formDiv = document.createElement("form"); formDiv.id = "passphrase-form-div"; // Create heading @@ -1671,7 +1671,7 @@

Message log

// Create submit button const submitButton = document.createElement("button"); - submitButton.type = "button"; + submitButton.type = "submit"; submitButton.id = "encrypt-and-export"; submitButton.innerText = "Encrypt & Export"; formDiv.appendChild(submitButton); @@ -1679,8 +1679,9 @@

Message log

// Append the form to the body document.body.appendChild(formDiv); - // Add click event listener to the submit button - submitButton.addEventListener("click", async () => { + // Add submit event listener to the form + formDiv.addEventListener("submit", async (event) => { + event.preventDefault(); const passphrase = passphraseInput.value; const confirmPassphrase = confirmInput.value; diff --git a/export/index.test.js b/export/index.test.js index c0d1df7..a493a27 100644 --- a/export/index.test.js +++ b/export/index.test.js @@ -556,7 +556,7 @@ describe("Passphrase Form Validation", () => { // Helper to create the passphrase form elements (mimics displayPassphraseForm) function createPassphraseForm() { - const formDiv = document.createElement("div"); + const formDiv = document.createElement("form"); formDiv.id = "passphrase-form-div"; const passphraseInput = document.createElement("input"); @@ -575,7 +575,7 @@ describe("Passphrase Form Validation", () => { formDiv.appendChild(errorMsg); const submitButton = document.createElement("button"); - submitButton.type = "button"; + submitButton.type = "submit"; submitButton.id = "encrypt-and-export"; formDiv.appendChild(submitButton); @@ -584,11 +584,18 @@ describe("Passphrase Form Validation", () => { return { formDiv, passphraseInput, confirmInput, errorMsg, submitButton }; } - // Helper to create click handler that mimics displayPassphraseForm logic + // Helper to submit form (triggers validation) + function submitForm(elements) { + const event = new dom.window.Event("submit", { bubbles: true, cancelable: true }); + elements.formDiv.dispatchEvent(event); + } + + // Helper to create submit handler that mimics displayPassphraseForm logic function addValidationHandler(elements, mnemonic, onSuccess) { - const { passphraseInput, confirmInput, errorMsg, submitButton } = elements; + const { formDiv, passphraseInput, confirmInput, errorMsg } = elements; - submitButton.addEventListener("click", async () => { + formDiv.addEventListener("submit", async (event) => { + event.preventDefault(); const passphrase = passphraseInput.value; const confirmPassphrase = confirmInput.value; @@ -641,8 +648,8 @@ describe("Passphrase Form Validation", () => { elements.passphraseInput.value = "short"; elements.confirmInput.value = "short"; - // Click submit - elements.submitButton.click(); + // Submit form + submitForm(elements); // Error should be displayed expect(elements.errorMsg.style.display).toBe("block"); @@ -659,7 +666,7 @@ describe("Passphrase Form Validation", () => { elements.passphraseInput.value = "1234567"; elements.confirmInput.value = "1234567"; - elements.submitButton.click(); + submitForm(elements); expect(elements.errorMsg.style.display).toBe("block"); expect(elements.errorMsg.innerText).toBe( @@ -679,7 +686,7 @@ describe("Passphrase Form Validation", () => { elements.passphraseInput.value = "12345678"; elements.confirmInput.value = "12345678"; - elements.submitButton.click(); + submitForm(elements); // Allow async handler to complete await new Promise((resolve) => setTimeout(resolve, 10)); @@ -696,7 +703,7 @@ describe("Passphrase Form Validation", () => { elements.passphraseInput.value = "password123"; elements.confirmInput.value = "password456"; - elements.submitButton.click(); + submitForm(elements); expect(elements.errorMsg.style.display).toBe("block"); expect(elements.errorMsg.innerText).toBe("Passphrases do not match."); @@ -710,7 +717,7 @@ describe("Passphrase Form Validation", () => { elements.passphraseInput.value = "short"; elements.confirmInput.value = "diff"; - elements.submitButton.click(); + submitForm(elements); // Length error should take precedence expect(elements.errorMsg.style.display).toBe("block"); @@ -726,13 +733,13 @@ describe("Passphrase Form Validation", () => { // First trigger an error elements.passphraseInput.value = "short"; elements.confirmInput.value = "short"; - elements.submitButton.click(); + submitForm(elements); expect(elements.errorMsg.style.display).toBe("block"); // Now enter valid passphrases elements.passphraseInput.value = "validpassword123"; elements.confirmInput.value = "validpassword123"; - elements.submitButton.click(); + submitForm(elements); // Allow async handler to complete await new Promise((resolve) => setTimeout(resolve, 10)); @@ -749,7 +756,7 @@ describe("Passphrase Form Validation", () => { elements.passphraseInput.value = "short"; elements.confirmInput.value = ""; - elements.submitButton.click(); + submitForm(elements); // Should show length error, not mismatch expect(elements.errorMsg.innerText).toBe( @@ -770,7 +777,7 @@ describe("Passphrase Form Validation", () => { elements.passphraseInput.value = specialPass; elements.confirmInput.value = specialPass; - elements.submitButton.click(); + submitForm(elements); await new Promise((resolve) => setTimeout(resolve, 10)); @@ -791,7 +798,7 @@ describe("Passphrase Form Validation", () => { elements.passphraseInput.value = unicodePass; elements.confirmInput.value = unicodePass; - elements.submitButton.click(); + submitForm(elements); await new Promise((resolve) => setTimeout(resolve, 10)); @@ -807,7 +814,7 @@ describe("Passphrase Form Validation", () => { elements.passphraseInput.value = "Password123"; elements.confirmInput.value = "password123"; - elements.submitButton.click(); + submitForm(elements); expect(elements.errorMsg.style.display).toBe("block"); expect(elements.errorMsg.innerText).toBe("Passphrases do not match."); From abd94a4d036ed8bc80920ccf49d89d6286f735c2 Mon Sep 17 00:00:00 2001 From: fainashalts Date: Wed, 4 Feb 2026 17:32:46 -0800 Subject: [PATCH 09/12] add passphrase strength indicator --- export/index.template.html | 129 +++++++++++++++++++++++++++++++++++++ 1 file changed, 129 insertions(+) diff --git a/export/index.template.html b/export/index.template.html index 366f516..f9d6fde 100644 --- a/export/index.template.html +++ b/export/index.template.html @@ -133,6 +133,30 @@ margin: 0.5em 0; text-align: left; } + #passphrase-strength { + margin-top: 0.5em; + text-align: left; + } + #passphrase-strength-bar { + height: 6px; + border-radius: 3px; + background-color: #e0e0e0; + overflow: hidden; + margin-bottom: 0.3em; + } + #passphrase-strength-fill { + height: 100%; + width: 0%; + transition: width 0.3s ease, background-color 0.3s ease; + } + #passphrase-strength-text { + font-size: 0.8em; + color: #666; + } + .strength-weak { background-color: #e74c3c; } + .strength-fair { background-color: #f39c12; } + .strength-good { background-color: #3498db; } + .strength-strong { background-color: #27ae60; } @@ -1648,6 +1672,95 @@

Message log

passphraseInput.minLength = 8; formDiv.appendChild(passphraseInput); + // Create passphrase strength indicator + const strengthDiv = document.createElement("div"); + strengthDiv.id = "passphrase-strength"; + + const strengthBar = document.createElement("div"); + strengthBar.id = "passphrase-strength-bar"; + + const strengthFill = document.createElement("div"); + strengthFill.id = "passphrase-strength-fill"; + strengthBar.appendChild(strengthFill); + strengthDiv.appendChild(strengthBar); + + const strengthText = document.createElement("span"); + strengthText.id = "passphrase-strength-text"; + strengthDiv.appendChild(strengthText); + formDiv.appendChild(strengthDiv); + + /** + * Evaluate passphrase strength and return score (0-4) with feedback + * @param {string} passphrase + * @returns {{score: number, label: string, feedback: string}} + */ + function evaluatePassphraseStrength(passphrase) { + if (!passphrase) { + return { score: 0, label: "", feedback: "" }; + } + + let score = 0; + const checks = { + length8: passphrase.length >= 8, + length12: passphrase.length >= 12, + length16: passphrase.length >= 16, + lowercase: /[a-z]/.test(passphrase), + uppercase: /[A-Z]/.test(passphrase), + numbers: /[0-9]/.test(passphrase), + special: /[^a-zA-Z0-9]/.test(passphrase), + }; + + // Base score from length + if (checks.length8) score += 1; + if (checks.length12) score += 1; + if (checks.length16) score += 1; + + // Character variety + const varietyCount = [checks.lowercase, checks.uppercase, checks.numbers, checks.special].filter(Boolean).length; + if (varietyCount >= 2) score += 1; + if (varietyCount >= 3) score += 1; + if (varietyCount >= 4) score += 1; + + // Cap at 4 + score = Math.min(score, 4); + + // Determine label and feedback + let label, feedback; + if (score <= 1) { + label = "Weak"; + feedback = "Add more characters and mix letters, numbers, and symbols."; + } else if (score === 2) { + label = "Fair"; + feedback = "Consider adding more length or character variety."; + } else if (score === 3) { + label = "Good"; + feedback = "Good passphrase strength."; + } else { + label = "Strong"; + feedback = "Excellent passphrase strength!"; + } + + return { score, label, feedback }; + } + + // Update strength indicator on input + passphraseInput.addEventListener("input", () => { + const { score, label, feedback } = evaluatePassphraseStrength(passphraseInput.value); + + // Update fill width and color + const strengthClasses = ["strength-weak", "strength-fair", "strength-good", "strength-strong"]; + strengthFill.className = ""; + + if (score === 0) { + strengthFill.style.width = "0%"; + strengthText.textContent = ""; + } else { + strengthFill.style.width = `${score * 25}%`; + strengthFill.classList.add(strengthClasses[Math.min(score - 1, 3)]); + strengthText.textContent = `${label} — ${feedback}`; + } + }); + // Create confirmation input const confirmLabel = document.createElement("label"); confirmLabel.setAttribute("for", "export-passphrase-confirm"); @@ -1720,6 +1833,14 @@

Message log

String.fromCharCode.apply(null, encryptedBytes) ); + // Clear passphrase fields for security before sending message + passphraseInput.value = ""; + confirmInput.value = ""; + // Reset strength indicator + strengthFill.style.width = "0%"; + strengthFill.className = ""; + strengthText.textContent = ""; + // Send message up TKHQ.sendMessageUp( "ENCRYPTED_WALLET_EXPORT", @@ -1729,6 +1850,14 @@

Message log

// Keep button disabled after success (operation complete) } catch (e) { + // Clear passphrase fields for security + passphraseInput.value = ""; + confirmInput.value = ""; + // Reset strength indicator + strengthFill.style.width = "0%"; + strengthFill.className = ""; + strengthText.textContent = ""; + errorMsg.innerText = "Encryption failed: " + e.toString(); errorMsg.style.display = "block"; submitButton.disabled = false; From 2001fe04d40d8a362ad7c90a40e509f259c24deb Mon Sep 17 00:00:00 2001 From: fainashalts Date: Fri, 6 Feb 2026 10:27:31 -0800 Subject: [PATCH 10/12] fix: zero out mnemonic and byte arrays after encrypted wallet export --- export/index.template.html | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/export/index.template.html b/export/index.template.html index f9d6fde..1139cac 100644 --- a/export/index.template.html +++ b/export/index.template.html @@ -1817,13 +1817,15 @@

Message log

errorMsg.style.display = "none"; submitButton.disabled = true; + let mnemonicBytes; + let encryptedBytes; try { // Encode mnemonic to Uint8Array const encoder = new TextEncoder(); - const mnemonicBytes = encoder.encode(mnemonic); + mnemonicBytes = encoder.encode(mnemonic); // Encrypt with passphrase - const encryptedBytes = await TKHQ.encryptWithPassphrase( + encryptedBytes = await TKHQ.encryptWithPassphrase( mnemonicBytes, passphrase ); @@ -1841,6 +1843,13 @@

Message log

strengthFill.className = ""; strengthText.textContent = ""; + // Zero out sensitive byte arrays (mutable, so we can wipe them) + for (let i = 0; i < mnemonicBytes.length; i++) mnemonicBytes[i] = 0; + for (let i = 0; i < encryptedBytes.length; i++) encryptedBytes[i] = 0; + + // Clear the mnemonic closure reference (can't truly zero a JS string, but removes the easy reference) + mnemonic = ""; + // Send message up TKHQ.sendMessageUp( "ENCRYPTED_WALLET_EXPORT", @@ -1848,8 +1857,17 @@

Message log

requestId ); - // Keep button disabled after success (operation complete) + // Remove the entire form from the DOM — no sensitive material should linger + formDiv.remove(); } catch (e) { + // Zero out sensitive byte arrays if they were created + if (mnemonicBytes) { + for (let i = 0; i < mnemonicBytes.length; i++) mnemonicBytes[i] = 0; + } + if (encryptedBytes) { + for (let i = 0; i < encryptedBytes.length; i++) encryptedBytes[i] = 0; + } + // Clear passphrase fields for security passphraseInput.value = ""; confirmInput.value = ""; From 5533728b4e7bec708c73c069585b2f919d6b20dc Mon Sep 17 00:00:00 2001 From: fainashalts Date: Fri, 6 Feb 2026 10:30:13 -0800 Subject: [PATCH 11/12] fix: replace String.fromCharCode.apply with Array.from to avoid stack overflow on large payloads --- export/index.template.html | 2 +- export/index.test.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/export/index.template.html b/export/index.template.html index 1139cac..2aa2ab0 100644 --- a/export/index.template.html +++ b/export/index.template.html @@ -1832,7 +1832,7 @@

Message log

// Convert to base64 const encryptedBase64 = btoa( - String.fromCharCode.apply(null, encryptedBytes) + Array.from(encryptedBytes, (b) => String.fromCharCode(b)).join("") ); // Clear passphrase fields for security before sending message diff --git a/export/index.test.js b/export/index.test.js index a493a27..beb25fd 100644 --- a/export/index.test.js +++ b/export/index.test.js @@ -526,7 +526,7 @@ describe("TKHQ", () => { const encrypted = await TKHQ.encryptWithPassphrase(mnemonicBytes, passphrase); // Convert to base64 (as would be done in displayPassphraseForm) - const encryptedBase64 = btoa(String.fromCharCode.apply(null, encrypted)); + const encryptedBase64 = btoa(Array.from(encrypted, (b) => String.fromCharCode(b)).join("")); expect(typeof encryptedBase64).toBe("string"); expect(encryptedBase64.length).toBeGreaterThan(0); From 8773586d1c5d3ece98f679c5d83dc27ba852e0c4 Mon Sep 17 00:00:00 2001 From: fainashalts Date: Fri, 6 Feb 2026 14:38:32 -0800 Subject: [PATCH 12/12] Revert unintentional changes to package-lock.json --- export-and-sign/package-lock.json | 42 +++++++++++++++-------- export/index.template.html | 55 +++++++++++++++++++++++-------- export/index.test.js | 27 +++++++++++---- export/package-lock.json | 8 ----- import/package-lock.json | 12 +------ 5 files changed, 92 insertions(+), 52 deletions(-) diff --git a/export-and-sign/package-lock.json b/export-and-sign/package-lock.json index 9165523..d57b981 100644 --- a/export-and-sign/package-lock.json +++ b/export-and-sign/package-lock.json @@ -92,7 +92,6 @@ "integrity": "sha512-97z/ju/Jy1rZmDxybphrBuI+jtJjFVoz7Mr9yUQVVVi+DNZE333uFQeMOqcCIy1x3WYBIbWftUSLmbNXNT7qFQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.22.13", @@ -2163,7 +2162,6 @@ "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jest/environment": "^29.7.0", "@jest/expect": "^29.7.0", @@ -3867,7 +3865,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3952,7 +3949,6 @@ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -4670,7 +4666,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -4760,6 +4755,20 @@ "dev": true, "license": "MIT" }, + "node_modules/bufferutil": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.1.0.tgz", + "integrity": "sha512-ZMANVnAixE6AWWnPzlW2KpUrxhm9woycYvPOo67jWHyFowASTEd9s+QN1EIMsSDtwhIxN4sWE1jotpuDUIgyIw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-gyp-build": "^4.3.0" + }, + "engines": { + "node": ">=6.14.2" + } + }, "node_modules/bundle-name": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", @@ -7448,7 +7457,6 @@ "integrity": "sha512-6YrDKTuqaP/TquFH7h4srYWsZx+x6k6+FbsTm0ziCwGHDP78Unr1r9F/H4+sGmMbX08GQcJ+K64x55b+7VM/jg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/html-minifier-terser": "^6.0.0", "html-minifier-terser": "^6.0.2", @@ -8454,7 +8462,6 @@ "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", @@ -11092,7 +11099,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -12998,8 +13004,7 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD", - "peer": true + "license": "0BSD" }, "node_modules/tsyringe": { "version": "4.10.0", @@ -13215,6 +13220,20 @@ "requires-port": "^1.0.0" } }, + "node_modules/utf-8-validate": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz", + "integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-gyp-build": "^4.3.0" + }, + "engines": { + "node": ">=6.14.2" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -13336,7 +13355,6 @@ "integrity": "sha512-7h/weGm9d/ywQ6qzJ+Xy+r9n/3qgp/thalBbpOi5i223dPXKi04IBtqPN9nTd+jBc7QKfvDbaBnFipYp4sJAUQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", @@ -13386,7 +13404,6 @@ "integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@discoveryjs/json-ext": "^0.5.0", "@webpack-cli/configtest": "^2.1.1", @@ -13909,7 +13926,6 @@ "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=8.3.0" }, diff --git a/export/index.template.html b/export/index.template.html index 2aa2ab0..bacad1a 100644 --- a/export/index.template.html +++ b/export/index.template.html @@ -153,10 +153,18 @@ font-size: 0.8em; color: #666; } - .strength-weak { background-color: #e74c3c; } - .strength-fair { background-color: #f39c12; } - .strength-good { background-color: #3498db; } - .strength-strong { background-color: #27ae60; } + .strength-weak { + background-color: #e74c3c; + } + .strength-fair { + background-color: #f39c12; + } + .strength-good { + background-color: #3498db; + } + .strength-strong { + background-color: #27ae60; + } @@ -1494,7 +1502,10 @@

Message log

TKHQ.sendMessageUp("ERROR", e.toString(), event.data["requestId"]); } } - if (event.data && event.data["type"] == "INJECT_WALLET_EXPORT_BUNDLE_ENCRYPTED") { + if ( + event.data && + event.data["type"] == "INJECT_WALLET_EXPORT_BUNDLE_ENCRYPTED" + ) { TKHQ.logMessage( `⬇️ Received message ${event.data["type"]}: ${event.data["value"]}, ${event.data["organizationId"]}` ); @@ -1716,7 +1727,12 @@

Message log

if (checks.length16) score += 1; // Character variety - const varietyCount = [checks.lowercase, checks.uppercase, checks.numbers, checks.special].filter(Boolean).length; + const varietyCount = [ + checks.lowercase, + checks.uppercase, + checks.numbers, + checks.special, + ].filter(Boolean).length; if (varietyCount >= 2) score += 1; if (varietyCount >= 3) score += 1; if (varietyCount >= 4) score += 1; @@ -1728,7 +1744,8 @@

Message log

let label, feedback; if (score <= 1) { label = "Weak"; - feedback = "Add more characters and mix letters, numbers, and symbols."; + feedback = + "Add more characters and mix letters, numbers, and symbols."; } else if (score === 2) { label = "Fair"; feedback = "Consider adding more length or character variety."; @@ -1745,12 +1762,19 @@

Message log

// Update strength indicator on input passphraseInput.addEventListener("input", () => { - const { score, label, feedback } = evaluatePassphraseStrength(passphraseInput.value); - + const { score, label, feedback } = evaluatePassphraseStrength( + passphraseInput.value + ); + // Update fill width and color - const strengthClasses = ["strength-weak", "strength-fair", "strength-good", "strength-strong"]; + const strengthClasses = [ + "strength-weak", + "strength-fair", + "strength-good", + "strength-strong", + ]; strengthFill.className = ""; - + if (score === 0) { strengthFill.style.width = "0%"; strengthText.textContent = ""; @@ -1845,7 +1869,8 @@

Message log

// Zero out sensitive byte arrays (mutable, so we can wipe them) for (let i = 0; i < mnemonicBytes.length; i++) mnemonicBytes[i] = 0; - for (let i = 0; i < encryptedBytes.length; i++) encryptedBytes[i] = 0; + for (let i = 0; i < encryptedBytes.length; i++) + encryptedBytes[i] = 0; // Clear the mnemonic closure reference (can't truly zero a JS string, but removes the easy reference) mnemonic = ""; @@ -1862,10 +1887,12 @@

Message log

} catch (e) { // Zero out sensitive byte arrays if they were created if (mnemonicBytes) { - for (let i = 0; i < mnemonicBytes.length; i++) mnemonicBytes[i] = 0; + for (let i = 0; i < mnemonicBytes.length; i++) + mnemonicBytes[i] = 0; } if (encryptedBytes) { - for (let i = 0; i < encryptedBytes.length; i++) encryptedBytes[i] = 0; + for (let i = 0; i < encryptedBytes.length; i++) + encryptedBytes[i] = 0; } // Clear passphrase fields for security diff --git a/export/index.test.js b/export/index.test.js index beb25fd..dd13277 100644 --- a/export/index.test.js +++ b/export/index.test.js @@ -466,7 +466,8 @@ describe("TKHQ", () => { }); it("decrypts data encrypted by encryptWithPassphrase correctly", async () => { - const originalText = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; + const originalText = + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; const plaintext = new TextEncoder().encode(originalText); const passphrase = "mySecurePassphrase!"; @@ -486,7 +487,10 @@ describe("TKHQ", () => { const correctPassphrase = "correctPassphrase"; const wrongPassphrase = "wrongPassphrase"; - const encrypted = await TKHQ.encryptWithPassphrase(plaintext, correctPassphrase); + const encrypted = await TKHQ.encryptWithPassphrase( + plaintext, + correctPassphrase + ); // Attempting to decrypt with wrong passphrase should throw await expect( @@ -523,10 +527,15 @@ describe("TKHQ", () => { const mnemonicBytes = new TextEncoder().encode(mnemonic); // Encrypt - const encrypted = await TKHQ.encryptWithPassphrase(mnemonicBytes, passphrase); + const encrypted = await TKHQ.encryptWithPassphrase( + mnemonicBytes, + passphrase + ); // Convert to base64 (as would be done in displayPassphraseForm) - const encryptedBase64 = btoa(Array.from(encrypted, (b) => String.fromCharCode(b)).join("")); + const encryptedBase64 = btoa( + Array.from(encrypted, (b) => String.fromCharCode(b)).join("") + ); expect(typeof encryptedBase64).toBe("string"); expect(encryptedBase64.length).toBeGreaterThan(0); @@ -538,7 +547,10 @@ describe("TKHQ", () => { ); // Decrypt - const decrypted = await TKHQ.decryptWithPassphrase(encryptedFromBase64, passphrase); + const decrypted = await TKHQ.decryptWithPassphrase( + encryptedFromBase64, + passphrase + ); const decryptedMnemonic = new TextDecoder().decode(decrypted); expect(decryptedMnemonic).toBe(mnemonic); @@ -586,7 +598,10 @@ describe("Passphrase Form Validation", () => { // Helper to submit form (triggers validation) function submitForm(elements) { - const event = new dom.window.Event("submit", { bubbles: true, cancelable: true }); + const event = new dom.window.Event("submit", { + bubbles: true, + cancelable: true, + }); elements.formDiv.dispatchEvent(event); } diff --git a/export/package-lock.json b/export/package-lock.json index e34d4fe..782df70 100644 --- a/export/package-lock.json +++ b/export/package-lock.json @@ -72,7 +72,6 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.2.tgz", "integrity": "sha512-n7s51eWdaWZ3vGT2tD4T7J6eJs3QoBXydv7vkUM06Bf1cbVD2Kc2UrkzhiQwobfV7NwOnQXYL7UBJ5VPU+RGoQ==", "dev": true, - "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.22.13", @@ -2080,7 +2079,6 @@ "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", "dev": true, - "peer": true, "dependencies": { "@jest/environment": "^29.7.0", "@jest/expect": "^29.7.0", @@ -3085,7 +3083,6 @@ "url": "https://github.com/sponsors/ai" } ], - "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001541", "electron-to-chromium": "^1.4.535", @@ -5219,7 +5216,6 @@ "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "dev": true, - "peer": true, "dependencies": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", @@ -8380,7 +8376,6 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.2.tgz", "integrity": "sha512-n7s51eWdaWZ3vGT2tD4T7J6eJs3QoBXydv7vkUM06Bf1cbVD2Kc2UrkzhiQwobfV7NwOnQXYL7UBJ5VPU+RGoQ==", "dev": true, - "peer": true, "requires": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.22.13", @@ -9765,7 +9760,6 @@ "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", "dev": true, - "peer": true, "requires": { "@jest/environment": "^29.7.0", "@jest/expect": "^29.7.0", @@ -10539,7 +10533,6 @@ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.1.tgz", "integrity": "sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ==", "dev": true, - "peer": true, "requires": { "caniuse-lite": "^1.0.30001541", "electron-to-chromium": "^1.4.535", @@ -12036,7 +12029,6 @@ "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "dev": true, - "peer": true, "requires": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", diff --git a/import/package-lock.json b/import/package-lock.json index 6851efa..a8ad0f5 100644 --- a/import/package-lock.json +++ b/import/package-lock.json @@ -84,7 +84,6 @@ "integrity": "sha512-97z/ju/Jy1rZmDxybphrBuI+jtJjFVoz7Mr9yUQVVVi+DNZE333uFQeMOqcCIy1x3WYBIbWftUSLmbNXNT7qFQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.22.13", @@ -2014,7 +2013,6 @@ "version": "29.7.0", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jest/environment": "^29.7.0", "@jest/expect": "^29.7.0", @@ -3142,7 +3140,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3207,7 +3204,6 @@ "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", @@ -3793,7 +3789,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", @@ -7198,7 +7193,6 @@ "version": "29.7.0", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", @@ -9118,7 +9112,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -10837,8 +10830,7 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "dev": true, - "license": "0BSD", - "peer": true + "license": "0BSD" }, "node_modules/type-check": { "version": "0.4.0", @@ -11118,7 +11110,6 @@ "integrity": "sha512-7h/weGm9d/ywQ6qzJ+Xy+r9n/3qgp/thalBbpOi5i223dPXKi04IBtqPN9nTd+jBc7QKfvDbaBnFipYp4sJAUQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", @@ -11168,7 +11159,6 @@ "integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@discoveryjs/json-ext": "^0.5.0", "@webpack-cli/configtest": "^2.1.1",