2a773119080b

fix: LV-8 replace unmaintained zxcvbn with @zxcvbn-ts/core 3.0.4 (+language-common 3.0.4, language-en 3.0.2 — no 3.0.4 published); Lock.tsx lazy loader with dictionaries

time
2026-06-10 02:53 UTC · 4 months ago
machine
koh2
agent
claude-opus-4-8
session
audit fix integration
parent
eb5ade2fb949
4 files bun.lock+11 −6package.json+3 −2src/popup/views/Lock.tsx+14 −3src/popup/views/zxcvbn-strength.test.ts+33 −0
bun.lock+11 −6
@@ -13,6 +13,9 @@1313         "@stablelib/sha256": "^2.0.1",1414         "@stablelib/x25519": "^2.0.1",1515         "@subspace/reed-solomon-erasure.wasm": "^0.2.5",16+        "@zxcvbn-ts/core": "3.0.4",17+        "@zxcvbn-ts/language-common": "3.0.4",18+        "@zxcvbn-ts/language-en": "3.0.2",1619         "@zxing/library": "^0.23.0",1720         "bwip-js": "^4.10.1",1821         "hash-wasm": "^4.12.0",@@ -22,7 +25,6 @@2225         "pdf-lib": "^1.17.1",2326         "pdfjs-dist": "^5.7.284",2427         "tldts": "^7.0.28",25-        "zxcvbn": "^4.4.2",2628       },2729       "devDependencies": {2830         "@crxjs/vite-plugin": "^2.4.0",@@ -31,7 +33,6 @@3133         "@types/node": "^22.0.0",3234         "@types/pako": "^2.0.4",3335         "@types/papaparse": "^5.5.2",34-        "@types/zxcvbn": "^4.4.5",3536         "@vitest/ui": "^4.1.4",3637         "chrome-webstore-upload-cli": "^4.0.0",3738         "jsdom": "^29.0.2",@@ -377,8 +378,6 @@377378 378379     "@types/papaparse": ["@types/[email protected]", "", { "dependencies": { "@types/node": "*" } }, "sha512-gFnFp/JMzLHCwRf7tQHrNnfhN4eYBVYYI897CGX4MY1tzY9l2aLkVyx2IlKZ/SAqDbB3I1AOZW5gTMGGsqWliA=="],379380 380-    "@types/zxcvbn": ["@types/[email protected]", "", {}, "sha512-FZJgC5Bxuqg7Rhsm/bx6gAruHHhDQ55r+s0JhDh8CQ16fD7NsJJ+p8YMMQDhSQoIrSmjpqqYWA96oQVMNkjRyA=="],381-382381     "@vitest/expect": ["@vitest/[email protected]", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.4", "@vitest/utils": "4.1.4", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" } }, "sha512-iPBpra+VDuXmBFI3FMKHSFXp3Gx5HfmSCE8X67Dn+bwephCnQCaB7qWK2ldHa+8ncN8hJU8VTMcxjPpyMkUjww=="],383382 384383     "@vitest/mocker": ["@vitest/[email protected]", "", { "dependencies": { "@vitest/spy": "4.1.4", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["msw"] }, "sha512-R9HTZBhW6yCSGbGQnDnH3QHfJxokKN4KB+Yvk9Q1le7eQNYwiCyKxmLmurSpFy6BzJanSLuEUDrD+j97Q+ZLPg=="],@@ -397,6 +396,12 @@397396 398397     "@webcomponents/custom-elements": ["@webcomponents/[email protected]", "", {}, "sha512-CqTpxOlUCPWRNUPZDxT5v2NnHXA4oox612iUGnmTUGQFhZ1Gkj8kirtl/2wcF6MqX7+PqqicZzOCBKKfIn0dww=="],399398 399+    "@zxcvbn-ts/core": ["@zxcvbn-ts/[email protected]", "", { "dependencies": { "fastest-levenshtein": "1.0.16" } }, "sha512-aQeiT0F09FuJaAqNrxynlAwZ2mW/1MdXakKWNmGM1Qp/VaY6CnB/GfnMS2T8gB2231Esp1/maCWd8vTG4OuShw=="],400+401+    "@zxcvbn-ts/language-common": ["@zxcvbn-ts/[email protected]", "", {}, "sha512-viSNNnRYtc7ULXzxrQIVUNwHAPSXRtoIwy/Tq4XQQdIknBzw4vz36lQLF6mvhMlTIlpjoN/Z1GFu/fwiAlUSsw=="],402+403+    "@zxcvbn-ts/language-en": ["@zxcvbn-ts/[email protected]", "", {}, "sha512-Zp+zL+I6Un2Bj0tRXNs6VUBq3Djt+hwTwUz4dkt2qgsQz47U0/XthZ4ULrT/RxjwJRl5LwiaKOOZeOtmixHnjg=="],404+400405     "@zxing/library": ["@zxing/[email protected]", "", { "dependencies": { "ts-custom-error": "^3.3.1" }, "optionalDependencies": { "@zxing/text-encoding": "~0.9.0" } }, "sha512-6fkkoFwP8CHxl6ugnPsj74PLJgX2iRv5zczGAyt5OBzQgxFhuhF0NCEc4t4OvSr8xAv2MRLlI0Iu9ZGDZQ2urA=="],401406 402407     "@zxing/text-encoding": ["@zxing/[email protected]", "", {}, "sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA=="],@@ -621,6 +626,8 @@621626 622627     "fast-uri": ["[email protected]", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="],623628 629+    "fastest-levenshtein": ["[email protected]", "", {}, "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg=="],630+624631     "fastq": ["[email protected]", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="],625632 626633     "fdir": ["[email protected]", "", { "peerDependencies": { "picomatch": "^3 || ^4" } }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],@@ -1164,8 +1171,6 @@11641171     "yocto-queue": ["[email protected]", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],11651172 11661173     "zip-dir": ["[email protected]", "", { "dependencies": { "async": "^3.2.0", "jszip": "^3.2.2" } }, "sha512-uhlsJZWz26FLYXOD6WVuq+fIcZ3aBPGo/cFdiLlv3KNwpa52IF3ISV8fLhQLiqVu5No3VhlqlgthN6gehil1Dg=="],1167-1168-    "zxcvbn": ["[email protected]", "", {}, "sha512-Bq0B+ixT/DMyG8kgX2xWcI5jUvCwqrMxSFam7m0lAf78nf04hv6lNCsyLYdyYTrCVMqNDY/206K7eExYCeSyUQ=="],11691174 11701175     "@babel/core/convert-source-map": ["[email protected]", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],11711176 

view file

package.json+3 −2
@@ -45,7 +45,9 @@4545     "pdf-lib": "^1.17.1",4646     "pdfjs-dist": "^5.7.284",4747     "tldts": "^7.0.28",48-    "zxcvbn": "^4.4.2"48+    "@zxcvbn-ts/core": "3.0.4",49+    "@zxcvbn-ts/language-common": "3.0.4",50+    "@zxcvbn-ts/language-en": "3.0.2"4951   },5052   "devDependencies": {5153     "@crxjs/vite-plugin": "^2.4.0",@@ -54,7 +56,6 @@5456     "@types/node": "^22.0.0",5557     "@types/pako": "^2.0.4",5658     "@types/papaparse": "^5.5.2",57-    "@types/zxcvbn": "^4.4.5",5859     "@vitest/ui": "^4.1.4",5960     "chrome-webstore-upload-cli": "^4.0.0",6061     "jsdom": "^29.0.2",

view file

src/popup/views/Lock.tsx+14 −3
@@ -22,12 +22,23 @@2222   '</svg>'2323 )}`;2424 25-// Lazy-load zxcvbn — it's heavy (~800 kB) and only needed during vault creation25+// Lazy-load @zxcvbn-ts — heavy (dictionaries ~700 kB) and only needed during26+// vault creation. Replaces the unmaintained [email protected] (LV-8); result.score27+// keeps the same 0-4 scale so downstream UI is unchanged.2628 let _zxcvbn: ((password: string) => { score: number }) | null = null;2729 async function getZxcvbn() {2830   if (_zxcvbn) return _zxcvbn;29-  const mod = await import("zxcvbn");30-  _zxcvbn = mod.default;31+  const [{ zxcvbn, zxcvbnOptions }, common, en] = await Promise.all([32+    import("@zxcvbn-ts/core"),33+    import("@zxcvbn-ts/language-common"),34+    import("@zxcvbn-ts/language-en"),35+  ]);36+  zxcvbnOptions.setOptions({37+    translations: en.translations,38+    graphs: common.adjacencyGraphs,39+    dictionary: { ...common.dictionary, ...en.dictionary },40+  });41+  _zxcvbn = (password: string) => zxcvbn(password);3142   return _zxcvbn;3243 }3344 

view file

src/popup/views/zxcvbn-strength.test.ts+33 −0
@@ -0,0 +1,33 @@1+// LV-8: the @zxcvbn-ts replacement must score the master-password strength2+// meter on the same 0-4 scale as the old zxcvbn, with dictionaries loaded3+// (language-common is what keeps weak passwords from scoring optimistically).4+import { beforeAll, describe, expect, it } from "vitest";5+import { zxcvbn, zxcvbnOptions } from "@zxcvbn-ts/core";6+import * as common from "@zxcvbn-ts/language-common";7+import * as en from "@zxcvbn-ts/language-en";8+9+beforeAll(() => {10+  // Mirror the loader in Lock.tsx exactly.11+  zxcvbnOptions.setOptions({12+    translations: en.translations,13+    graphs: common.adjacencyGraphs,14+    dictionary: { ...common.dictionary, ...en.dictionary },15+  });16+});17+18+describe("LV-8 password strength scoring", () => {19+  it("scores a common weak password low (<= 1)", () => {20+    expect(zxcvbn("password123").score).toBeLessThanOrEqual(1);21+  });22+23+  it("scores a long random passphrase at the top of the scale (4)", () => {24+    expect(zxcvbn("9xQ!vZ2pLm74Tr#Ke8Wd5Bn").score).toBe(4);25+  });26+27+  it("keeps the 0-4 integer scale", () => {28+    const s = zxcvbn("correct horse battery staple").score;29+    expect(Number.isInteger(s)).toBe(true);30+    expect(s).toBeGreaterThanOrEqual(0);31+    expect(s).toBeLessThanOrEqual(4);32+  });33+});

view file