mirror of
https://github.com/shuaiplus/nodewarden.git
synced 2026-08-05 06:50:10 +00:00
Compare commits
1
Commits
main
..
8128303d9b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8128303d9b |
Generated
+184
-176
@@ -11,27 +11,27 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@noble/hashes": "^2.2.0",
|
"@noble/hashes": "^2.2.0",
|
||||||
"@simplewebauthn/server": "^13.3.2",
|
"@simplewebauthn/server": "^13.3.2",
|
||||||
"@tanstack/react-query": "^5.101.2",
|
"@tanstack/react-query": "^5.101.4",
|
||||||
"@zip.js/zip.js": "^2.8.26",
|
"@zip.js/zip.js": "^2.8.34",
|
||||||
"fflate": "^0.8.3",
|
"fflate": "^0.8.3",
|
||||||
"jsqr": "1.4.0",
|
"jsqr": "1.4.0",
|
||||||
"lucide-preact": "^1.22.0",
|
"lucide-preact": "^1.26.0",
|
||||||
"preact": "^10.29.3",
|
"preact": "^10.29.7",
|
||||||
"qrcode-generator": "^2.0.4",
|
"qrcode-generator": "^2.0.4",
|
||||||
"wouter": "^3.10.0"
|
"wouter": "^3.10.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@cloudflare/workers-types": "^4.20260630.1",
|
"@cloudflare/workers-types": "^4.20260630.1",
|
||||||
"@preact/preset-vite": "^2.10.5",
|
"@preact/preset-vite": "^2.10.6",
|
||||||
"@types/node": "^26.0.1",
|
"@types/node": "^26.1.1",
|
||||||
"autoprefixer": "^10.5.2",
|
"autoprefixer": "^10.5.4",
|
||||||
"opencc-js": "^1.3.2",
|
"opencc-js": "^1.4.1",
|
||||||
"postcss": "^8.5.23",
|
"postcss": "^8.5.23",
|
||||||
"tailwindcss": "^3.4.19",
|
"tailwindcss": "^3.4.19",
|
||||||
"tsx": "^4.22.4",
|
"tsx": "^4.23.1",
|
||||||
"typescript": "^6.0.3",
|
"typescript": "^6.0.3",
|
||||||
"vite": "^8.1.3",
|
"vite": "^8.1.5",
|
||||||
"wrangler": "^4.105.0"
|
"wrangler": "^4.114.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@alloc/quick-lru": {
|
"node_modules/@alloc/quick-lru": {
|
||||||
@@ -613,9 +613,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@cloudflare/workerd-darwin-64": {
|
"node_modules/@cloudflare/workerd-darwin-64": {
|
||||||
"version": "1.20260625.1",
|
"version": "1.20260722.1",
|
||||||
"resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20260625.1.tgz",
|
"resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20260722.1.tgz",
|
||||||
"integrity": "sha512-naCfBv0WnnTQIQPTniqMoUlklOIFjrAcSn1X+IAOhY8aFLF/xGYtFjs1eEE8sFib3ZuChGGpU23FFORVczqr0A==",
|
"integrity": "sha512-vZOP8vIS3NwnuaO+gz0FZ7kIGeiO3bZmxV35Ph9zOXKSREhDFlH7wQ7mkCdhW3O4jnXsew+XT7b+DNEI2CcJGQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -630,9 +630,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@cloudflare/workerd-darwin-arm64": {
|
"node_modules/@cloudflare/workerd-darwin-arm64": {
|
||||||
"version": "1.20260625.1",
|
"version": "1.20260722.1",
|
||||||
"resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20260625.1.tgz",
|
"resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20260722.1.tgz",
|
||||||
"integrity": "sha512-jmH6zjp6Wrux46+qtFwDwrj+vd7s5bdwEqeGvdnwE0a4IEeAhKs0L42HQOyID+g5lkrHq9m55+AbhtmRAm63Pw==",
|
"integrity": "sha512-EmIQymihDq6WNdER4+LF8Qn80yqayBUpJ+tkOO7wmY8pmgfyXjIUFNXotl21AHovTeu2seR7HdVUgeN/BilCWw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -647,9 +647,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@cloudflare/workerd-linux-64": {
|
"node_modules/@cloudflare/workerd-linux-64": {
|
||||||
"version": "1.20260625.1",
|
"version": "1.20260722.1",
|
||||||
"resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20260625.1.tgz",
|
"resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20260722.1.tgz",
|
||||||
"integrity": "sha512-MiQkpA/dX8d83Zp64pzHUKfd6ca4cvwxnNobSP6CnXvfESvnNI9pfa+nfwnParla36sPmnYntNkjR7NjRuDeKQ==",
|
"integrity": "sha512-jvZ3k9fxcnEn04s80CgIYxQfpOyAiz/8qC42DP8EBa9tR27qWyg9wmm31zIobVlrgBZn/+8NfdP73avRGcQOjQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -664,9 +664,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@cloudflare/workerd-linux-arm64": {
|
"node_modules/@cloudflare/workerd-linux-arm64": {
|
||||||
"version": "1.20260625.1",
|
"version": "1.20260722.1",
|
||||||
"resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20260625.1.tgz",
|
"resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20260722.1.tgz",
|
||||||
"integrity": "sha512-LxxW7Qv60Xvv37+w6gUSDpYZziyqMy+cZWd9IvSA5ehVgKAxmzEaYPMiSZlxk32nbIWL9u/tfjXYCOKJ4Lo+XQ==",
|
"integrity": "sha512-BOSB55SMNdy+DA5uj2WirgiNanpHGis5PVvXH1wSfvjRKr4JGgWK+EZzxz0RFUo6QjjQQC/NimEzNZ7va7jmKg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -681,9 +681,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@cloudflare/workerd-windows-64": {
|
"node_modules/@cloudflare/workerd-windows-64": {
|
||||||
"version": "1.20260625.1",
|
"version": "1.20260722.1",
|
||||||
"resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20260625.1.tgz",
|
"resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20260722.1.tgz",
|
||||||
"integrity": "sha512-LH6iIX1HHaTwVKV5VokDxxUErXJzQoNZFRwVm7Vx/3fB/ApcTcRCUaMqcxI4as94jEUqg+pmX5czOndiveohow==",
|
"integrity": "sha512-sYM8YgUpKnRz2xjvdJLX1Ojzoi4MlA4gk8WTTExhGydjYB2UTs5NIbv0ZmpKgMoK9io3ixgmiW56ZnTbcWOdiA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -1900,9 +1900,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@oxc-project/types": {
|
"node_modules/@oxc-project/types": {
|
||||||
"version": "0.138.0",
|
"version": "0.139.0",
|
||||||
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.138.0.tgz",
|
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.139.0.tgz",
|
||||||
"integrity": "sha512-1a7ZKmrRTCoN1XMZ4L0PyyqrMnrNlLyPuOkdSX2MZg7IiIGRUyurNhAm73ptDOraoBcIordsIGKNPKUzy3ZmfA==",
|
"integrity": "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
@@ -2107,9 +2107,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@preact/preset-vite": {
|
"node_modules/@preact/preset-vite": {
|
||||||
"version": "2.10.5",
|
"version": "2.10.6",
|
||||||
"resolved": "https://registry.npmjs.org/@preact/preset-vite/-/preset-vite-2.10.5.tgz",
|
"resolved": "https://registry.npmjs.org/@preact/preset-vite/-/preset-vite-2.10.6.tgz",
|
||||||
"integrity": "sha512-p0vJpxiVO7KWWazWny3LUZ+saXyZKWv6Ju0bYMWNJRp2YveufRPgSUB1C4MTqGJfz07EehMgfN+AJNwQy+w6Iw==",
|
"integrity": "sha512-ZZ5RIT2ZVXg8UxRA8VZpoT8CdpDG7RFIqOT4bELqNBp85+HKvQBbgmh3gsoW1UOQXx26TLe+ZRNKI7q281Kckw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -2199,9 +2199,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-android-arm64": {
|
"node_modules/@rolldown/binding-android-arm64": {
|
||||||
"version": "1.1.4",
|
"version": "1.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.5.tgz",
|
||||||
"integrity": "sha512-EZLpf/8y7GXkkra90ML47kzik/GMP3EMcE9bPyHmRfxLC6z9+aW5A8poCsoxjrT5GfEcNAAvWwUHjvP1pUQkfw==",
|
"integrity": "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -2216,9 +2216,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-darwin-arm64": {
|
"node_modules/@rolldown/binding-darwin-arm64": {
|
||||||
"version": "1.1.4",
|
"version": "1.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.5.tgz",
|
||||||
"integrity": "sha512-aUi+HBvmYb7j8krl1+qJgkG8C17fO79gk3c+jPw4S8glRFc1DTija9S3EyaTSQUm5GJXYKDAsugBEhFHH2vYiQ==",
|
"integrity": "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -2233,9 +2233,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-darwin-x64": {
|
"node_modules/@rolldown/binding-darwin-x64": {
|
||||||
"version": "1.1.4",
|
"version": "1.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.5.tgz",
|
||||||
"integrity": "sha512-F7hHC3gwY11+vByKPRWqwGbeXWVgKmL+pTGCinaEhdihzBV2aQ0fvZOch9cXYUOKuKKq429HeYXOqQLc7wFCEg==",
|
"integrity": "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -2250,9 +2250,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-freebsd-x64": {
|
"node_modules/@rolldown/binding-freebsd-x64": {
|
||||||
"version": "1.1.4",
|
"version": "1.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.5.tgz",
|
||||||
"integrity": "sha512-sI5yw+7s92SK6odiEhD5lKCBlWcpjHS5qyqpVQbZAJ0fIzEUXrmbl3DH2ybR3PZogulNJF+COLtmA8hUfvkCCQ==",
|
"integrity": "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -2267,9 +2267,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
|
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
|
||||||
"version": "1.1.4",
|
"version": "1.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.5.tgz",
|
||||||
"integrity": "sha512-mCi0OKgEieFircrtVYmQAFGszRtMnZ6fpZAXrxanXAu7lqZcsK1E1RAaZNG0uKAnxox3B1f4EyQNnoyMfN1vAA==",
|
"integrity": "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
@@ -2284,9 +2284,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-arm64-gnu": {
|
"node_modules/@rolldown/binding-linux-arm64-gnu": {
|
||||||
"version": "1.1.4",
|
"version": "1.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.5.tgz",
|
||||||
"integrity": "sha512-B9Ial3Kv5sh0SHnB1g/QWcUQCEvCF6QKGAl4zXypYj65mVI+B4AhFBwPtSN7pDrJeIx8Z7zdy4ntx+wQABom7w==",
|
"integrity": "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -2304,9 +2304,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-arm64-musl": {
|
"node_modules/@rolldown/binding-linux-arm64-musl": {
|
||||||
"version": "1.1.4",
|
"version": "1.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.5.tgz",
|
||||||
"integrity": "sha512-lZVym0PuHE1KZ22gmFTC15lAkrg9iTszR617oYRB/iPY1A56ywoJzVKOJBKaot5RiikCObmur6pogpse3gRcng==",
|
"integrity": "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -2324,9 +2324,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-ppc64-gnu": {
|
"node_modules/@rolldown/binding-linux-ppc64-gnu": {
|
||||||
"version": "1.1.4",
|
"version": "1.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.5.tgz",
|
||||||
"integrity": "sha512-t2DNiLJWNTbnEHyUzTumldML6ET4/g16467LZoDDJ3tSxGvguL5/NyC2lCsNKuyRycg9XeDQF5SSv+TNOhQEXg==",
|
"integrity": "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
@@ -2344,9 +2344,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-s390x-gnu": {
|
"node_modules/@rolldown/binding-linux-s390x-gnu": {
|
||||||
"version": "1.1.4",
|
"version": "1.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.5.tgz",
|
||||||
"integrity": "sha512-0WIRnL1Uw4BvTZRLQt+PVgo6ZKTJadlC2btP+/EOXv2f/DWbY0rEgl+y834mIVwP1FkTlWVTrGGJXf12lru7EQ==",
|
"integrity": "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"s390x"
|
"s390x"
|
||||||
],
|
],
|
||||||
@@ -2364,9 +2364,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-x64-gnu": {
|
"node_modules/@rolldown/binding-linux-x64-gnu": {
|
||||||
"version": "1.1.4",
|
"version": "1.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.5.tgz",
|
||||||
"integrity": "sha512-JWtGshGfX+oENAKonoNkqEJX+7hC8yfhi9GUyPX1VX4mdh1y5r+ZiJLR5XzAB0aoP6s/PcILsGjKq8O0mm24bw==",
|
"integrity": "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -2384,9 +2384,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-x64-musl": {
|
"node_modules/@rolldown/binding-linux-x64-musl": {
|
||||||
"version": "1.1.4",
|
"version": "1.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.5.tgz",
|
||||||
"integrity": "sha512-rT6yQcxUuXs4CnbofqwHRRV0iem349rLMYpTjkgQGLjrY4ado/eDzwPZPTCgTOlF6Nkp8NEv70yLMTn6qkWxsQ==",
|
"integrity": "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -2404,9 +2404,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-openharmony-arm64": {
|
"node_modules/@rolldown/binding-openharmony-arm64": {
|
||||||
"version": "1.1.4",
|
"version": "1.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.5.tgz",
|
||||||
"integrity": "sha512-KXMGoboq5cyaCQjDA4GLuRiOwBQ0EyFnJoVViLeZ45/3rFItRODEr+NdsBcVpll40hhNArlm/speWGRvj08LzA==",
|
"integrity": "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -2421,9 +2421,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-wasm32-wasi": {
|
"node_modules/@rolldown/binding-wasm32-wasi": {
|
||||||
"version": "1.1.4",
|
"version": "1.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.5.tgz",
|
||||||
"integrity": "sha512-5K83rb36oJiY7BCyE9zLZtGcPV4g5wvq+xwdO0XPIwDVZI8cyB/AUjkNXGb92/rnmezEkjMOpgY61rtwjQtFwg==",
|
"integrity": "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"wasm32"
|
"wasm32"
|
||||||
],
|
],
|
||||||
@@ -2440,9 +2440,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-win32-arm64-msvc": {
|
"node_modules/@rolldown/binding-win32-arm64-msvc": {
|
||||||
"version": "1.1.4",
|
"version": "1.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.5.tgz",
|
||||||
"integrity": "sha512-PnWBtw3TV5KOg69HQQDR0mnQuyCmSGR2pAB4DC1rPF808fgKeTUMj2EOEyKATpgiuxuR5APQmiDO7PDgEjTFSA==",
|
"integrity": "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -2457,9 +2457,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-win32-x64-msvc": {
|
"node_modules/@rolldown/binding-win32-x64-msvc": {
|
||||||
"version": "1.1.4",
|
"version": "1.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.5.tgz",
|
||||||
"integrity": "sha512-M1lpniBePobTfsa7Ks9a199e1akxsXn+GYBUKsEzv3YFzOm1HJAMNwKI3qr0Zq+mxwx9gOZoTdP1yXRYsZUocQ==",
|
"integrity": "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -2543,9 +2543,9 @@
|
|||||||
"license": "CC0-1.0"
|
"license": "CC0-1.0"
|
||||||
},
|
},
|
||||||
"node_modules/@tanstack/query-core": {
|
"node_modules/@tanstack/query-core": {
|
||||||
"version": "5.101.2",
|
"version": "5.101.4",
|
||||||
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.101.2.tgz",
|
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.101.4.tgz",
|
||||||
"integrity": "sha512-hH5MLoJhF7KaIGd7q3xTXGXvslI+GYlM1Z/35aSHHWaCJWB7XvTSHYuV3eM7tw+aE0mT/xMro4M4Q9rCGHT0lw==",
|
"integrity": "sha512-gNwcvOJcRbLWPOLG/2OBm+zM+Yv+MKsXKEOWC57USuZDEsI71hEErQsiEGx5wX9rzWWkfwM0fVSPoiIFSsxfiw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "github",
|
"type": "github",
|
||||||
@@ -2553,12 +2553,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tanstack/react-query": {
|
"node_modules/@tanstack/react-query": {
|
||||||
"version": "5.101.2",
|
"version": "5.101.4",
|
||||||
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.101.2.tgz",
|
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.101.4.tgz",
|
||||||
"integrity": "sha512-seDkr6kzGzX1okaaTtZPtgA688CDPlXUz1C6xSg0ESqn04Vuc8tlrYms1s3de+znBqhPVxFRfpAfUf+6XvfPWg==",
|
"integrity": "sha512-yRg2pfOCxIs4ZJW3XYYHU/WgtD04FHSnfHlpRT7h7pR77hwkdRG4wxbKe4aq6P0RvXUTBSQpQeadS1SUYUe+KA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tanstack/query-core": "5.101.2"
|
"@tanstack/query-core": "5.101.4"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "github",
|
"type": "github",
|
||||||
@@ -2601,9 +2601,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "26.0.1",
|
"version": "26.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-26.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.1.tgz",
|
||||||
"integrity": "sha512-fc3KiUoBt6kie0N9bIW3E47vZsuaMf0PM2AaUpLCLT0s/LvX1nxAim6Fc049cNxODPpGm6qRAuUOB86SkRuPQw==",
|
"integrity": "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -2611,9 +2611,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@zip.js/zip.js": {
|
"node_modules/@zip.js/zip.js": {
|
||||||
"version": "2.8.26",
|
"version": "2.8.34",
|
||||||
"resolved": "https://registry.npmjs.org/@zip.js/zip.js/-/zip.js-2.8.26.tgz",
|
"resolved": "https://registry.npmjs.org/@zip.js/zip.js/-/zip.js-2.8.34.tgz",
|
||||||
"integrity": "sha512-RQ4h9F6DOiHxpdocUDrOl6xBM+yOtz+LkUol47AVWcfebGBDpZ7w7Xvz9PS24JgXvLGiXXzSAfdCdVy1tPlaFA==",
|
"integrity": "sha512-+6a3lyqq69rpseLbvDPiVIWsZ/HdTGAAD6afFtug6ECPDGttb2dHnPC6cJgdPofYkzL9OvXizegq+DQVfL2rnA==",
|
||||||
"license": "BSD-3-Clause",
|
"license": "BSD-3-Clause",
|
||||||
"engines": {
|
"engines": {
|
||||||
"bun": ">=0.7.0",
|
"bun": ">=0.7.0",
|
||||||
@@ -2677,9 +2677,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/autoprefixer": {
|
"node_modules/autoprefixer": {
|
||||||
"version": "10.5.2",
|
"version": "10.5.4",
|
||||||
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.2.tgz",
|
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.4.tgz",
|
||||||
"integrity": "sha512-rD5t5DwOjJdmSORcTq64j8MawTC+tbQ+HHqjR4NDumamy/ambn1UJrlKL+KdwujWxMkFjPM3pPHOEA9tl4767Q==",
|
"integrity": "sha512-MaU0U/za7N3r6brxD4YB/l4NSrFzLPlANv6wEuQVaIPlD3L4W9rFcQPbL/EilY9BHhHvhfcz3gInDLrEtWT4EA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -2697,8 +2697,8 @@
|
|||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"browserslist": "^4.28.4",
|
"browserslist": "^4.28.6",
|
||||||
"caniuse-lite": "^1.0.30001799",
|
"caniuse-lite": "^1.0.30001806",
|
||||||
"fraction.js": "^5.3.4",
|
"fraction.js": "^5.3.4",
|
||||||
"picocolors": "^1.1.1",
|
"picocolors": "^1.1.1",
|
||||||
"postcss-value-parser": "^4.2.0"
|
"postcss-value-parser": "^4.2.0"
|
||||||
@@ -2714,9 +2714,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/autoprefixer/node_modules/browserslist": {
|
"node_modules/autoprefixer/node_modules/browserslist": {
|
||||||
"version": "4.28.4",
|
"version": "4.28.7",
|
||||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.4.tgz",
|
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.7.tgz",
|
||||||
"integrity": "sha512-MTc8i/x9jBQd1iMw2CFGS+rwMa07eYjLR0CCTLDACl9xhxy+nIs3KeML/biicXtk9JrZ6dnnTatmc7ErPXIxqw==",
|
"integrity": "sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -2734,10 +2734,10 @@
|
|||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"baseline-browser-mapping": "^2.10.38",
|
"baseline-browser-mapping": "^2.10.44",
|
||||||
"caniuse-lite": "^1.0.30001799",
|
"caniuse-lite": "^1.0.30001806",
|
||||||
"electron-to-chromium": "^1.5.376",
|
"electron-to-chromium": "^1.5.393",
|
||||||
"node-releases": "^2.0.48",
|
"node-releases": "^2.0.51",
|
||||||
"update-browserslist-db": "^1.2.3"
|
"update-browserslist-db": "^1.2.3"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -2758,9 +2758,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/baseline-browser-mapping": {
|
"node_modules/baseline-browser-mapping": {
|
||||||
"version": "2.10.40",
|
"version": "2.11.1",
|
||||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.40.tgz",
|
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.1.tgz",
|
||||||
"integrity": "sha512-BSSLZ9/Cjjv7Gtj5B68ZzXcXUg8iOf3fme+FCuh8rC/Go+Kmh8cox7M3A8dolou16s64QjLPOSdngh7GxXvkSw==",
|
"integrity": "sha512-HYXq73DDpCtNzOmrFsm9eSwCvWCql0RzqjpDzXN9EadiLJ4DNat0nsZ/Bzmy+Ud12mb4/zKDY0cQ805ZzN+i0A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -2855,9 +2855,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/caniuse-lite": {
|
"node_modules/caniuse-lite": {
|
||||||
"version": "1.0.30001799",
|
"version": "1.0.30001806",
|
||||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001799.tgz",
|
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001806.tgz",
|
||||||
"integrity": "sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==",
|
"integrity": "sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -3089,9 +3089,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/electron-to-chromium": {
|
"node_modules/electron-to-chromium": {
|
||||||
"version": "1.5.381",
|
"version": "1.5.396",
|
||||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.381.tgz",
|
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.396.tgz",
|
||||||
"integrity": "sha512-n9Wa6yB+vDsGuA8AKbl/0z7HbvWqt5jxIdvr1IUicd0ryPrk7/xzwqLv8D9AbbvZ6avVNtXYLTfmgFHkwkyelg==",
|
"integrity": "sha512-yHiw2Y3C3H9U6TMbOfoWK/BPreiOPXRfTWPBwQBoZG6/8TB6eOPnsy5oaRYuatR7Fw2SJ4kKforgufeo7fq0EQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
@@ -3802,9 +3802,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lucide-preact": {
|
"node_modules/lucide-preact": {
|
||||||
"version": "1.22.0",
|
"version": "1.26.0",
|
||||||
"resolved": "https://registry.npmjs.org/lucide-preact/-/lucide-preact-1.22.0.tgz",
|
"resolved": "https://registry.npmjs.org/lucide-preact/-/lucide-preact-1.26.0.tgz",
|
||||||
"integrity": "sha512-zFaBtoaWQgvapVEI96M3b5iUOlAEvpUfDuW3Gs8K1RHDyoOTrnXm+Cz5tupm8StKbRKn3W/YKIPolllf5voVDw==",
|
"integrity": "sha512-Vx39SWX2JsBgbB+fgwKrzQmqsivo2b/E+4LwbDrjF0m4Oc8r0gZFWBMRvtuS8/b2eK0VRKOG+P6U4RaOS1CAmQ==",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"preact": "^10.27.2"
|
"preact": "^10.27.2"
|
||||||
@@ -3858,16 +3858,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/miniflare": {
|
"node_modules/miniflare": {
|
||||||
"version": "4.20260625.0",
|
"version": "4.20260722.0",
|
||||||
"resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20260625.0.tgz",
|
"resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20260722.0.tgz",
|
||||||
"integrity": "sha512-3kKXwRUObJsnBYPBgR0NiNZYKF/yv8GFyha1cx2EeAEraxNODgRVcyeRo+F1ok1tg5Mg7iUpOWSkknQTHuFhwA==",
|
"integrity": "sha512-LW6ABMhCx/yIEFBLC/DO4yAhdm2T/G7jp7pr5T2kj895+CCIaHZqpMXdW9O6YE48LcYcCJChwWc8aEs1vpbTXw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@cspotcode/source-map-support": "0.8.1",
|
"@cspotcode/source-map-support": "0.8.1",
|
||||||
"sharp": "0.34.5",
|
"sharp": "0.35.2",
|
||||||
"undici": "7.28.0",
|
"undici": "7.28.0",
|
||||||
"workerd": "1.20260625.1",
|
"workerd": "1.20260722.1",
|
||||||
"ws": "8.21.0",
|
"ws": "8.21.0",
|
||||||
"youch": "4.1.0-beta.10"
|
"youch": "4.1.0-beta.10"
|
||||||
},
|
},
|
||||||
@@ -3934,9 +3934,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/node-releases": {
|
"node_modules/node-releases": {
|
||||||
"version": "2.0.50",
|
"version": "2.0.51",
|
||||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.50.tgz",
|
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.51.tgz",
|
||||||
"integrity": "sha512-J6l92tKHX6w8Jy5nO1Vuc01NoIiRGi/d6qBKVxh+IQ8Cr3b6HbVNfKiF8ZpFKufTwpwxMmce2W3iQZ861ZRyTg==",
|
"integrity": "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -4001,9 +4001,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/opencc-js": {
|
"node_modules/opencc-js": {
|
||||||
"version": "1.3.2",
|
"version": "1.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/opencc-js/-/opencc-js-1.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/opencc-js/-/opencc-js-1.4.1.tgz",
|
||||||
"integrity": "sha512-lO4Kq8J4TcPTa8qHcx5qazQCn+NM68kNwLJOQ58DG9MV8v25XJxByMB74zDGmr3LjJMGqDdrvQfoCi3FO0SC2A==",
|
"integrity": "sha512-2lPLcrg7cnh1ATSduOxhnk/jq6KlR6w+5Ihl4wF7Z5e5Rva4vxTyugV5rZ7EiUCOZl+EDOLetIix+BhgYDCC+A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT AND Apache-2.0"
|
"license": "MIT AND Apache-2.0"
|
||||||
},
|
},
|
||||||
@@ -4036,9 +4036,9 @@
|
|||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/picomatch": {
|
"node_modules/picomatch": {
|
||||||
"version": "4.0.4",
|
"version": "4.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz",
|
||||||
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
"integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -4225,13 +4225,21 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/preact": {
|
"node_modules/preact": {
|
||||||
"version": "10.29.3",
|
"version": "10.29.7",
|
||||||
"resolved": "https://registry.npmjs.org/preact/-/preact-10.29.3.tgz",
|
"resolved": "https://registry.npmjs.org/preact/-/preact-10.29.7.tgz",
|
||||||
"integrity": "sha512-D9NL1GAnJZhc3RndVs4gDdxEeU9TcHgywMrhhOsnpdlvFjdbx0gAsLUnH6JEhlJH5giL7Tx5biWPUSEXE/HPzw==",
|
"integrity": "sha512-DCHYrK/B10yUD3ZjLfhZ3WIE/9Vf9VFUODcRE2dRomTYDpJk6z6L9wecSfhfE6M9ZTHUdyQkoC46arIDhEV84Q==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
"url": "https://opencollective.com/preact"
|
"url": "https://opencollective.com/preact"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"preact-render-to-string": ">=5"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"preact-render-to-string": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/pvtsutils": {
|
"node_modules/pvtsutils": {
|
||||||
@@ -4374,13 +4382,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/rolldown": {
|
"node_modules/rolldown": {
|
||||||
"version": "1.1.4",
|
"version": "1.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.5.tgz",
|
||||||
"integrity": "sha512-IjZYiLxZwpnhwhdBH2ugdTGVSdhCQUmLxLoqyjiL0JxYjyRst+5a0P3xfrTxJ5F638j4Mvvw5FAX5XE6eHpXbA==",
|
"integrity": "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@oxc-project/types": "=0.138.0",
|
"@oxc-project/types": "=0.139.0",
|
||||||
"@rolldown/pluginutils": "^1.0.0"
|
"@rolldown/pluginutils": "^1.0.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -4390,21 +4398,21 @@
|
|||||||
"node": "^20.19.0 || >=22.12.0"
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@rolldown/binding-android-arm64": "1.1.4",
|
"@rolldown/binding-android-arm64": "1.1.5",
|
||||||
"@rolldown/binding-darwin-arm64": "1.1.4",
|
"@rolldown/binding-darwin-arm64": "1.1.5",
|
||||||
"@rolldown/binding-darwin-x64": "1.1.4",
|
"@rolldown/binding-darwin-x64": "1.1.5",
|
||||||
"@rolldown/binding-freebsd-x64": "1.1.4",
|
"@rolldown/binding-freebsd-x64": "1.1.5",
|
||||||
"@rolldown/binding-linux-arm-gnueabihf": "1.1.4",
|
"@rolldown/binding-linux-arm-gnueabihf": "1.1.5",
|
||||||
"@rolldown/binding-linux-arm64-gnu": "1.1.4",
|
"@rolldown/binding-linux-arm64-gnu": "1.1.5",
|
||||||
"@rolldown/binding-linux-arm64-musl": "1.1.4",
|
"@rolldown/binding-linux-arm64-musl": "1.1.5",
|
||||||
"@rolldown/binding-linux-ppc64-gnu": "1.1.4",
|
"@rolldown/binding-linux-ppc64-gnu": "1.1.5",
|
||||||
"@rolldown/binding-linux-s390x-gnu": "1.1.4",
|
"@rolldown/binding-linux-s390x-gnu": "1.1.5",
|
||||||
"@rolldown/binding-linux-x64-gnu": "1.1.4",
|
"@rolldown/binding-linux-x64-gnu": "1.1.5",
|
||||||
"@rolldown/binding-linux-x64-musl": "1.1.4",
|
"@rolldown/binding-linux-x64-musl": "1.1.5",
|
||||||
"@rolldown/binding-openharmony-arm64": "1.1.4",
|
"@rolldown/binding-openharmony-arm64": "1.1.5",
|
||||||
"@rolldown/binding-wasm32-wasi": "1.1.4",
|
"@rolldown/binding-wasm32-wasi": "1.1.5",
|
||||||
"@rolldown/binding-win32-arm64-msvc": "1.1.4",
|
"@rolldown/binding-win32-arm64-msvc": "1.1.5",
|
||||||
"@rolldown/binding-win32-x64-msvc": "1.1.4"
|
"@rolldown/binding-win32-x64-msvc": "1.1.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/run-parallel": {
|
"node_modules/run-parallel": {
|
||||||
@@ -4683,9 +4691,9 @@
|
|||||||
"license": "0BSD"
|
"license": "0BSD"
|
||||||
},
|
},
|
||||||
"node_modules/tsx": {
|
"node_modules/tsx": {
|
||||||
"version": "4.22.4",
|
"version": "4.23.1",
|
||||||
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.4.tgz",
|
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.1.tgz",
|
||||||
"integrity": "sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg==",
|
"integrity": "sha512-GQHnkIfxyx1wYCOS/wonik5MVRZU9hi1TEZmzGZSCJB1y9YgoZ8H6itNE/u4suE+yLmOzuE4E5S4TZ/ZX2wcWQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -4734,9 +4742,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/undici": {
|
"node_modules/undici": {
|
||||||
"version": "8.5.0",
|
"version": "8.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/undici/-/undici-8.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/undici/-/undici-8.9.0.tgz",
|
||||||
"integrity": "sha512-xamtWoB1EshgjpmlXd7GGm2VfdDtw1+rD8uhry8pSNW3If6S8E0m2T2+orSKeZXEn/aPJMviCpDBA65WJt8zhg==",
|
"integrity": "sha512-aWZpUj7XoGonMClx4gdDRfgBjqeA+F473aDmROQQbM9n6PRfK/u1q/a0X4wMTgcHfT8H6fpbt98PFuDUwFg2YA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -4808,16 +4816,16 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "8.1.3",
|
"version": "8.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-8.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-8.1.5.tgz",
|
||||||
"integrity": "sha512-Ds+gBRbj0lwRO2Y5hwnUBdxSwlAve9LeRyU4sNnAr0ewW0gWF0n5bgXgUzbgZ49MV9BVUAQUFYVcDUcilUExMA==",
|
"integrity": "sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"lightningcss": "^1.32.0",
|
"lightningcss": "^1.32.0",
|
||||||
"picomatch": "^4.0.4",
|
"picomatch": "^4.0.5",
|
||||||
"postcss": "^8.5.16",
|
"postcss": "^8.5.17",
|
||||||
"rolldown": "~1.1.3",
|
"rolldown": "~1.1.5",
|
||||||
"tinyglobby": "^0.2.17"
|
"tinyglobby": "^0.2.17"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -4904,9 +4912,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/workerd": {
|
"node_modules/workerd": {
|
||||||
"version": "1.20260625.1",
|
"version": "1.20260722.1",
|
||||||
"resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20260625.1.tgz",
|
"resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20260722.1.tgz",
|
||||||
"integrity": "sha512-GApQvFX52SDM6L4u0+RRnUDB1wJOnEwoXjinkmOPtIyofWBxrlZckdegJSYc1leg++lLZ3+DQ4zMVmBqYVtzfA==",
|
"integrity": "sha512-NycKuc1x2onvsRfGGpM093vRlLFU2zHDAM0+APpccfg4+gZxDGCH27RmdDvkeBuoZyYqgLo3oAfF6re4mvC3vQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
@@ -4917,11 +4925,11 @@
|
|||||||
"node": ">=16"
|
"node": ">=16"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@cloudflare/workerd-darwin-64": "1.20260625.1",
|
"@cloudflare/workerd-darwin-64": "1.20260722.1",
|
||||||
"@cloudflare/workerd-darwin-arm64": "1.20260625.1",
|
"@cloudflare/workerd-darwin-arm64": "1.20260722.1",
|
||||||
"@cloudflare/workerd-linux-64": "1.20260625.1",
|
"@cloudflare/workerd-linux-64": "1.20260722.1",
|
||||||
"@cloudflare/workerd-linux-arm64": "1.20260625.1",
|
"@cloudflare/workerd-linux-arm64": "1.20260722.1",
|
||||||
"@cloudflare/workerd-windows-64": "1.20260625.1"
|
"@cloudflare/workerd-windows-64": "1.20260722.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/wouter": {
|
"node_modules/wouter": {
|
||||||
@@ -4939,9 +4947,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/wrangler": {
|
"node_modules/wrangler": {
|
||||||
"version": "4.105.0",
|
"version": "4.114.0",
|
||||||
"resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.105.0.tgz",
|
"resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.114.0.tgz",
|
||||||
"integrity": "sha512-7dXFH6OLj1Fv0y6ZeRPUxFTkp+duWD7/xxVi/1c0vfOeEYwIFKWB7cdqnY05DvY1Ta3BnqAwRkXfLs8PDj538g==",
|
"integrity": "sha512-M65P25t5UHA1TIJfgZXDcj+YzVobgKdRguM2QPz0xnxLFuOcuE3ErgllDht0iaho7MS4o0g/Bb4YK2+GT+bibg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT OR Apache-2.0",
|
"license": "MIT OR Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -4949,10 +4957,10 @@
|
|||||||
"@cloudflare/unenv-preset": "2.16.1",
|
"@cloudflare/unenv-preset": "2.16.1",
|
||||||
"blake3-wasm": "2.1.5",
|
"blake3-wasm": "2.1.5",
|
||||||
"esbuild": "0.28.1",
|
"esbuild": "0.28.1",
|
||||||
"miniflare": "4.20260625.0",
|
"miniflare": "4.20260722.0",
|
||||||
"path-to-regexp": "6.3.0",
|
"path-to-regexp": "6.3.0",
|
||||||
"unenv": "2.0.0-rc.24",
|
"unenv": "2.0.0-rc.24",
|
||||||
"workerd": "1.20260625.1"
|
"workerd": "1.20260722.1"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"cf-wrangler": "bin/cf-wrangler.js",
|
"cf-wrangler": "bin/cf-wrangler.js",
|
||||||
@@ -4966,7 +4974,7 @@
|
|||||||
"fsevents": "2.3.3"
|
"fsevents": "2.3.3"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@cloudflare/workers-types": "^4.20260625.1"
|
"@cloudflare/workers-types": "^5.20260722.1"
|
||||||
},
|
},
|
||||||
"peerDependenciesMeta": {
|
"peerDependenciesMeta": {
|
||||||
"@cloudflare/workers-types": {
|
"@cloudflare/workers-types": {
|
||||||
|
|||||||
+11
-11
@@ -56,26 +56,26 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@cloudflare/workers-types": "^4.20260630.1",
|
"@cloudflare/workers-types": "^4.20260630.1",
|
||||||
"@preact/preset-vite": "^2.10.5",
|
"@preact/preset-vite": "^2.10.6",
|
||||||
"@types/node": "^26.0.1",
|
"@types/node": "^26.1.1",
|
||||||
"autoprefixer": "^10.5.2",
|
"autoprefixer": "^10.5.4",
|
||||||
"opencc-js": "^1.3.2",
|
"opencc-js": "^1.4.1",
|
||||||
"postcss": "^8.5.23",
|
"postcss": "^8.5.23",
|
||||||
"tailwindcss": "^3.4.19",
|
"tailwindcss": "^3.4.19",
|
||||||
"tsx": "^4.22.4",
|
"tsx": "^4.23.1",
|
||||||
"typescript": "^6.0.3",
|
"typescript": "^6.0.3",
|
||||||
"vite": "^8.1.3",
|
"vite": "^8.1.5",
|
||||||
"wrangler": "^4.105.0"
|
"wrangler": "^4.114.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@noble/hashes": "^2.2.0",
|
"@noble/hashes": "^2.2.0",
|
||||||
"@simplewebauthn/server": "^13.3.2",
|
"@simplewebauthn/server": "^13.3.2",
|
||||||
"@tanstack/react-query": "^5.101.2",
|
"@tanstack/react-query": "^5.101.4",
|
||||||
"@zip.js/zip.js": "^2.8.26",
|
"@zip.js/zip.js": "^2.8.34",
|
||||||
"fflate": "^0.8.3",
|
"fflate": "^0.8.3",
|
||||||
"jsqr": "1.4.0",
|
"jsqr": "1.4.0",
|
||||||
"lucide-preact": "^1.22.0",
|
"lucide-preact": "^1.26.0",
|
||||||
"preact": "^10.29.3",
|
"preact": "^10.29.7",
|
||||||
"qrcode-generator": "^2.0.4",
|
"qrcode-generator": "^2.0.4",
|
||||||
"wouter": "^3.10.0"
|
"wouter": "^3.10.0"
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-8
@@ -1247,14 +1247,19 @@ export async function handleDownloadAdminBackupAttachment(request: Request, env:
|
|||||||
if (!isAdmin(actorUser)) return errorResponse('Forbidden', 403);
|
if (!isAdmin(actorUser)) return errorResponse('Forbidden', 403);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Read the request body only. Accepting these fields from the query string
|
const url = new URL(request.url);
|
||||||
// would put the master-password authentication hash in the URL, where it is
|
let input: { blobName?: unknown; masterPasswordHash?: unknown } = {};
|
||||||
// captured by request logs, browser history and Referer headers.
|
if (request.method === 'POST') {
|
||||||
let input: { blobName?: unknown; masterPasswordHash?: unknown };
|
try {
|
||||||
try {
|
input = await request.json<{ blobName?: unknown; masterPasswordHash?: unknown }>();
|
||||||
input = await request.json<{ blobName?: unknown; masterPasswordHash?: unknown }>();
|
} catch {
|
||||||
} catch {
|
return errorResponse('Backup attachment download payload is invalid', 400);
|
||||||
return errorResponse('Backup attachment download payload is invalid', 400);
|
}
|
||||||
|
} else {
|
||||||
|
input = {
|
||||||
|
blobName: url.searchParams.get('blobName') || '',
|
||||||
|
masterPasswordHash: url.searchParams.get('masterPasswordHash') || '',
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const verificationError = await requireBackupUserVerification(
|
const verificationError = await requireBackupUserVerification(
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import {
|
|||||||
handleRunAdminConfiguredBackup,
|
handleRunAdminConfiguredBackup,
|
||||||
handleUpdateAdminBackupSettings,
|
handleUpdateAdminBackupSettings,
|
||||||
} from './handlers/backup';
|
} from './handlers/backup';
|
||||||
import { errorResponse } from './utils/response';
|
|
||||||
|
|
||||||
export async function handleAdminBackupRoute(
|
export async function handleAdminBackupRoute(
|
||||||
request: Request,
|
request: Request,
|
||||||
@@ -27,22 +26,8 @@ export async function handleAdminBackupRoute(
|
|||||||
return handleAdminExportBackup(request, env, actorUser);
|
return handleAdminExportBackup(request, env, actorUser);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (path === '/api/admin/backup/blob') {
|
if (path === '/api/admin/backup/blob' && (method === 'GET' || method === 'POST')) {
|
||||||
// POST only: this endpoint requires master-password verification, and a GET
|
return handleDownloadAdminBackupAttachment(request, env, actorUser);
|
||||||
// could only carry that credential in the query string, where it would leak
|
|
||||||
// into request logs, proxy logs, browser history and Referer headers.
|
|
||||||
// The credential is the same value clients send to /identity/connect/token,
|
|
||||||
// so a leaked copy is enough to sign in as this admin.
|
|
||||||
if (method === 'POST') {
|
|
||||||
return handleDownloadAdminBackupAttachment(request, env, actorUser);
|
|
||||||
}
|
|
||||||
if (method === 'GET') {
|
|
||||||
return errorResponse(
|
|
||||||
'Use POST with a JSON body for this endpoint. Credentials must not be sent in the URL.',
|
|
||||||
405
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (path === '/api/admin/backup/settings') {
|
if (path === '/api/admin/backup/settings') {
|
||||||
|
|||||||
@@ -1,77 +1,158 @@
|
|||||||
import { useEffect, useMemo, useState } from 'preact/hooks';
|
import { useEffect, useMemo, useState } from 'preact/hooks';
|
||||||
import { Check, Copy, Download, LoaderCircle, Minus, Plus, RefreshCw, ShieldCheck } from 'lucide-preact';
|
import { Check, Copy, Minus, Plus, RefreshCw, ShieldCheck } from 'lucide-preact';
|
||||||
import { copyTextToClipboard } from '@/lib/clipboard';
|
import { copyTextToClipboard } from '@/lib/clipboard';
|
||||||
|
import { EFFLongWordList } from '@/lib/eff-word-list';
|
||||||
import { t } from '@/lib/i18n';
|
import { t } from '@/lib/i18n';
|
||||||
import {
|
|
||||||
clampInteger,
|
|
||||||
defaultGeneratorSettings,
|
|
||||||
estimateStrength,
|
|
||||||
generateValue,
|
|
||||||
normalizeGeneratorSettings,
|
|
||||||
type EmailMode,
|
|
||||||
type EmailOptions,
|
|
||||||
type GeneratorMode,
|
|
||||||
type GeneratorSettings,
|
|
||||||
type PassphraseOptions,
|
|
||||||
type PasswordOptions,
|
|
||||||
type PinOptions,
|
|
||||||
type SshKeyOptions,
|
|
||||||
type UsernameOptions,
|
|
||||||
} from '@/lib/password-generator';
|
|
||||||
import { generateSshKey, type GeneratedSshKey } from '@/lib/ssh-key-generator';
|
|
||||||
|
|
||||||
const SETTINGS_KEY = 'nodewarden.passwordGenerator.settings.v2';
|
type GeneratorMode = 'password' | 'passphrase';
|
||||||
|
|
||||||
function readSettings(): GeneratorSettings {
|
interface PasswordOptions {
|
||||||
|
length: number;
|
||||||
|
uppercase: boolean;
|
||||||
|
lowercase: boolean;
|
||||||
|
numbers: boolean;
|
||||||
|
special: boolean;
|
||||||
|
minNumbers: number;
|
||||||
|
minSpecial: number;
|
||||||
|
avoidAmbiguous: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PassphraseOptions {
|
||||||
|
words: number;
|
||||||
|
separator: string;
|
||||||
|
capitalize: boolean;
|
||||||
|
includeNumber: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SETTINGS_KEY = 'nodewarden.passwordGenerator.settings.v1';
|
||||||
|
const UPPERCASE = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||||
|
const LOWERCASE = 'abcdefghijklmnopqrstuvwxyz';
|
||||||
|
const DIGITS = '0123456789';
|
||||||
|
const SPECIAL = '!@#$%^&*';
|
||||||
|
const AMBIGUOUS = new Set(['I', 'L', 'O', 'l', 'o', '0', '1']);
|
||||||
|
|
||||||
|
const defaultPasswordOptions: PasswordOptions = {
|
||||||
|
length: 14,
|
||||||
|
uppercase: true,
|
||||||
|
lowercase: true,
|
||||||
|
numbers: true,
|
||||||
|
special: false,
|
||||||
|
minNumbers: 1,
|
||||||
|
minSpecial: 1,
|
||||||
|
avoidAmbiguous: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultPassphraseOptions: PassphraseOptions = {
|
||||||
|
words: 6,
|
||||||
|
separator: '-',
|
||||||
|
capitalize: false,
|
||||||
|
includeNumber: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
function clamp(value: unknown, minimum: number, maximum: number, fallback: number): number {
|
||||||
|
const parsed = Number(value);
|
||||||
|
return Number.isFinite(parsed) ? Math.min(maximum, Math.max(minimum, Math.round(parsed))) : fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readSettings(): { mode: GeneratorMode; password: PasswordOptions; passphrase: PassphraseOptions } {
|
||||||
try {
|
try {
|
||||||
const current = localStorage.getItem(SETTINGS_KEY);
|
const stored = JSON.parse(localStorage.getItem(SETTINGS_KEY) || '{}') as Partial<{ mode: GeneratorMode; password: Partial<PasswordOptions>; passphrase: Partial<PassphraseOptions> }>;
|
||||||
if (current) return normalizeGeneratorSettings(JSON.parse(current));
|
return {
|
||||||
|
mode: stored.mode === 'passphrase' ? 'passphrase' : 'password',
|
||||||
// Preserve compatible options for users upgrading from the original generator.
|
password: {
|
||||||
const legacy = JSON.parse(localStorage.getItem('nodewarden.passwordGenerator.settings.v1') || '{}');
|
...defaultPasswordOptions,
|
||||||
return normalizeGeneratorSettings(legacy);
|
...stored.password,
|
||||||
|
length: clamp(stored.password?.length, 5, 128, defaultPasswordOptions.length),
|
||||||
|
minNumbers: clamp(stored.password?.minNumbers, 0, 9, defaultPasswordOptions.minNumbers),
|
||||||
|
minSpecial: clamp(stored.password?.minSpecial, 0, 9, defaultPasswordOptions.minSpecial),
|
||||||
|
},
|
||||||
|
passphrase: {
|
||||||
|
...defaultPassphraseOptions,
|
||||||
|
...stored.passphrase,
|
||||||
|
words: clamp(stored.passphrase?.words, 3, 20, defaultPassphraseOptions.words),
|
||||||
|
separator: String(stored.passphrase?.separator ?? defaultPassphraseOptions.separator).slice(0, 1),
|
||||||
|
},
|
||||||
|
};
|
||||||
} catch {
|
} catch {
|
||||||
return defaultGeneratorSettings;
|
return { mode: 'password', password: defaultPasswordOptions, passphrase: defaultPassphraseOptions };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function randomIndex(length: number): number {
|
||||||
|
const range = 0x1_0000_0000;
|
||||||
|
const upperBound = Math.floor(range / length) * length;
|
||||||
|
const buffer = new Uint32Array(1);
|
||||||
|
do crypto.getRandomValues(buffer); while (buffer[0] >= upperBound);
|
||||||
|
return buffer[0] % length;
|
||||||
|
}
|
||||||
|
|
||||||
|
function pick(characters: string): string {
|
||||||
|
return characters[randomIndex(characters.length)];
|
||||||
|
}
|
||||||
|
|
||||||
|
function shuffle(value: string[]): string[] {
|
||||||
|
for (let index = value.length - 1; index > 0; index -= 1) {
|
||||||
|
const next = randomIndex(index + 1);
|
||||||
|
[value[index], value[next]] = [value[next], value[index]];
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function filtered(characters: string, avoidAmbiguous: boolean): string {
|
||||||
|
return avoidAmbiguous ? characters.split('').filter((character) => !AMBIGUOUS.has(character)).join('') : characters;
|
||||||
|
}
|
||||||
|
|
||||||
|
function generatePassword(options: PasswordOptions): string {
|
||||||
|
const sets: Array<{ chars: string; minimum: number }> = [];
|
||||||
|
if (options.uppercase) sets.push({ chars: filtered(UPPERCASE, options.avoidAmbiguous), minimum: 1 });
|
||||||
|
if (options.lowercase) sets.push({ chars: filtered(LOWERCASE, options.avoidAmbiguous), minimum: 1 });
|
||||||
|
if (options.numbers) sets.push({ chars: filtered(DIGITS, options.avoidAmbiguous), minimum: options.minNumbers });
|
||||||
|
if (options.special) sets.push({ chars: SPECIAL, minimum: options.minSpecial });
|
||||||
|
if (!sets.length) sets.push({ chars: filtered(LOWERCASE, options.avoidAmbiguous), minimum: 1 });
|
||||||
|
|
||||||
|
const minimumLength = sets.reduce((total, set) => total + set.minimum, 0);
|
||||||
|
const length = Math.max(options.length, minimumLength, 5);
|
||||||
|
const allCharacters = sets.map((set) => set.chars).join('');
|
||||||
|
const characters = sets.flatMap((set) => Array.from({ length: set.minimum }, () => pick(set.chars)));
|
||||||
|
while (characters.length < length) characters.push(pick(allCharacters));
|
||||||
|
return shuffle(characters).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function generatePassphrase(options: PassphraseOptions): string {
|
||||||
|
const words = Array.from({ length: options.words }, () => EFFLongWordList[randomIndex(EFFLongWordList.length)]);
|
||||||
|
if (options.capitalize) {
|
||||||
|
for (let index = 0; index < words.length; index += 1) words[index] = words[index][0].toUpperCase() + words[index].slice(1);
|
||||||
|
}
|
||||||
|
if (options.includeNumber) words[randomIndex(words.length)] += String(randomIndex(10));
|
||||||
|
return words.join(options.separator);
|
||||||
|
}
|
||||||
|
|
||||||
|
function strengthLabel(mode: GeneratorMode, value: string): { label: string; score: number } {
|
||||||
|
const score = mode === 'password' ? Math.min(4, Math.max(1, Math.floor(value.length / 5))) : Math.min(4, Math.max(1, Math.floor(value.split(/[-_. ]/).filter(Boolean).length / 2)));
|
||||||
|
return { score, label: t(['txt_password_strength_weak', 'txt_password_strength_fair', 'txt_password_strength_good', 'txt_password_strength_strong'][score - 1]) };
|
||||||
|
}
|
||||||
|
|
||||||
export default function PasswordGeneratorPage() {
|
export default function PasswordGeneratorPage() {
|
||||||
const initial = useMemo(readSettings, []);
|
const initial = useMemo(readSettings, []);
|
||||||
const [settings, setSettings] = useState<GeneratorSettings>(initial);
|
const [mode, setMode] = useState<GeneratorMode>(initial.mode);
|
||||||
|
const [passwordOptions, setPasswordOptions] = useState<PasswordOptions>(initial.password);
|
||||||
|
const [passphraseOptions, setPassphraseOptions] = useState<PassphraseOptions>(initial.passphrase);
|
||||||
const [seed, setSeed] = useState(0);
|
const [seed, setSeed] = useState(0);
|
||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
const [sshKey, setSshKey] = useState<GeneratedSshKey | null>(null);
|
|
||||||
const [sshKeyError, setSshKeyError] = useState('');
|
|
||||||
const [sshKeyLoading, setSshKeyLoading] = useState(false);
|
|
||||||
|
|
||||||
const generated = useMemo(() => settings.mode === 'sshKey' ? sshKey?.fingerprint || '' : generateValue(settings), [settings, seed, sshKey]);
|
const generated = useMemo(
|
||||||
const strength = useMemo(
|
() => (mode === 'password' ? generatePassword(passwordOptions) : generatePassphrase(passphraseOptions)),
|
||||||
() => estimateStrength(settings.mode, generated, settings.mode === 'passphrase' ? settings.passphrase.words : undefined),
|
[mode, passwordOptions, passphraseOptions, seed]
|
||||||
[generated, settings.mode, settings.passphrase.words],
|
|
||||||
);
|
);
|
||||||
const strengthLabel = strength
|
const strength = useMemo(() => strengthLabel(mode, generated), [generated, mode]);
|
||||||
? t(['txt_password_strength_weak', 'txt_password_strength_fair', 'txt_password_strength_good', 'txt_password_strength_strong'][strength - 1])
|
|
||||||
: '';
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
try {
|
try {
|
||||||
localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings));
|
localStorage.setItem(SETTINGS_KEY, JSON.stringify({ mode, password: passwordOptions, passphrase: passphraseOptions }));
|
||||||
} catch {
|
} catch {
|
||||||
// The generator remains fully usable when browser storage is unavailable.
|
// The generator remains fully usable when browser storage is unavailable.
|
||||||
}
|
}
|
||||||
}, [settings]);
|
}, [mode, passwordOptions, passphraseOptions]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (settings.mode !== 'sshKey') return;
|
|
||||||
let cancelled = false;
|
|
||||||
setSshKeyLoading(true);
|
|
||||||
setSshKeyError('');
|
|
||||||
void generateSshKey({ ...settings.sshKey, comment: '' })
|
|
||||||
.then((value) => { if (!cancelled) setSshKey(value); })
|
|
||||||
.catch(() => { if (!cancelled) { setSshKey(null); setSshKeyError(t('txt_generator_ssh_error')); } })
|
|
||||||
.finally(() => { if (!cancelled) setSshKeyLoading(false); });
|
|
||||||
return () => { cancelled = true; };
|
|
||||||
}, [settings.mode, settings.sshKey.type, settings.sshKey.rsaLength, seed]);
|
|
||||||
|
|
||||||
const regenerate = () => {
|
const regenerate = () => {
|
||||||
setCopied(false);
|
setCopied(false);
|
||||||
@@ -79,49 +160,17 @@ export default function PasswordGeneratorPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const copy = async () => {
|
const copy = async () => {
|
||||||
const value = settings.mode === 'sshKey' && sshKey ? publicKeyWithComment(sshKey.publicKey, settings.sshKey.comment) : generated;
|
await copyTextToClipboard(generated, { onSuccess: () => setCopied(true), onError: () => setCopied(false) });
|
||||||
await copyTextToClipboard(value, { onSuccess: () => setCopied(true), onError: () => setCopied(false) });
|
|
||||||
window.setTimeout(() => setCopied(false), 1600);
|
window.setTimeout(() => setCopied(false), 1600);
|
||||||
};
|
};
|
||||||
|
|
||||||
const changeMode = (mode: GeneratorMode) => {
|
|
||||||
setSettings((current) => ({ ...current, mode }));
|
|
||||||
setCopied(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const changePasswordOption = <K extends keyof PasswordOptions>(key: K, value: PasswordOptions[K]) => {
|
const changePasswordOption = <K extends keyof PasswordOptions>(key: K, value: PasswordOptions[K]) => {
|
||||||
setSettings((current) => ({ ...current, password: { ...current.password, [key]: value } }));
|
setPasswordOptions((current) => ({ ...current, [key]: value }));
|
||||||
setCopied(false);
|
setCopied(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const changeCharacterType = (key: 'uppercase' | 'lowercase' | 'numbers' | 'special', checked: boolean) => {
|
|
||||||
const enabled = ['uppercase', 'lowercase', 'numbers', 'special'].filter((item) => settings.password[item as 'uppercase']);
|
|
||||||
if (!checked && enabled.length === 1 && enabled[0] === key) return;
|
|
||||||
changePasswordOption(key, checked);
|
|
||||||
};
|
|
||||||
|
|
||||||
const changePassphraseOption = <K extends keyof PassphraseOptions>(key: K, value: PassphraseOptions[K]) => {
|
const changePassphraseOption = <K extends keyof PassphraseOptions>(key: K, value: PassphraseOptions[K]) => {
|
||||||
setSettings((current) => ({ ...current, passphrase: { ...current.passphrase, [key]: value } }));
|
setPassphraseOptions((current) => ({ ...current, [key]: value }));
|
||||||
setCopied(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const changePinOption = <K extends keyof PinOptions>(key: K, value: PinOptions[K]) => {
|
|
||||||
setSettings((current) => ({ ...current, pin: { ...current.pin, [key]: value } }));
|
|
||||||
setCopied(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const changeUsernameOption = <K extends keyof UsernameOptions>(key: K, value: UsernameOptions[K]) => {
|
|
||||||
setSettings((current) => ({ ...current, username: { ...current.username, [key]: value } }));
|
|
||||||
setCopied(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const changeEmailOption = <K extends keyof EmailOptions>(key: K, value: EmailOptions[K]) => {
|
|
||||||
setSettings((current) => ({ ...current, email: { ...current.email, [key]: value } }));
|
|
||||||
setCopied(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const changeSshKeyOption = <K extends keyof SshKeyOptions>(key: K, value: SshKeyOptions[K]) => {
|
|
||||||
setSettings((current) => ({ ...current, sshKey: { ...current.sshKey, [key]: value } }));
|
|
||||||
setCopied(false);
|
setCopied(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -129,182 +178,62 @@ export default function PasswordGeneratorPage() {
|
|||||||
<section className="generator-page" aria-label={t('txt_password_generator')}>
|
<section className="generator-page" aria-label={t('txt_password_generator')}>
|
||||||
<div className="generator-layout">
|
<div className="generator-layout">
|
||||||
<section className="generator-output-card" aria-live="polite">
|
<section className="generator-output-card" aria-live="polite">
|
||||||
<div className="settings-category-tabs generator-mode-tabs" role="tablist" aria-label={t('txt_generator_type')}>
|
<div className="settings-category-tabs" role="tablist" aria-label={t('txt_generator_type')}>
|
||||||
{([
|
<button type="button" role="tab" aria-selected={mode === 'password'} className={`settings-category-tab ${mode === 'password' ? 'active' : ''}`} onClick={() => setMode('password')}>{t('txt_password')}</button>
|
||||||
['password', 'txt_password'],
|
<button type="button" role="tab" aria-selected={mode === 'passphrase'} className={`settings-category-tab ${mode === 'passphrase' ? 'active' : ''}`} onClick={() => setMode('passphrase')}>{t('txt_passphrase')}</button>
|
||||||
['passphrase', 'txt_passphrase'],
|
</div>
|
||||||
['pin', 'txt_generator_pin'],
|
<output className="generator-value" aria-label={t('txt_generated_password')}>{generated}</output>
|
||||||
['username', 'txt_generator_username'],
|
<div className="generator-strength-row">
|
||||||
['email', 'txt_generator_email_alias'],
|
<div className="generator-strength" aria-label={`${t('txt_password_strength')}: ${strength.label}`}>
|
||||||
['sshKey', 'txt_generator_ssh_key'],
|
{[1, 2, 3, 4].map((level) => <span key={level} className={level <= strength.score ? `active level-${strength.score}` : ''} />)}
|
||||||
] as const).map(([mode, label]) => (
|
</div>
|
||||||
<button key={mode} type="button" role="tab" aria-selected={settings.mode === mode} className={`settings-category-tab ${settings.mode === mode ? 'active' : ''}`} onClick={() => changeMode(mode)}>{t(label)}</button>
|
<span><ShieldCheck size={15} /> {strength.label}</span>
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
{settings.mode === 'sshKey' ? (
|
|
||||||
<SshKeyOutput value={sshKey} loading={sshKeyLoading} error={sshKeyError} comment={settings.sshKey.comment} />
|
|
||||||
) : <output className={`generator-value ${generated ? '' : 'empty'}`} aria-label={t('txt_generated_value')}>{generated || t('txt_generator_email_required_hint')}</output>}
|
|
||||||
{settings.mode !== 'sshKey' && <div className="generator-meta-row">
|
|
||||||
{strength > 0 ? (
|
|
||||||
<>
|
|
||||||
<div className="generator-strength" aria-label={`${t('txt_password_strength')}: ${strengthLabel}`}>
|
|
||||||
{[1, 2, 3, 4].map((level) => <span key={level} className={level <= strength ? `active level-${strength}` : ''} />)}
|
|
||||||
</div>
|
|
||||||
<span><ShieldCheck size={15} /> {strengthLabel}</span>
|
|
||||||
</>
|
|
||||||
) : <span />}
|
|
||||||
<span>{t('txt_generator_character_count', { count: generated.length })}</span>
|
|
||||||
</div>}
|
|
||||||
<div className="actions generator-actions">
|
<div className="actions generator-actions">
|
||||||
<button type="button" className="btn btn-primary" disabled={sshKeyLoading} onClick={regenerate}>{sshKeyLoading ? <LoaderCircle size={16} className="btn-icon generator-spinner" /> : <RefreshCw size={16} className="btn-icon" />}{t('txt_regenerate')}</button>
|
<button type="button" className="btn btn-primary" onClick={regenerate}><RefreshCw size={16} className="btn-icon" />{t('txt_regenerate')}</button>
|
||||||
<button type="button" className="btn btn-secondary" disabled={(settings.mode === 'sshKey' && !sshKey) || !generated} onClick={() => void copy()}><Copy size={16} className="btn-icon" />{copied ? t('txt_copied') : settings.mode === 'sshKey' ? t('txt_generator_copy_public_key') : t('txt_copy')}</button>
|
<button type="button" className="btn btn-secondary" onClick={() => void copy()}><Copy size={16} className="btn-icon" />{copied ? t('txt_copied') : t('txt_copy')}</button>
|
||||||
</div>
|
</div>
|
||||||
<p className="generator-security-note"><Check size={15} />{t(settings.mode === 'sshKey' ? 'txt_generator_ssh_security_note' : 'txt_generator_security_note')}</p>
|
<p className="generator-security-note"><Check size={15} />{t('txt_generator_security_note')}</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="generator-options-card" aria-labelledby="generator-options-title">
|
<section className="generator-options-card" aria-labelledby="generator-options-title">
|
||||||
<h2 id="generator-options-title">{t('txt_options')}</h2>
|
<h2 id="generator-options-title">{t('txt_options')}</h2>
|
||||||
{settings.mode === 'password' && (
|
{mode === 'password' ? (
|
||||||
<PasswordOptionFields options={settings.password} onChange={changePasswordOption} onCharacterTypeChange={changeCharacterType} />
|
|
||||||
)}
|
|
||||||
{settings.mode === 'passphrase' && (
|
|
||||||
<>
|
<>
|
||||||
<GeneratorNumberStepper id="words" label={t('txt_generator_words')} value={settings.passphrase.words} minimum={3} maximum={20} fallback={6} onChange={(value) => changePassphraseOption('words', value)} />
|
<GeneratorNumberStepper id="length" label={t('txt_generator_length')} value={passwordOptions.length} minimum={5} maximum={128} fallback={14} onChange={(value) => changePasswordOption('length', value)} />
|
||||||
<label className="generator-select-field" htmlFor="generator-word-list"><span>{t('txt_generator_word_list')}</span><select id="generator-word-list" className="input" value={settings.passphrase.wordList} onChange={(event) => changePassphraseOption('wordList', event.currentTarget.value as 'eff' | 'custom')}><option value="eff">{t('txt_generator_eff_word_list')}</option><option value="custom">{t('txt_generator_custom_word_list')}</option></select></label>
|
<fieldset className="generator-option-group"><legend>{t('txt_generator_character_types')}</legend>
|
||||||
{settings.passphrase.wordList === 'custom' && <label className="generator-text-field" htmlFor="generator-custom-words"><span>{t('txt_generator_custom_words')}</span><textarea id="generator-custom-words" className="input generator-word-list-input" rows={6} spellcheck={false} placeholder={t('txt_generator_custom_words_placeholder')} value={settings.passphrase.customWords} onInput={(event) => changePassphraseOption('customWords', event.currentTarget.value)} /></label>}
|
<GeneratorToggle checked={passwordOptions.uppercase} onChange={(checked) => changePasswordOption('uppercase', checked)} label={t('txt_generator_uppercase')} />
|
||||||
<label className="generator-number-field" htmlFor="generator-separator"><span>{t('txt_generator_separator')}</span><input id="generator-separator" className="input" type="text" maxLength={1} value={settings.passphrase.separator} onInput={(event) => changePassphraseOption('separator', event.currentTarget.value.slice(0, 1))} /></label>
|
<GeneratorToggle checked={passwordOptions.lowercase} onChange={(checked) => changePasswordOption('lowercase', checked)} label={t('txt_generator_lowercase')} />
|
||||||
|
<GeneratorToggle checked={passwordOptions.numbers} onChange={(checked) => changePasswordOption('numbers', checked)} label={t('txt_generator_numbers')} />
|
||||||
|
{passwordOptions.numbers && <GeneratorNumberStepper id="min-numbers" compact label={t('txt_generator_minimum')} value={passwordOptions.minNumbers} minimum={0} maximum={9} fallback={1} onChange={(value) => changePasswordOption('minNumbers', value)} />}
|
||||||
|
<GeneratorToggle checked={passwordOptions.special} onChange={(checked) => changePasswordOption('special', checked)} label={t('txt_generator_special')} />
|
||||||
|
{passwordOptions.special && <GeneratorNumberStepper id="min-special" compact label={t('txt_generator_minimum')} value={passwordOptions.minSpecial} minimum={0} maximum={9} fallback={1} onChange={(value) => changePasswordOption('minSpecial', value)} />}
|
||||||
|
</fieldset>
|
||||||
|
<GeneratorToggle checked={passwordOptions.avoidAmbiguous} onChange={(checked) => changePasswordOption('avoidAmbiguous', checked)} label={t('txt_generator_avoid_ambiguous')} />
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<GeneratorNumberStepper id="words" label={t('txt_generator_words')} value={passphraseOptions.words} minimum={3} maximum={20} fallback={6} onChange={(value) => changePassphraseOption('words', value)} />
|
||||||
|
<label className="generator-number-field" htmlFor="generator-separator"><span>{t('txt_generator_separator')}</span><input id="generator-separator" className="input" type="text" maxLength={1} value={passphraseOptions.separator} onInput={(event) => changePassphraseOption('separator', event.currentTarget.value.slice(0, 1))} /></label>
|
||||||
<div className="generator-option-group">
|
<div className="generator-option-group">
|
||||||
<GeneratorToggle checked={settings.passphrase.capitalize} onChange={(checked) => changePassphraseOption('capitalize', checked)} label={t('txt_generator_capitalize')} />
|
<GeneratorToggle checked={passphraseOptions.capitalize} onChange={(checked) => changePassphraseOption('capitalize', checked)} label={t('txt_generator_capitalize')} />
|
||||||
<GeneratorToggle checked={settings.passphrase.includeNumber} onChange={(checked) => changePassphraseOption('includeNumber', checked)} label={t('txt_generator_include_number')} />
|
<GeneratorToggle checked={passphraseOptions.includeNumber} onChange={(checked) => changePassphraseOption('includeNumber', checked)} label={t('txt_generator_include_number')} />
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{settings.mode === 'pin' && (
|
|
||||||
<>
|
|
||||||
<GeneratorNumberStepper id="pin-length" label={t('txt_generator_length')} value={settings.pin.length} minimum={3} maximum={64} fallback={6} onChange={(value) => changePinOption('length', value)} />
|
|
||||||
<p className="generator-options-note">{t('txt_generator_pin_description')}</p>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{settings.mode === 'username' && (
|
|
||||||
<UsernameOptionFields options={settings.username} onChange={changeUsernameOption} />
|
|
||||||
)}
|
|
||||||
{settings.mode === 'email' && (
|
|
||||||
<EmailOptionFields options={settings.email} onChange={changeEmailOption} />
|
|
||||||
)}
|
|
||||||
{settings.mode === 'sshKey' && (
|
|
||||||
<SshKeyOptionFields options={settings.sshKey} onChange={changeSshKeyOption} />
|
|
||||||
)}
|
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function PasswordOptionFields(props: { options: PasswordOptions; onChange: <K extends keyof PasswordOptions>(key: K, value: PasswordOptions[K]) => void; onCharacterTypeChange: (key: 'uppercase' | 'lowercase' | 'numbers' | 'special', checked: boolean) => void }) {
|
|
||||||
const { options } = props;
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<GeneratorNumberStepper id="length" label={t('txt_generator_length')} value={options.length} minimum={5} maximum={128} fallback={16} onChange={(value) => props.onChange('length', value)} />
|
|
||||||
<fieldset className="generator-option-group"><legend>{t('txt_generator_character_types')}</legend>
|
|
||||||
<GeneratorToggle checked={options.uppercase} onChange={(checked) => props.onCharacterTypeChange('uppercase', checked)} label={t('txt_generator_uppercase')} />
|
|
||||||
{options.uppercase && <GeneratorNumberStepper id="min-uppercase" compact label={t('txt_generator_minimum')} value={options.minUppercase} minimum={0} maximum={9} fallback={1} onChange={(value) => props.onChange('minUppercase', value)} />}
|
|
||||||
<GeneratorToggle checked={options.lowercase} onChange={(checked) => props.onCharacterTypeChange('lowercase', checked)} label={t('txt_generator_lowercase')} />
|
|
||||||
{options.lowercase && <GeneratorNumberStepper id="min-lowercase" compact label={t('txt_generator_minimum')} value={options.minLowercase} minimum={0} maximum={9} fallback={1} onChange={(value) => props.onChange('minLowercase', value)} />}
|
|
||||||
<GeneratorToggle checked={options.numbers} onChange={(checked) => props.onCharacterTypeChange('numbers', checked)} label={t('txt_generator_numbers')} />
|
|
||||||
{options.numbers && <GeneratorNumberStepper id="min-numbers" compact label={t('txt_generator_minimum')} value={options.minNumbers} minimum={0} maximum={9} fallback={1} onChange={(value) => props.onChange('minNumbers', value)} />}
|
|
||||||
<GeneratorToggle checked={options.special} onChange={(checked) => props.onCharacterTypeChange('special', checked)} label={t('txt_generator_special')} />
|
|
||||||
{options.special && <GeneratorNumberStepper id="min-special" compact label={t('txt_generator_minimum')} value={options.minSpecial} minimum={0} maximum={9} fallback={1} onChange={(value) => props.onChange('minSpecial', value)} />}
|
|
||||||
</fieldset>
|
|
||||||
<GeneratorToggle checked={options.avoidAmbiguous} onChange={(checked) => props.onChange('avoidAmbiguous', checked)} label={t('txt_generator_avoid_ambiguous')} />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function UsernameOptionFields(props: { options: UsernameOptions; onChange: <K extends keyof UsernameOptions>(key: K, value: UsernameOptions[K]) => void }) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<GeneratorNumberStepper id="username-words" label={t('txt_generator_words')} value={props.options.words} minimum={1} maximum={10} fallback={2} onChange={(value) => props.onChange('words', value)} />
|
|
||||||
<div className="generator-option-group">
|
|
||||||
<GeneratorToggle checked={props.options.capitalize} onChange={(checked) => props.onChange('capitalize', checked)} label={t('txt_generator_capitalize')} />
|
|
||||||
<GeneratorToggle checked={props.options.includeNumber} onChange={(checked) => props.onChange('includeNumber', checked)} label={t('txt_generator_include_number')} />
|
|
||||||
</div>
|
|
||||||
<label className="generator-select-field" htmlFor="generator-username-word-list"><span>{t('txt_generator_word_list')}</span><select id="generator-username-word-list" className="input" value={props.options.wordList} onChange={(event) => props.onChange('wordList', event.currentTarget.value as 'eff' | 'custom')}><option value="eff">{t('txt_generator_eff_word_list')}</option><option value="custom">{t('txt_generator_custom_word_list')}</option></select></label>
|
|
||||||
{props.options.wordList === 'custom' && <label className="generator-text-field" htmlFor="generator-username-custom-words"><span>{t('txt_generator_custom_words')}</span><textarea id="generator-username-custom-words" className="input generator-word-list-input" rows={6} spellcheck={false} placeholder={t('txt_generator_custom_words_placeholder')} value={props.options.customWords} onInput={(event) => props.onChange('customWords', event.currentTarget.value)} /></label>}
|
|
||||||
<label className="generator-text-field" htmlFor="generator-username-custom-word"><span>{t('txt_generator_custom_word')}</span><input id="generator-username-custom-word" className="input" type="text" autocomplete="off" maxLength={128} value={props.options.customWord} onInput={(event) => props.onChange('customWord', event.currentTarget.value)} /></label>
|
|
||||||
<label className="generator-text-field" htmlFor="generator-username-delimiter"><span>{t('txt_generator_separator')}</span><input id="generator-username-delimiter" className="input" type="text" autocomplete="off" maxLength={8} value={props.options.delimiter} onInput={(event) => props.onChange('delimiter', event.currentTarget.value.slice(0, 8))} /></label>
|
|
||||||
<p className="generator-options-note">{t('txt_generator_long_word_username_description')}</p>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function EmailOptionFields(props: { options: EmailOptions; onChange: <K extends keyof EmailOptions>(key: K, value: EmailOptions[K]) => void }) {
|
|
||||||
const types: Array<[EmailMode, string]> = [
|
|
||||||
['plusAddressed', 'txt_generator_plus_addressed_email'],
|
|
||||||
['catchAll', 'txt_generator_catch_all_email'],
|
|
||||||
['subdomain', 'txt_generator_subdomain_email'],
|
|
||||||
];
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<label className="generator-select-field" htmlFor="generator-email-type"><span>{t('txt_generator_email_type')}</span><select id="generator-email-type" className="input" value={props.options.type} onChange={(event) => props.onChange('type', event.currentTarget.value as EmailMode)}>{types.map(([value, label]) => <option key={value} value={value}>{t(label)}</option>)}</select></label>
|
|
||||||
{props.options.type === 'catchAll'
|
|
||||||
? <label className="generator-text-field" htmlFor="generator-domain"><span>{t('txt_generator_domain')}</span><input id="generator-domain" className="input" type="text" autocomplete="off" value={props.options.domain} onInput={(event) => props.onChange('domain', event.currentTarget.value)} /></label>
|
|
||||||
: <label className="generator-text-field" htmlFor="generator-email"><span>{t('txt_generator_email')}</span><input id="generator-email" className="input" type="email" autocomplete="off" value={props.options.email} onInput={(event) => props.onChange('email', event.currentTarget.value)} /></label>}
|
|
||||||
<p className="generator-options-note">{t('txt_generator_email_description')}</p>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function publicKeyWithComment(publicKey: string, comment: string): string {
|
|
||||||
const base = publicKey.trim().split(/\s+/).slice(0, 2).join(' ');
|
|
||||||
const safeComment = comment.replace(/[\r\n]+/g, ' ').trim();
|
|
||||||
return safeComment ? `${base} ${safeComment}` : base;
|
|
||||||
}
|
|
||||||
|
|
||||||
function downloadText(filename: string, value: string): void {
|
|
||||||
const url = URL.createObjectURL(new Blob([value], { type: 'text/plain;charset=utf-8' }));
|
|
||||||
const anchor = document.createElement('a');
|
|
||||||
anchor.href = url;
|
|
||||||
anchor.download = filename;
|
|
||||||
anchor.style.display = 'none';
|
|
||||||
document.body.appendChild(anchor);
|
|
||||||
anchor.click();
|
|
||||||
anchor.remove();
|
|
||||||
window.setTimeout(() => URL.revokeObjectURL(url), 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
function SshKeyOutput(props: { value: GeneratedSshKey | null; loading: boolean; error: string; comment: string }) {
|
|
||||||
if (props.loading) return <div className="generator-key-status"><LoaderCircle size={24} className="generator-spinner" /><span>{t('txt_generator_ssh_generating')}</span></div>;
|
|
||||||
if (props.error) return <div className="generator-key-status error">{props.error}</div>;
|
|
||||||
if (!props.value) return null;
|
|
||||||
const publicKey = publicKeyWithComment(props.value.publicKey, props.comment);
|
|
||||||
const copyField = (value: string) => void copyTextToClipboard(value);
|
|
||||||
return (
|
|
||||||
<div className="generator-key-output">
|
|
||||||
<div className="generator-key-summary"><strong>{props.value.type}{props.value.type === 'RSA' ? ` ${props.value.bits}` : ''}</strong><code>{props.value.fingerprint}</code></div>
|
|
||||||
<div className="generator-key-field"><span>{t('txt_generator_public_key')}</span><code>{publicKey}</code><div className="generator-key-field-actions"><button type="button" className="btn btn-secondary small" onClick={() => copyField(publicKey)}><Copy size={14} />{t('txt_copy')}</button><button type="button" className="btn btn-secondary small" onClick={() => downloadText('id_nodewarden.pub', `${publicKey}\n`)}><Download size={14} />{t('txt_download')}</button></div></div>
|
|
||||||
<details className="generator-private-key"><summary>{t('txt_generator_private_key')}</summary><code>{props.value.privateKey}</code><div className="generator-key-field-actions"><button type="button" className="btn btn-secondary small" onClick={() => copyField(props.value!.privateKey)}><Copy size={14} />{t('txt_copy')}</button><button type="button" className="btn btn-secondary small" onClick={() => downloadText('id_nodewarden', props.value!.privateKey)}><Download size={14} />{t('txt_download')}</button></div></details>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function SshKeyOptionFields(props: { options: SshKeyOptions; onChange: <K extends keyof SshKeyOptions>(key: K, value: SshKeyOptions[K]) => void }) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<label className="generator-select-field" htmlFor="generator-ssh-type"><span>{t('txt_generator_ssh_algorithm')}</span><select id="generator-ssh-type" className="input" value={props.options.type} onChange={(event) => props.onChange('type', event.currentTarget.value as SshKeyOptions['type'])}><option value="ed25519">Ed25519</option><option value="rsa">RSA</option></select></label>
|
|
||||||
{props.options.type === 'rsa' && <label className="generator-select-field" htmlFor="generator-rsa-length"><span>{t('txt_generator_key_length')}</span><select id="generator-rsa-length" className="input" value={props.options.rsaLength} onChange={(event) => props.onChange('rsaLength', Number(event.currentTarget.value) as SshKeyOptions['rsaLength'])}><option value={2048}>2048</option><option value={3072}>3072</option><option value={4096}>4096</option></select></label>}
|
|
||||||
<label className="generator-text-field" htmlFor="generator-ssh-comment"><span>{t('txt_generator_ssh_comment')}</span><input id="generator-ssh-comment" className="input" type="text" autocomplete="off" maxLength={256} placeholder="user@example.com" value={props.options.comment} onInput={(event) => props.onChange('comment', event.currentTarget.value)} /></label>
|
|
||||||
<p className="generator-options-note">{t(props.options.type === 'rsa' ? 'txt_generator_ssh_rsa_description' : 'txt_generator_ssh_ed25519_description')}</p>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function GeneratorToggle(props: { checked: boolean; label: string; onChange: (checked: boolean) => void }) {
|
function GeneratorToggle(props: { checked: boolean; label: string; onChange: (checked: boolean) => void }) {
|
||||||
return <label className="generator-toggle"><input type="checkbox" checked={props.checked} onChange={(event) => props.onChange(event.currentTarget.checked)} /><span aria-hidden="true" /><strong>{props.label}</strong></label>;
|
return <label className="generator-toggle"><input type="checkbox" checked={props.checked} onChange={(event) => props.onChange(event.currentTarget.checked)} /><span aria-hidden="true" /><strong>{props.label}</strong></label>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function GeneratorNumberStepper(props: { id: string; label: string; value: number; minimum: number; maximum: number; fallback: number; compact?: boolean; onChange: (value: number) => void }) {
|
function GeneratorNumberStepper(props: { id: string; label: string; value: number; minimum: number; maximum: number; fallback: number; compact?: boolean; onChange: (value: number) => void }) {
|
||||||
const id = `generator-stepper-${props.id}`;
|
const id = `generator-stepper-${props.id}`;
|
||||||
const setValue = (value: number) => props.onChange(clampInteger(value, props.minimum, props.maximum, props.fallback));
|
const setValue = (value: number) => props.onChange(clamp(value, props.minimum, props.maximum, props.fallback));
|
||||||
return (
|
return (
|
||||||
<div className={`generator-number-field ${props.compact ? 'compact' : ''}`}>
|
<div className={`generator-number-field ${props.compact ? 'compact' : ''}`}>
|
||||||
<label htmlFor={id}>{props.label}</label>
|
<label htmlFor={id}>{props.label}</label>
|
||||||
|
|||||||
@@ -8,10 +8,6 @@ const de: Record<string, string> = {
|
|||||||
"nav_vault_items": "Tresor",
|
"nav_vault_items": "Tresor",
|
||||||
"nav_sends": "Sendungen",
|
"nav_sends": "Sendungen",
|
||||||
"nav_generator": "Passwortwerkzeug", "txt_password_generator": "Passwortgenerator", "txt_password_generator_description": "Erstellen Sie lokal auf diesem Gerät ein starkes, einzigartiges Passwort.", "txt_generator_type": "Generatortyp", "txt_passphrase": "Kennwortsatz", "txt_generated_password": "Generiertes Passwort", "txt_password_strength": "Stärke", "txt_password_strength_weak": "Schwach", "txt_password_strength_fair": "Mittel", "txt_password_strength_good": "Gut", "txt_password_strength_strong": "Stark", "txt_generator_security_note": "Die Erzeugung erfolgt lokal. Ihr Passwort wird nie an den Server gesendet.", "txt_generator_length": "Länge", "txt_generator_character_types": "Zeichentypen", "txt_generator_uppercase": "Großbuchstaben (A-Z)", "txt_generator_lowercase": "Kleinbuchstaben (a-z)", "txt_generator_numbers": "Zahlen (0-9)", "txt_generator_special": "Sonderzeichen (!@#$%^&*)", "txt_generator_minimum": "Mindestanzahl", "txt_generator_avoid_ambiguous": "Verwechselbare Zeichen vermeiden", "txt_generator_words": "Anzahl der Wörter", "txt_generator_separator": "Worttrenner", "txt_generator_capitalize": "Großschreibung", "txt_generator_include_number": "Eine Zahl einfügen",
|
"nav_generator": "Passwortwerkzeug", "txt_password_generator": "Passwortgenerator", "txt_password_generator_description": "Erstellen Sie lokal auf diesem Gerät ein starkes, einzigartiges Passwort.", "txt_generator_type": "Generatortyp", "txt_passphrase": "Kennwortsatz", "txt_generated_password": "Generiertes Passwort", "txt_password_strength": "Stärke", "txt_password_strength_weak": "Schwach", "txt_password_strength_fair": "Mittel", "txt_password_strength_good": "Gut", "txt_password_strength_strong": "Stark", "txt_generator_security_note": "Die Erzeugung erfolgt lokal. Ihr Passwort wird nie an den Server gesendet.", "txt_generator_length": "Länge", "txt_generator_character_types": "Zeichentypen", "txt_generator_uppercase": "Großbuchstaben (A-Z)", "txt_generator_lowercase": "Kleinbuchstaben (a-z)", "txt_generator_numbers": "Zahlen (0-9)", "txt_generator_special": "Sonderzeichen (!@#$%^&*)", "txt_generator_minimum": "Mindestanzahl", "txt_generator_avoid_ambiguous": "Verwechselbare Zeichen vermeiden", "txt_generator_words": "Anzahl der Wörter", "txt_generator_separator": "Worttrenner", "txt_generator_capitalize": "Großschreibung", "txt_generator_include_number": "Eine Zahl einfügen",
|
||||||
"txt_generated_value": "Erzeugter Wert", "txt_generator_character_count": "{count} Zeichen", "txt_generator_pin": "PIN-Code", "txt_generator_pin_description": "Erzeugt lokal eine numerische PIN mit kryptografisch sicheren Zufallswerten.", "txt_generator_username": "Benutzername", "txt_generator_plus_addressed_email": "E-Mail mit Plus-Adressierung", "txt_generator_catch_all_email": "Catch-all-E-Mail", "txt_generator_email": "E-Mail-Adresse", "txt_generator_domain": "Domäne",
|
|
||||||
"txt_generator_word_list": "Wortliste", "txt_generator_eff_word_list": "Lange EFF-Wortliste", "txt_generator_custom_word_list": "Eigene Wortliste", "txt_generator_custom_words": "Eigene Wörter", "txt_generator_custom_words_placeholder": "Mindestens zwei Wörter durch Leerzeichen, Kommas oder Zeilen trennen",
|
|
||||||
"txt_generator_long_word_username": "Langwort-Benutzername", "txt_generator_custom_word": "Eigenes Merkwort", "txt_generator_subdomain_email": "Subdomain-E-Mail", "txt_generator_ssh_key": "SSH-Schlüssel", "txt_generator_ssh_generating": "SSH-Schlüssel wird erzeugt…", "txt_generator_ssh_error": "Dieser Browser unterstützt keine SSH-Schlüsselerzeugung.", "txt_generator_copy_public_key": "Öffentlichen Schlüssel kopieren", "txt_generator_ssh_security_note": "Das Schlüsselpaar wird lokal erzeugt und nicht gesendet. Speichern Sie den privaten Schlüssel jetzt; NodeWarden behält ihn nicht.", "txt_generator_public_key": "Öffentlicher Schlüssel", "txt_generator_private_key": "Privater Schlüssel — geheim halten", "txt_generator_ssh_algorithm": "Algorithmus", "txt_generator_key_length": "Schlüssellänge", "txt_generator_ssh_comment": "Kommentar des öffentlichen Schlüssels", "txt_generator_ssh_rsa_description": "RSA ist breit kompatibel. Mindestens 2048 Bit verwenden; 4096 Bit werden empfohlen.", "txt_generator_ssh_ed25519_description": "Ed25519 ist schnell, kompakt und für moderne SSH-Clients empfohlen.",
|
|
||||||
"txt_generator_email_alias": "E-Mail-Alias", "txt_generator_email_type": "Alias-Typ", "txt_generator_email_required_hint": "E-Mail-Adresse oder Domain eingeben, um einen Alias zu erzeugen.", "txt_generator_email_description": "Ihre Eingabe bleibt auf diesem Gerät und wird nur zum Zusammensetzen des Alias verwendet.", "txt_generator_long_word_username_description": "Ein oder mehrere lange Wörter zu einem einprägsamen Benutzernamen kombinieren.",
|
|
||||||
"nav_backup_strategy": "Cloud-Backup",
|
"nav_backup_strategy": "Cloud-Backup",
|
||||||
"nav_import_export": "Import und Export",
|
"nav_import_export": "Import und Export",
|
||||||
"nav_group_data_backup": "Daten & Backup",
|
"nav_group_data_backup": "Daten & Backup",
|
||||||
|
|||||||
@@ -31,40 +31,6 @@ const en: Record<string, string> = {
|
|||||||
"txt_generator_separator": "Word separator",
|
"txt_generator_separator": "Word separator",
|
||||||
"txt_generator_capitalize": "Capitalize",
|
"txt_generator_capitalize": "Capitalize",
|
||||||
"txt_generator_include_number": "Include a number",
|
"txt_generator_include_number": "Include a number",
|
||||||
"txt_generated_value": "Generated value",
|
|
||||||
"txt_generator_character_count": "{count} characters",
|
|
||||||
"txt_generator_pin": "PIN",
|
|
||||||
"txt_generator_pin_description": "Generate a numeric PIN locally using cryptographically secure randomness.",
|
|
||||||
"txt_generator_username": "Username",
|
|
||||||
"txt_generator_plus_addressed_email": "Plus-addressed email",
|
|
||||||
"txt_generator_catch_all_email": "Catch-all email",
|
|
||||||
"txt_generator_email": "Email address",
|
|
||||||
"txt_generator_domain": "Domain",
|
|
||||||
"txt_generator_word_list": "Word list",
|
|
||||||
"txt_generator_eff_word_list": "EFF long word list",
|
|
||||||
"txt_generator_custom_word_list": "Custom word list",
|
|
||||||
"txt_generator_custom_words": "Custom words",
|
|
||||||
"txt_generator_custom_words_placeholder": "Enter at least two words separated by spaces, commas, or new lines",
|
|
||||||
"txt_generator_long_word_username": "Long-word username",
|
|
||||||
"txt_generator_custom_word": "Custom helper word",
|
|
||||||
"txt_generator_subdomain_email": "Subdomain email",
|
|
||||||
"txt_generator_ssh_key": "SSH key",
|
|
||||||
"txt_generator_ssh_generating": "Generating SSH key…",
|
|
||||||
"txt_generator_ssh_error": "SSH key generation is not supported by this browser.",
|
|
||||||
"txt_generator_copy_public_key": "Copy public key",
|
|
||||||
"txt_generator_ssh_security_note": "The key pair is generated locally and is never sent to the server. Save the private key now; NodeWarden does not retain it.",
|
|
||||||
"txt_generator_public_key": "Public key",
|
|
||||||
"txt_generator_private_key": "Private key — keep secret",
|
|
||||||
"txt_generator_ssh_algorithm": "Algorithm",
|
|
||||||
"txt_generator_key_length": "Key length",
|
|
||||||
"txt_generator_ssh_comment": "Public key comment",
|
|
||||||
"txt_generator_ssh_rsa_description": "RSA is widely compatible. Use at least 2048 bits; 4096 bits is recommended.",
|
|
||||||
"txt_generator_ssh_ed25519_description": "Ed25519 is fast, compact, and recommended for modern SSH clients.",
|
|
||||||
"txt_generator_email_alias": "Email alias",
|
|
||||||
"txt_generator_email_type": "Email alias type",
|
|
||||||
"txt_generator_email_required_hint": "Enter your email address or domain to generate an alias.",
|
|
||||||
"txt_generator_email_description": "Your address stays on this device and is only used to compose the alias.",
|
|
||||||
"txt_generator_long_word_username_description": "Combine one or more long words into a memorable username.",
|
|
||||||
"nav_backup_strategy": "Cloud Backup",
|
"nav_backup_strategy": "Cloud Backup",
|
||||||
"nav_import_export": "Import & Export",
|
"nav_import_export": "Import & Export",
|
||||||
"nav_group_data_backup": "Data & Backup",
|
"nav_group_data_backup": "Data & Backup",
|
||||||
|
|||||||
@@ -8,10 +8,6 @@ const es: Record<string, string> = {
|
|||||||
"nav_vault_items": "Bóveda",
|
"nav_vault_items": "Bóveda",
|
||||||
"nav_sends": "Envíos",
|
"nav_sends": "Envíos",
|
||||||
"nav_generator": "Generador", "txt_password_generator": "Generador de contraseñas", "txt_password_generator_description": "Crea una contraseña única y segura localmente en este dispositivo.", "txt_generator_type": "Tipo de generador", "txt_passphrase": "Frase de contraseña", "txt_generated_password": "Contraseña generada", "txt_password_strength": "Seguridad", "txt_password_strength_weak": "Débil", "txt_password_strength_fair": "Regular", "txt_password_strength_good": "Buena", "txt_password_strength_strong": "Fuerte", "txt_generator_security_note": "La generación se realiza localmente. Tu contraseña nunca se envía al servidor.", "txt_generator_length": "Longitud", "txt_generator_character_types": "Tipos de caracteres", "txt_generator_uppercase": "Mayúsculas (A-Z)", "txt_generator_lowercase": "Minúsculas (a-z)", "txt_generator_numbers": "Números (0-9)", "txt_generator_special": "Caracteres especiales (!@#$%^&*)", "txt_generator_minimum": "Mínimo", "txt_generator_avoid_ambiguous": "Evitar caracteres ambiguos", "txt_generator_words": "Número de palabras", "txt_generator_separator": "Separador de palabras", "txt_generator_capitalize": "Usar mayúsculas", "txt_generator_include_number": "Incluir un número",
|
"nav_generator": "Generador", "txt_password_generator": "Generador de contraseñas", "txt_password_generator_description": "Crea una contraseña única y segura localmente en este dispositivo.", "txt_generator_type": "Tipo de generador", "txt_passphrase": "Frase de contraseña", "txt_generated_password": "Contraseña generada", "txt_password_strength": "Seguridad", "txt_password_strength_weak": "Débil", "txt_password_strength_fair": "Regular", "txt_password_strength_good": "Buena", "txt_password_strength_strong": "Fuerte", "txt_generator_security_note": "La generación se realiza localmente. Tu contraseña nunca se envía al servidor.", "txt_generator_length": "Longitud", "txt_generator_character_types": "Tipos de caracteres", "txt_generator_uppercase": "Mayúsculas (A-Z)", "txt_generator_lowercase": "Minúsculas (a-z)", "txt_generator_numbers": "Números (0-9)", "txt_generator_special": "Caracteres especiales (!@#$%^&*)", "txt_generator_minimum": "Mínimo", "txt_generator_avoid_ambiguous": "Evitar caracteres ambiguos", "txt_generator_words": "Número de palabras", "txt_generator_separator": "Separador de palabras", "txt_generator_capitalize": "Usar mayúsculas", "txt_generator_include_number": "Incluir un número",
|
||||||
"txt_generated_value": "Valor generado", "txt_generator_character_count": "{count} caracteres", "txt_generator_pin": "PIN", "txt_generator_pin_description": "Genera localmente un PIN numérico con aleatoriedad criptográficamente segura.", "txt_generator_username": "Nombre de usuario", "txt_generator_plus_addressed_email": "Correo con direccionamiento plus", "txt_generator_catch_all_email": "Correo catch-all", "txt_generator_email": "Correo electrónico", "txt_generator_domain": "Dominio",
|
|
||||||
"txt_generator_word_list": "Lista de palabras", "txt_generator_eff_word_list": "Lista larga de EFF", "txt_generator_custom_word_list": "Lista personalizada", "txt_generator_custom_words": "Palabras personalizadas", "txt_generator_custom_words_placeholder": "Introduce al menos dos palabras separadas por espacios, comas o líneas",
|
|
||||||
"txt_generator_long_word_username": "Usuario de palabras largas", "txt_generator_custom_word": "Palabra auxiliar personalizada", "txt_generator_subdomain_email": "Correo de subdominio", "txt_generator_ssh_key": "Clave SSH", "txt_generator_ssh_generating": "Generando clave SSH…", "txt_generator_ssh_error": "Este navegador no admite la generación de claves SSH.", "txt_generator_copy_public_key": "Copiar clave pública", "txt_generator_ssh_security_note": "El par se genera localmente y no se envía. Guarda ahora la clave privada; NodeWarden no la conserva.", "txt_generator_public_key": "Clave pública", "txt_generator_private_key": "Clave privada — mantenla en secreto", "txt_generator_ssh_algorithm": "Algoritmo", "txt_generator_key_length": "Longitud de clave", "txt_generator_ssh_comment": "Comentario de clave pública", "txt_generator_ssh_rsa_description": "RSA es ampliamente compatible. Usa al menos 2048 bits; se recomiendan 4096.", "txt_generator_ssh_ed25519_description": "Ed25519 es rápido, compacto y recomendado para clientes SSH modernos.",
|
|
||||||
"txt_generator_email_alias": "Alias de correo", "txt_generator_email_type": "Tipo de alias", "txt_generator_email_required_hint": "Introduce tu correo o dominio para generar un alias.", "txt_generator_email_description": "Los datos permanecen en este dispositivo y solo se usan para formar el alias.", "txt_generator_long_word_username_description": "Combina una o más palabras largas en un nombre de usuario fácil de recordar.",
|
|
||||||
"nav_backup_strategy": "Copia de seguridad en la nube",
|
"nav_backup_strategy": "Copia de seguridad en la nube",
|
||||||
"nav_import_export": "Importar y exportar",
|
"nav_import_export": "Importar y exportar",
|
||||||
"nav_group_data_backup": "Datos y copias",
|
"nav_group_data_backup": "Datos y copias",
|
||||||
|
|||||||
@@ -8,10 +8,6 @@ const fi: Record<string, string> = {
|
|||||||
"nav_vault_items": "Holvi",
|
"nav_vault_items": "Holvi",
|
||||||
"nav_sends": "Lähetykset",
|
"nav_sends": "Lähetykset",
|
||||||
"nav_generator": "Luoja", "txt_password_generator": "Salasanageneraattori", "txt_password_generator_description": "Luo vahva ja yksilöllinen salasana paikallisesti tällä laitteella.", "txt_generator_type": "Generaattorin tyyppi", "txt_passphrase": "Salalause", "txt_generated_password": "Luotu salasana", "txt_password_strength": "Vahvuus", "txt_password_strength_weak": "Heikko", "txt_password_strength_fair": "Kohtalainen", "txt_password_strength_good": "Hyvä", "txt_password_strength_strong": "Vahva", "txt_generator_security_note": "Generointi tapahtuu paikallisesti. Salasanaa ei koskaan lähetetä palvelimelle.", "txt_generator_length": "Pituus", "txt_generator_character_types": "Merkkityypit", "txt_generator_uppercase": "Isot kirjaimet (A-Z)", "txt_generator_lowercase": "Pienet kirjaimet (a-z)", "txt_generator_numbers": "Numerot (0-9)", "txt_generator_special": "Erikoismerkit (!@#$%^&*)", "txt_generator_minimum": "Vähintään", "txt_generator_avoid_ambiguous": "Vältä epäselviä merkkejä", "txt_generator_words": "Sanojen määrä", "txt_generator_separator": "Sanaerotin", "txt_generator_capitalize": "Iso alkukirjain", "txt_generator_include_number": "Sisällytä numero",
|
"nav_generator": "Luoja", "txt_password_generator": "Salasanageneraattori", "txt_password_generator_description": "Luo vahva ja yksilöllinen salasana paikallisesti tällä laitteella.", "txt_generator_type": "Generaattorin tyyppi", "txt_passphrase": "Salalause", "txt_generated_password": "Luotu salasana", "txt_password_strength": "Vahvuus", "txt_password_strength_weak": "Heikko", "txt_password_strength_fair": "Kohtalainen", "txt_password_strength_good": "Hyvä", "txt_password_strength_strong": "Vahva", "txt_generator_security_note": "Generointi tapahtuu paikallisesti. Salasanaa ei koskaan lähetetä palvelimelle.", "txt_generator_length": "Pituus", "txt_generator_character_types": "Merkkityypit", "txt_generator_uppercase": "Isot kirjaimet (A-Z)", "txt_generator_lowercase": "Pienet kirjaimet (a-z)", "txt_generator_numbers": "Numerot (0-9)", "txt_generator_special": "Erikoismerkit (!@#$%^&*)", "txt_generator_minimum": "Vähintään", "txt_generator_avoid_ambiguous": "Vältä epäselviä merkkejä", "txt_generator_words": "Sanojen määrä", "txt_generator_separator": "Sanaerotin", "txt_generator_capitalize": "Iso alkukirjain", "txt_generator_include_number": "Sisällytä numero",
|
||||||
"txt_generated_value": "Luotu arvo", "txt_generator_character_count": "{count} merkkiä", "txt_generator_pin": "PIN", "txt_generator_pin_description": "Luo numeerisen PIN-koodin paikallisesti kryptografisesti turvallisella satunnaisuudella.", "txt_generator_username": "Käyttäjänimi", "txt_generator_plus_addressed_email": "Plus-osoitteinen sähköposti", "txt_generator_catch_all_email": "Catch-all-sähköposti", "txt_generator_email": "Sähköpostiosoite", "txt_generator_domain": "Verkkotunnus",
|
|
||||||
"txt_generator_word_list": "Sanalista", "txt_generator_eff_word_list": "Pitkä EFF-sanalista", "txt_generator_custom_word_list": "Oma sanalista", "txt_generator_custom_words": "Omat sanat", "txt_generator_custom_words_placeholder": "Anna vähintään kaksi sanaa välilyönnein, pilkuin tai riveittäin",
|
|
||||||
"txt_generator_long_word_username": "Pitkäsanainen käyttäjänimi", "txt_generator_custom_word": "Oma muistisanaa", "txt_generator_subdomain_email": "Aliverkkotunnuksen sähköposti", "txt_generator_ssh_key": "SSH-avain", "txt_generator_ssh_generating": "Luodaan SSH-avainta…", "txt_generator_ssh_error": "Selain ei tue SSH-avaimen luontia.", "txt_generator_copy_public_key": "Kopioi julkinen avain", "txt_generator_ssh_security_note": "Avainpari luodaan paikallisesti eikä sitä lähetetä. Tallenna yksityinen avain nyt; NodeWarden ei säilytä sitä.", "txt_generator_public_key": "Julkinen avain", "txt_generator_private_key": "Yksityinen avain — pidä salassa", "txt_generator_ssh_algorithm": "Algoritmi", "txt_generator_key_length": "Avaimen pituus", "txt_generator_ssh_comment": "Julkisen avaimen kommentti", "txt_generator_ssh_rsa_description": "RSA on laajasti yhteensopiva. Käytä vähintään 2048 bittiä; 4096 on suositus.", "txt_generator_ssh_ed25519_description": "Ed25519 on nopea ja kompakti sekä suositeltu moderneille SSH-asiakkaille.",
|
|
||||||
"txt_generator_email_alias": "Sähköpostialias", "txt_generator_email_type": "Aliaksen tyyppi", "txt_generator_email_required_hint": "Anna sähköpostiosoite tai verkkotunnus aliaksen luomiseksi.", "txt_generator_email_description": "Syöte pysyy tällä laitteella ja sitä käytetään vain aliaksen muodostamiseen.", "txt_generator_long_word_username_description": "Yhdistä yksi tai useampi pitkä sana helposti muistettavaksi käyttäjänimeksi.",
|
|
||||||
"nav_backup_strategy": "Pilvivarmuuskopiointi",
|
"nav_backup_strategy": "Pilvivarmuuskopiointi",
|
||||||
"nav_import_export": "Tuonti ja Vienti",
|
"nav_import_export": "Tuonti ja Vienti",
|
||||||
"nav_group_data_backup": "Data & Varmuuskopiointi",
|
"nav_group_data_backup": "Data & Varmuuskopiointi",
|
||||||
|
|||||||
@@ -8,10 +8,6 @@ const fr: Record<string, string> = {
|
|||||||
"nav_vault_items": "Coffre-fort",
|
"nav_vault_items": "Coffre-fort",
|
||||||
"nav_sends": "Envois",
|
"nav_sends": "Envois",
|
||||||
"nav_generator": "Générateur", "txt_password_generator": "Générateur de mots de passe", "txt_password_generator_description": "Créez un mot de passe fort et unique localement sur cet appareil.", "txt_generator_type": "Type de générateur", "txt_passphrase": "Phrase secrète", "txt_generated_password": "Mot de passe généré", "txt_password_strength": "Robustesse", "txt_password_strength_weak": "Faible", "txt_password_strength_fair": "Correcte", "txt_password_strength_good": "Bonne", "txt_password_strength_strong": "Forte", "txt_generator_security_note": "La génération est locale. Votre mot de passe n'est jamais envoyé au serveur.", "txt_generator_length": "Longueur", "txt_generator_character_types": "Types de caractères", "txt_generator_uppercase": "Majuscules (A-Z)", "txt_generator_lowercase": "Minuscules (a-z)", "txt_generator_numbers": "Chiffres (0-9)", "txt_generator_special": "Caractères spéciaux (!@#$%^&*)", "txt_generator_minimum": "Minimum", "txt_generator_avoid_ambiguous": "Éviter les caractères ambigus", "txt_generator_words": "Nombre de mots", "txt_generator_separator": "Séparateur de mots", "txt_generator_capitalize": "Mettre une majuscule", "txt_generator_include_number": "Inclure un chiffre",
|
"nav_generator": "Générateur", "txt_password_generator": "Générateur de mots de passe", "txt_password_generator_description": "Créez un mot de passe fort et unique localement sur cet appareil.", "txt_generator_type": "Type de générateur", "txt_passphrase": "Phrase secrète", "txt_generated_password": "Mot de passe généré", "txt_password_strength": "Robustesse", "txt_password_strength_weak": "Faible", "txt_password_strength_fair": "Correcte", "txt_password_strength_good": "Bonne", "txt_password_strength_strong": "Forte", "txt_generator_security_note": "La génération est locale. Votre mot de passe n'est jamais envoyé au serveur.", "txt_generator_length": "Longueur", "txt_generator_character_types": "Types de caractères", "txt_generator_uppercase": "Majuscules (A-Z)", "txt_generator_lowercase": "Minuscules (a-z)", "txt_generator_numbers": "Chiffres (0-9)", "txt_generator_special": "Caractères spéciaux (!@#$%^&*)", "txt_generator_minimum": "Minimum", "txt_generator_avoid_ambiguous": "Éviter les caractères ambigus", "txt_generator_words": "Nombre de mots", "txt_generator_separator": "Séparateur de mots", "txt_generator_capitalize": "Mettre une majuscule", "txt_generator_include_number": "Inclure un chiffre",
|
||||||
"txt_generated_value": "Valeur générée", "txt_generator_character_count": "{count} caractères", "txt_generator_pin": "PIN", "txt_generator_pin_description": "Génère localement un PIN numérique avec un aléa cryptographiquement sûr.", "txt_generator_username": "Nom d’utilisateur", "txt_generator_plus_addressed_email": "Adresse e-mail avec plus", "txt_generator_catch_all_email": "Adresse e-mail catch-all", "txt_generator_email": "Adresse e-mail", "txt_generator_domain": "Domaine",
|
|
||||||
"txt_generator_word_list": "Liste de mots", "txt_generator_eff_word_list": "Liste longue EFF", "txt_generator_custom_word_list": "Liste personnalisée", "txt_generator_custom_words": "Mots personnalisés", "txt_generator_custom_words_placeholder": "Saisissez au moins deux mots séparés par des espaces, virgules ou lignes",
|
|
||||||
"txt_generator_long_word_username": "Identifiant à mots longs", "txt_generator_custom_word": "Mot aide-mémoire personnalisé", "txt_generator_subdomain_email": "E-mail de sous-domaine", "txt_generator_ssh_key": "Clé SSH", "txt_generator_ssh_generating": "Génération de la clé SSH…", "txt_generator_ssh_error": "Ce navigateur ne prend pas en charge la génération de clés SSH.", "txt_generator_copy_public_key": "Copier la clé publique", "txt_generator_ssh_security_note": "La paire est générée localement et jamais envoyée. Enregistrez la clé privée maintenant ; NodeWarden ne la conserve pas.", "txt_generator_public_key": "Clé publique", "txt_generator_private_key": "Clé privée — à garder secrète", "txt_generator_ssh_algorithm": "Algorithme", "txt_generator_key_length": "Longueur de clé", "txt_generator_ssh_comment": "Commentaire de clé publique", "txt_generator_ssh_rsa_description": "RSA est largement compatible. Utilisez au moins 2048 bits ; 4096 sont recommandés.", "txt_generator_ssh_ed25519_description": "Ed25519 est rapide, compact et recommandé pour les clients SSH modernes.",
|
|
||||||
"txt_generator_email_alias": "Alias e-mail", "txt_generator_email_type": "Type d’alias", "txt_generator_email_required_hint": "Saisissez votre adresse e-mail ou domaine pour générer un alias.", "txt_generator_email_description": "La saisie reste sur cet appareil et sert uniquement à composer l’alias.", "txt_generator_long_word_username_description": "Combinez un ou plusieurs mots longs en un identifiant mémorable.",
|
|
||||||
"nav_backup_strategy": "Sauvegarde Cloud",
|
"nav_backup_strategy": "Sauvegarde Cloud",
|
||||||
"nav_import_export": "Importer & Exporter",
|
"nav_import_export": "Importer & Exporter",
|
||||||
"nav_group_data_backup": "Données & Sauvegarde",
|
"nav_group_data_backup": "Données & Sauvegarde",
|
||||||
|
|||||||
@@ -8,10 +8,6 @@ const it: Record<string, string> = {
|
|||||||
"nav_vault_items": "Cassaforte",
|
"nav_vault_items": "Cassaforte",
|
||||||
"nav_sends": "Invii",
|
"nav_sends": "Invii",
|
||||||
"nav_generator": "Generatore", "txt_password_generator": "Generatore di password", "txt_password_generator_description": "Crea una password forte e univoca localmente su questo dispositivo.", "txt_generator_type": "Tipo di generatore", "txt_passphrase": "Frase segreta", "txt_generated_password": "Password generata", "txt_password_strength": "Robustezza", "txt_password_strength_weak": "Debole", "txt_password_strength_fair": "Discreta", "txt_password_strength_good": "Buona", "txt_password_strength_strong": "Forte", "txt_generator_security_note": "La generazione avviene localmente. La password non viene mai inviata al server.", "txt_generator_length": "Lunghezza", "txt_generator_character_types": "Tipi di caratteri", "txt_generator_uppercase": "Maiuscole (A-Z)", "txt_generator_lowercase": "Minuscole (a-z)", "txt_generator_numbers": "Numeri (0-9)", "txt_generator_special": "Caratteri speciali (!@#$%^&*)", "txt_generator_minimum": "Minimo", "txt_generator_avoid_ambiguous": "Evita caratteri ambigui", "txt_generator_words": "Numero di parole", "txt_generator_separator": "Separatore di parole", "txt_generator_capitalize": "Iniziale maiuscola", "txt_generator_include_number": "Includi un numero",
|
"nav_generator": "Generatore", "txt_password_generator": "Generatore di password", "txt_password_generator_description": "Crea una password forte e univoca localmente su questo dispositivo.", "txt_generator_type": "Tipo di generatore", "txt_passphrase": "Frase segreta", "txt_generated_password": "Password generata", "txt_password_strength": "Robustezza", "txt_password_strength_weak": "Debole", "txt_password_strength_fair": "Discreta", "txt_password_strength_good": "Buona", "txt_password_strength_strong": "Forte", "txt_generator_security_note": "La generazione avviene localmente. La password non viene mai inviata al server.", "txt_generator_length": "Lunghezza", "txt_generator_character_types": "Tipi di caratteri", "txt_generator_uppercase": "Maiuscole (A-Z)", "txt_generator_lowercase": "Minuscole (a-z)", "txt_generator_numbers": "Numeri (0-9)", "txt_generator_special": "Caratteri speciali (!@#$%^&*)", "txt_generator_minimum": "Minimo", "txt_generator_avoid_ambiguous": "Evita caratteri ambigui", "txt_generator_words": "Numero di parole", "txt_generator_separator": "Separatore di parole", "txt_generator_capitalize": "Iniziale maiuscola", "txt_generator_include_number": "Includi un numero",
|
||||||
"txt_generated_value": "Valore generato", "txt_generator_character_count": "{count} caratteri", "txt_generator_pin": "Codice PIN", "txt_generator_pin_description": "Genera localmente un PIN numerico con casualità crittograficamente sicura.", "txt_generator_username": "Nome utente", "txt_generator_plus_addressed_email": "Email con indirizzamento plus", "txt_generator_catch_all_email": "Email catch-all", "txt_generator_email": "Indirizzo email", "txt_generator_domain": "Dominio",
|
|
||||||
"txt_generator_word_list": "Elenco parole", "txt_generator_eff_word_list": "Elenco lungo EFF", "txt_generator_custom_word_list": "Elenco personalizzato", "txt_generator_custom_words": "Parole personalizzate", "txt_generator_custom_words_placeholder": "Inserisci almeno due parole separate da spazi, virgole o righe",
|
|
||||||
"txt_generator_long_word_username": "Nome utente a parole lunghe", "txt_generator_custom_word": "Parola mnemonica personalizzata", "txt_generator_subdomain_email": "Email con sottodominio", "txt_generator_ssh_key": "Chiave SSH", "txt_generator_ssh_generating": "Generazione chiave SSH…", "txt_generator_ssh_error": "Questo browser non supporta la generazione di chiavi SSH.", "txt_generator_copy_public_key": "Copia chiave pubblica", "txt_generator_ssh_security_note": "La coppia viene generata localmente e mai inviata. Salva ora la chiave privata; NodeWarden non la conserva.", "txt_generator_public_key": "Chiave pubblica", "txt_generator_private_key": "Chiave privata — mantienila segreta", "txt_generator_ssh_algorithm": "Algoritmo", "txt_generator_key_length": "Lunghezza chiave", "txt_generator_ssh_comment": "Commento chiave pubblica", "txt_generator_ssh_rsa_description": "RSA è ampiamente compatibile. Usa almeno 2048 bit; 4096 sono consigliati.", "txt_generator_ssh_ed25519_description": "Ed25519 è veloce, compatto e consigliato per client SSH moderni.",
|
|
||||||
"txt_generator_email_alias": "Alias email", "txt_generator_email_type": "Tipo di alias", "txt_generator_email_required_hint": "Inserisci l’indirizzo email o il dominio per generare un alias.", "txt_generator_email_description": "I dati restano su questo dispositivo e servono solo a comporre l’alias.", "txt_generator_long_word_username_description": "Combina una o più parole lunghe in un nome utente facile da ricordare.",
|
|
||||||
"nav_backup_strategy": "Backup su Cloud",
|
"nav_backup_strategy": "Backup su Cloud",
|
||||||
"nav_import_export": "Importa ed Esporta",
|
"nav_import_export": "Importa ed Esporta",
|
||||||
"nav_group_data_backup": "Dati e Backup",
|
"nav_group_data_backup": "Dati e Backup",
|
||||||
|
|||||||
@@ -9,10 +9,6 @@ const ru: Record<string, string> = {
|
|||||||
"nav_vault_items": "Хранилище",
|
"nav_vault_items": "Хранилище",
|
||||||
"nav_sends": "Отправляет",
|
"nav_sends": "Отправляет",
|
||||||
"nav_generator": "Генератор", "txt_password_generator": "Генератор паролей", "txt_password_generator_description": "Создайте надежный уникальный пароль локально на этом устройстве.", "txt_generator_type": "Тип генератора", "txt_passphrase": "Парольная фраза", "txt_generated_password": "Созданный пароль", "txt_password_strength": "Надежность", "txt_password_strength_weak": "Слабый", "txt_password_strength_fair": "Средний", "txt_password_strength_good": "Хороший", "txt_password_strength_strong": "Надежный", "txt_generator_security_note": "Генерация выполняется локально. Пароль никогда не отправляется на сервер.", "txt_generator_length": "Длина", "txt_generator_character_types": "Типы символов", "txt_generator_uppercase": "Заглавные буквы (A-Z)", "txt_generator_lowercase": "Строчные буквы (a-z)", "txt_generator_numbers": "Цифры (0-9)", "txt_generator_special": "Специальные символы (!@#$%^&*)", "txt_generator_minimum": "Минимум", "txt_generator_avoid_ambiguous": "Исключить похожие символы", "txt_generator_words": "Количество слов", "txt_generator_separator": "Разделитель слов", "txt_generator_capitalize": "С заглавной буквы", "txt_generator_include_number": "Добавить число",
|
"nav_generator": "Генератор", "txt_password_generator": "Генератор паролей", "txt_password_generator_description": "Создайте надежный уникальный пароль локально на этом устройстве.", "txt_generator_type": "Тип генератора", "txt_passphrase": "Парольная фраза", "txt_generated_password": "Созданный пароль", "txt_password_strength": "Надежность", "txt_password_strength_weak": "Слабый", "txt_password_strength_fair": "Средний", "txt_password_strength_good": "Хороший", "txt_password_strength_strong": "Надежный", "txt_generator_security_note": "Генерация выполняется локально. Пароль никогда не отправляется на сервер.", "txt_generator_length": "Длина", "txt_generator_character_types": "Типы символов", "txt_generator_uppercase": "Заглавные буквы (A-Z)", "txt_generator_lowercase": "Строчные буквы (a-z)", "txt_generator_numbers": "Цифры (0-9)", "txt_generator_special": "Специальные символы (!@#$%^&*)", "txt_generator_minimum": "Минимум", "txt_generator_avoid_ambiguous": "Исключить похожие символы", "txt_generator_words": "Количество слов", "txt_generator_separator": "Разделитель слов", "txt_generator_capitalize": "С заглавной буквы", "txt_generator_include_number": "Добавить число",
|
||||||
"txt_generated_value": "Созданное значение", "txt_generator_character_count": "Символов: {count}", "txt_generator_pin": "PIN-код", "txt_generator_pin_description": "Создаёт числовой PIN локально с криптографически стойкой случайностью.", "txt_generator_username": "Имя пользователя", "txt_generator_plus_addressed_email": "Email с плюс-адресацией", "txt_generator_catch_all_email": "Catch-all email", "txt_generator_email": "Адрес электронной почты", "txt_generator_domain": "Домен",
|
|
||||||
"txt_generator_word_list": "Список слов", "txt_generator_eff_word_list": "Длинный список EFF", "txt_generator_custom_word_list": "Свой список слов", "txt_generator_custom_words": "Свои слова", "txt_generator_custom_words_placeholder": "Введите не менее двух слов через пробел, запятую или с новой строки",
|
|
||||||
"txt_generator_long_word_username": "Имя из длинных слов", "txt_generator_custom_word": "Своё мнемоническое слово", "txt_generator_subdomain_email": "Email с поддоменом", "txt_generator_ssh_key": "SSH-ключ", "txt_generator_ssh_generating": "Создание SSH-ключа…", "txt_generator_ssh_error": "Этот браузер не поддерживает создание SSH-ключей.", "txt_generator_copy_public_key": "Копировать открытый ключ", "txt_generator_ssh_security_note": "Пара создаётся локально и не отправляется. Сохраните закрытый ключ сейчас; NodeWarden его не хранит.", "txt_generator_public_key": "Открытый ключ", "txt_generator_private_key": "Закрытый ключ — храните в тайне", "txt_generator_ssh_algorithm": "Алгоритм", "txt_generator_key_length": "Длина ключа", "txt_generator_ssh_comment": "Комментарий открытого ключа", "txt_generator_ssh_rsa_description": "RSA широко совместим. Используйте минимум 2048 бит; рекомендуется 4096.", "txt_generator_ssh_ed25519_description": "Ed25519 быстр, компактен и рекомендуется для современных SSH-клиентов.",
|
|
||||||
"txt_generator_email_alias": "Почтовый псевдоним", "txt_generator_email_type": "Тип псевдонима", "txt_generator_email_required_hint": "Введите адрес электронной почты или домен, чтобы создать псевдоним.", "txt_generator_email_description": "Введённые данные остаются на устройстве и используются только для создания псевдонима.", "txt_generator_long_word_username_description": "Объедините одно или несколько длинных слов в запоминающееся имя пользователя.",
|
|
||||||
"nav_backup_strategy": "Облачное резервное копирование",
|
"nav_backup_strategy": "Облачное резервное копирование",
|
||||||
"nav_import_export": "Импорт и экспорт",
|
"nav_import_export": "Импорт и экспорт",
|
||||||
"nav_group_data_backup": "Данные и резервные копии",
|
"nav_group_data_backup": "Данные и резервные копии",
|
||||||
|
|||||||
@@ -8,10 +8,6 @@ const sv: Record<string, string> = {
|
|||||||
"nav_vault_items": "Valv",
|
"nav_vault_items": "Valv",
|
||||||
"nav_sends": "Skickat",
|
"nav_sends": "Skickat",
|
||||||
"nav_generator": "Generator", "txt_password_generator": "Lösenordsgenerator", "txt_password_generator_description": "Skapa ett starkt och unikt lösenord lokalt på den här enheten.", "txt_generator_type": "Generatortyp", "txt_passphrase": "Lösenfras", "txt_generated_password": "Genererat lösenord", "txt_password_strength": "Styrka", "txt_password_strength_weak": "Svagt", "txt_password_strength_fair": "Medel", "txt_password_strength_good": "Bra", "txt_password_strength_strong": "Starkt", "txt_generator_security_note": "Generering sker lokalt. Ditt lösenord skickas aldrig till servern.", "txt_generator_length": "Längd", "txt_generator_character_types": "Teckentyper", "txt_generator_uppercase": "Versaler (A-Z)", "txt_generator_lowercase": "Gemener (a-z)", "txt_generator_numbers": "Siffror (0-9)", "txt_generator_special": "Specialtecken (!@#$%^&*)", "txt_generator_minimum": "Minst", "txt_generator_avoid_ambiguous": "Undvik tvetydiga tecken", "txt_generator_words": "Antal ord", "txt_generator_separator": "Ordavgränsare", "txt_generator_capitalize": "Stor begynnelsebokstav", "txt_generator_include_number": "Inkludera en siffra",
|
"nav_generator": "Generator", "txt_password_generator": "Lösenordsgenerator", "txt_password_generator_description": "Skapa ett starkt och unikt lösenord lokalt på den här enheten.", "txt_generator_type": "Generatortyp", "txt_passphrase": "Lösenfras", "txt_generated_password": "Genererat lösenord", "txt_password_strength": "Styrka", "txt_password_strength_weak": "Svagt", "txt_password_strength_fair": "Medel", "txt_password_strength_good": "Bra", "txt_password_strength_strong": "Starkt", "txt_generator_security_note": "Generering sker lokalt. Ditt lösenord skickas aldrig till servern.", "txt_generator_length": "Längd", "txt_generator_character_types": "Teckentyper", "txt_generator_uppercase": "Versaler (A-Z)", "txt_generator_lowercase": "Gemener (a-z)", "txt_generator_numbers": "Siffror (0-9)", "txt_generator_special": "Specialtecken (!@#$%^&*)", "txt_generator_minimum": "Minst", "txt_generator_avoid_ambiguous": "Undvik tvetydiga tecken", "txt_generator_words": "Antal ord", "txt_generator_separator": "Ordavgränsare", "txt_generator_capitalize": "Stor begynnelsebokstav", "txt_generator_include_number": "Inkludera en siffra",
|
||||||
"txt_generated_value": "Genererat värde", "txt_generator_character_count": "{count} tecken", "txt_generator_pin": "PIN-kod", "txt_generator_pin_description": "Skapar en numerisk PIN lokalt med kryptografiskt säker slumpning.", "txt_generator_username": "Användarnamn", "txt_generator_plus_addressed_email": "Plusadresserad e-post", "txt_generator_catch_all_email": "Catch-all-e-post", "txt_generator_email": "E-postadress", "txt_generator_domain": "Domän",
|
|
||||||
"txt_generator_word_list": "Ordlista", "txt_generator_eff_word_list": "Lång EFF-ordlista", "txt_generator_custom_word_list": "Egen ordlista", "txt_generator_custom_words": "Egna ord", "txt_generator_custom_words_placeholder": "Ange minst två ord åtskilda med blanksteg, kommatecken eller rader",
|
|
||||||
"txt_generator_long_word_username": "Användarnamn med långa ord", "txt_generator_custom_word": "Eget minnesord", "txt_generator_subdomain_email": "E-post med underdomän", "txt_generator_ssh_key": "SSH-nyckel", "txt_generator_ssh_generating": "Skapar SSH-nyckel…", "txt_generator_ssh_error": "Webbläsaren stöder inte skapande av SSH-nycklar.", "txt_generator_copy_public_key": "Kopiera offentlig nyckel", "txt_generator_ssh_security_note": "Nyckelparet skapas lokalt och skickas aldrig. Spara den privata nyckeln nu; NodeWarden behåller den inte.", "txt_generator_public_key": "Offentlig nyckel", "txt_generator_private_key": "Privat nyckel — håll den hemlig", "txt_generator_ssh_algorithm": "Algoritm", "txt_generator_key_length": "Nyckellängd", "txt_generator_ssh_comment": "Kommentar för offentlig nyckel", "txt_generator_ssh_rsa_description": "RSA har bred kompatibilitet. Använd minst 2048 bitar; 4096 rekommenderas.", "txt_generator_ssh_ed25519_description": "Ed25519 är snabb, kompakt och rekommenderas för moderna SSH-klienter.",
|
|
||||||
"txt_generator_email_alias": "E-postalias", "txt_generator_email_type": "Aliastyp", "txt_generator_email_required_hint": "Ange din e-postadress eller domän för att skapa ett alias.", "txt_generator_email_description": "Inmatningen stannar på enheten och används endast för att skapa aliaset.", "txt_generator_long_word_username_description": "Kombinera ett eller flera långa ord till ett minnesvärt användarnamn.",
|
|
||||||
"nav_backup_strategy": "Molnsäkerhetskopiering",
|
"nav_backup_strategy": "Molnsäkerhetskopiering",
|
||||||
"nav_import_export": "Importera och Exportera",
|
"nav_import_export": "Importera och Exportera",
|
||||||
"nav_group_data_backup": "Data och Säkerhetskopiering",
|
"nav_group_data_backup": "Data och Säkerhetskopiering",
|
||||||
|
|||||||
@@ -11,10 +11,6 @@ const zhCN: Record<string, string> = {
|
|||||||
"txt_password_generator": "密码生成器",
|
"txt_password_generator": "密码生成器",
|
||||||
"txt_password_generator_description": "在此设备本地生成强且唯一的密码。",
|
"txt_password_generator_description": "在此设备本地生成强且唯一的密码。",
|
||||||
"txt_generator_type": "生成类型", "txt_passphrase": "密码短语", "txt_generated_password": "已生成密码", "txt_password_strength": "强度", "txt_password_strength_weak": "弱", "txt_password_strength_fair": "一般", "txt_password_strength_good": "良好", "txt_password_strength_strong": "强", "txt_generator_security_note": "生成过程仅在本地进行,密码不会发送到服务器。", "txt_generator_length": "长度", "txt_generator_character_types": "字符类型", "txt_generator_uppercase": "大写字母 (A-Z)", "txt_generator_lowercase": "小写字母 (a-z)", "txt_generator_numbers": "数字 (0-9)", "txt_generator_special": "特殊字符 (!@#$%^&*)", "txt_generator_minimum": "最少数量", "txt_generator_avoid_ambiguous": "避免易混淆字符", "txt_generator_words": "单词数量", "txt_generator_separator": "单词分隔符", "txt_generator_capitalize": "首字母大写", "txt_generator_include_number": "包含数字",
|
"txt_generator_type": "生成类型", "txt_passphrase": "密码短语", "txt_generated_password": "已生成密码", "txt_password_strength": "强度", "txt_password_strength_weak": "弱", "txt_password_strength_fair": "一般", "txt_password_strength_good": "良好", "txt_password_strength_strong": "强", "txt_generator_security_note": "生成过程仅在本地进行,密码不会发送到服务器。", "txt_generator_length": "长度", "txt_generator_character_types": "字符类型", "txt_generator_uppercase": "大写字母 (A-Z)", "txt_generator_lowercase": "小写字母 (a-z)", "txt_generator_numbers": "数字 (0-9)", "txt_generator_special": "特殊字符 (!@#$%^&*)", "txt_generator_minimum": "最少数量", "txt_generator_avoid_ambiguous": "避免易混淆字符", "txt_generator_words": "单词数量", "txt_generator_separator": "单词分隔符", "txt_generator_capitalize": "首字母大写", "txt_generator_include_number": "包含数字",
|
||||||
"txt_generated_value": "已生成内容", "txt_generator_character_count": "{count} 个字符", "txt_generator_pin": "PIN 码", "txt_generator_pin_description": "使用密码学安全随机数在本地生成纯数字 PIN 码。", "txt_generator_username": "用户名", "txt_generator_plus_addressed_email": "加号别名邮箱", "txt_generator_catch_all_email": "全域接收邮箱", "txt_generator_email": "邮箱地址", "txt_generator_domain": "域名",
|
|
||||||
"txt_generator_word_list": "词表", "txt_generator_eff_word_list": "EFF 长词表", "txt_generator_custom_word_list": "自定义词表", "txt_generator_custom_words": "自定义单词", "txt_generator_custom_words_placeholder": "输入至少两个单词,用空格、逗号或换行分隔",
|
|
||||||
"txt_generator_long_word_username": "长单词用户名", "txt_generator_custom_word": "自定义助记词", "txt_generator_subdomain_email": "子域电子邮件", "txt_generator_ssh_key": "SSH 密钥", "txt_generator_ssh_generating": "正在生成 SSH 密钥…", "txt_generator_ssh_error": "此浏览器不支持 SSH 密钥生成。", "txt_generator_copy_public_key": "复制公钥", "txt_generator_ssh_security_note": "密钥对仅在本地生成,不会发送到服务器。请立即保存私钥,NodeWarden 不会保留它。", "txt_generator_public_key": "公钥", "txt_generator_private_key": "私钥 — 请保密", "txt_generator_ssh_algorithm": "算法", "txt_generator_key_length": "密钥长度", "txt_generator_ssh_comment": "公钥注释", "txt_generator_ssh_rsa_description": "RSA 兼容性广泛。请至少使用 2048 位,推荐 4096 位。", "txt_generator_ssh_ed25519_description": "Ed25519 快速且紧凑,推荐用于现代 SSH 客户端。",
|
|
||||||
"txt_generator_email_alias": "邮箱别名", "txt_generator_email_type": "邮箱别名类型", "txt_generator_email_required_hint": "输入邮箱地址或域名后,即可生成别名。", "txt_generator_email_description": "输入内容只保留在此设备上,仅用于组合邮箱别名。", "txt_generator_long_word_username_description": "将一个或多个长单词组合成易记的用户名。",
|
|
||||||
"nav_backup_strategy": "云端备份",
|
"nav_backup_strategy": "云端备份",
|
||||||
"nav_import_export": "导入导出",
|
"nav_import_export": "导入导出",
|
||||||
"nav_group_data_backup": "数据与备份",
|
"nav_group_data_backup": "数据与备份",
|
||||||
|
|||||||
@@ -11,10 +11,6 @@ const zhTW: Record<string, string> = {
|
|||||||
"txt_password_generator": "密碼產生器",
|
"txt_password_generator": "密碼產生器",
|
||||||
"txt_password_generator_description": "在此裝置本機建立強而唯一的密碼。",
|
"txt_password_generator_description": "在此裝置本機建立強而唯一的密碼。",
|
||||||
"txt_generator_type": "產生類型", "txt_passphrase": "密碼片語", "txt_generated_password": "已產生密碼", "txt_password_strength": "強度", "txt_password_strength_weak": "弱", "txt_password_strength_fair": "普通", "txt_password_strength_good": "良好", "txt_password_strength_strong": "強", "txt_generator_security_note": "產生程序僅在本機進行,密碼不會傳送到伺服器。", "txt_generator_length": "長度", "txt_generator_character_types": "字元類型", "txt_generator_uppercase": "大寫字母 (A-Z)", "txt_generator_lowercase": "小寫字母 (a-z)", "txt_generator_numbers": "數字 (0-9)", "txt_generator_special": "特殊字元 (!@#$%^&*)", "txt_generator_minimum": "最少數量", "txt_generator_avoid_ambiguous": "避免易混淆字元", "txt_generator_words": "單字數量", "txt_generator_separator": "單字分隔符號", "txt_generator_capitalize": "首字母大寫", "txt_generator_include_number": "包含數字",
|
"txt_generator_type": "產生類型", "txt_passphrase": "密碼片語", "txt_generated_password": "已產生密碼", "txt_password_strength": "強度", "txt_password_strength_weak": "弱", "txt_password_strength_fair": "普通", "txt_password_strength_good": "良好", "txt_password_strength_strong": "強", "txt_generator_security_note": "產生程序僅在本機進行,密碼不會傳送到伺服器。", "txt_generator_length": "長度", "txt_generator_character_types": "字元類型", "txt_generator_uppercase": "大寫字母 (A-Z)", "txt_generator_lowercase": "小寫字母 (a-z)", "txt_generator_numbers": "數字 (0-9)", "txt_generator_special": "特殊字元 (!@#$%^&*)", "txt_generator_minimum": "最少數量", "txt_generator_avoid_ambiguous": "避免易混淆字元", "txt_generator_words": "單字數量", "txt_generator_separator": "單字分隔符號", "txt_generator_capitalize": "首字母大寫", "txt_generator_include_number": "包含數字",
|
||||||
"txt_generated_value": "已產生內容", "txt_generator_character_count": "{count} 個字元", "txt_generator_pin": "PIN 碼", "txt_generator_pin_description": "使用密碼學安全亂數在本機產生純數字 PIN 碼。", "txt_generator_username": "使用者名稱", "txt_generator_plus_addressed_email": "加號別名信箱", "txt_generator_catch_all_email": "全域接收信箱", "txt_generator_email": "電子郵件地址", "txt_generator_domain": "網域",
|
|
||||||
"txt_generator_word_list": "詞表", "txt_generator_eff_word_list": "EFF 長詞表", "txt_generator_custom_word_list": "自訂詞表", "txt_generator_custom_words": "自訂單字", "txt_generator_custom_words_placeholder": "輸入至少兩個單字,以空格、逗號或換行分隔",
|
|
||||||
"txt_generator_long_word_username": "長單字使用者名稱", "txt_generator_custom_word": "自訂助記詞", "txt_generator_subdomain_email": "子網域電子郵件", "txt_generator_ssh_key": "SSH 金鑰", "txt_generator_ssh_generating": "正在產生 SSH 金鑰…", "txt_generator_ssh_error": "此瀏覽器不支援 SSH 金鑰產生。", "txt_generator_copy_public_key": "複製公鑰", "txt_generator_ssh_security_note": "金鑰組只在本機產生,不會傳送到伺服器。請立即儲存私鑰,NodeWarden 不會保留它。", "txt_generator_public_key": "公鑰", "txt_generator_private_key": "私鑰 — 請保密", "txt_generator_ssh_algorithm": "演算法", "txt_generator_key_length": "金鑰長度", "txt_generator_ssh_comment": "公鑰註解", "txt_generator_ssh_rsa_description": "RSA 相容性廣泛。請至少使用 2048 位元,建議 4096 位元。", "txt_generator_ssh_ed25519_description": "Ed25519 快速且精簡,建議用於現代 SSH 用戶端。",
|
|
||||||
"txt_generator_email_alias": "信箱別名", "txt_generator_email_type": "信箱別名類型", "txt_generator_email_required_hint": "輸入電子郵件地址或網域後,即可產生別名。", "txt_generator_email_description": "輸入內容只保留在此裝置上,僅用於組合信箱別名。", "txt_generator_long_word_username_description": "將一個或多個長單字組合成容易記住的使用者名稱。",
|
|
||||||
"nav_backup_strategy": "雲端備份",
|
"nav_backup_strategy": "雲端備份",
|
||||||
"nav_import_export": "導入導出",
|
"nav_import_export": "導入導出",
|
||||||
"nav_group_data_backup": "資料與備份",
|
"nav_group_data_backup": "資料與備份",
|
||||||
|
|||||||
@@ -1,271 +0,0 @@
|
|||||||
import { EFFLongWordList } from '@/lib/eff-word-list';
|
|
||||||
|
|
||||||
export type GeneratorMode = 'password' | 'passphrase' | 'pin' | 'username' | 'email' | 'sshKey';
|
|
||||||
export type EmailMode = 'plusAddressed' | 'catchAll' | 'subdomain';
|
|
||||||
|
|
||||||
export interface PasswordOptions {
|
|
||||||
length: number;
|
|
||||||
uppercase: boolean;
|
|
||||||
lowercase: boolean;
|
|
||||||
numbers: boolean;
|
|
||||||
special: boolean;
|
|
||||||
minUppercase: number;
|
|
||||||
minLowercase: number;
|
|
||||||
minNumbers: number;
|
|
||||||
minSpecial: number;
|
|
||||||
avoidAmbiguous: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PassphraseOptions {
|
|
||||||
words: number;
|
|
||||||
separator: string;
|
|
||||||
capitalize: boolean;
|
|
||||||
includeNumber: boolean;
|
|
||||||
wordList: 'eff' | 'custom';
|
|
||||||
customWords: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PinOptions { length: number }
|
|
||||||
|
|
||||||
export interface UsernameOptions {
|
|
||||||
words: number;
|
|
||||||
delimiter: string;
|
|
||||||
capitalize: boolean;
|
|
||||||
includeNumber: boolean;
|
|
||||||
customWord: string;
|
|
||||||
wordList: 'eff' | 'custom';
|
|
||||||
customWords: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface EmailOptions {
|
|
||||||
type: EmailMode;
|
|
||||||
email: string;
|
|
||||||
domain: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SshKeyOptions {
|
|
||||||
type: 'ed25519' | 'rsa';
|
|
||||||
rsaLength: 2048 | 3072 | 4096;
|
|
||||||
comment: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface GeneratorSettings {
|
|
||||||
mode: GeneratorMode;
|
|
||||||
password: PasswordOptions;
|
|
||||||
passphrase: PassphraseOptions;
|
|
||||||
pin: PinOptions;
|
|
||||||
username: UsernameOptions;
|
|
||||||
email: EmailOptions;
|
|
||||||
sshKey: SshKeyOptions;
|
|
||||||
}
|
|
||||||
|
|
||||||
const UPPERCASE = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
|
||||||
const LOWERCASE = 'abcdefghijklmnopqrstuvwxyz';
|
|
||||||
const DIGITS = '0123456789';
|
|
||||||
const SPECIAL = '!@#$%^&*_-+=:;,.?~';
|
|
||||||
const AMBIGUOUS = new Set(['I', 'L', 'O', 'l', 'o', '0', '1', '|']);
|
|
||||||
|
|
||||||
export const defaultGeneratorSettings: GeneratorSettings = {
|
|
||||||
mode: 'password',
|
|
||||||
password: {
|
|
||||||
length: 16,
|
|
||||||
uppercase: true,
|
|
||||||
lowercase: true,
|
|
||||||
numbers: true,
|
|
||||||
special: false,
|
|
||||||
minUppercase: 1,
|
|
||||||
minLowercase: 1,
|
|
||||||
minNumbers: 1,
|
|
||||||
minSpecial: 1,
|
|
||||||
avoidAmbiguous: false,
|
|
||||||
},
|
|
||||||
passphrase: { words: 6, separator: '-', capitalize: false, includeNumber: false, wordList: 'eff', customWords: '' },
|
|
||||||
pin: { length: 6 },
|
|
||||||
username: { words: 2, delimiter: '', capitalize: true, includeNumber: true, customWord: '', wordList: 'eff', customWords: '' },
|
|
||||||
email: { type: 'plusAddressed', email: '', domain: '' },
|
|
||||||
sshKey: { type: 'ed25519', rsaLength: 4096, comment: '' },
|
|
||||||
};
|
|
||||||
|
|
||||||
export function clampInteger(value: unknown, minimum: number, maximum: number, fallback: number): number {
|
|
||||||
const parsed = Number(value);
|
|
||||||
return Number.isFinite(parsed) ? Math.min(maximum, Math.max(minimum, Math.round(parsed))) : fallback;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function normalizeGeneratorSettings(value: unknown): GeneratorSettings {
|
|
||||||
const stored = value && typeof value === 'object' ? value as Partial<GeneratorSettings> : {};
|
|
||||||
const password: Partial<PasswordOptions> = stored.password && typeof stored.password === 'object' ? stored.password : {};
|
|
||||||
const passphrase: Partial<PassphraseOptions> = stored.passphrase && typeof stored.passphrase === 'object' ? stored.passphrase : {};
|
|
||||||
const pin: Partial<PinOptions> = stored.pin && typeof stored.pin === 'object' ? stored.pin : {};
|
|
||||||
const username: Partial<UsernameOptions> = stored.username && typeof stored.username === 'object' ? stored.username : {};
|
|
||||||
const legacyUsername = stored.username && typeof stored.username === 'object' ? stored.username as Partial<UsernameOptions> & Partial<EmailOptions> : {};
|
|
||||||
const email: Partial<EmailOptions> = stored.email && typeof stored.email === 'object' ? stored.email : legacyUsername;
|
|
||||||
const sshKey: Partial<SshKeyOptions> = stored.sshKey && typeof stored.sshKey === 'object' ? stored.sshKey : {};
|
|
||||||
const modes: GeneratorMode[] = ['password', 'passphrase', 'pin', 'username', 'email', 'sshKey'];
|
|
||||||
const emailModes: EmailMode[] = ['plusAddressed', 'catchAll', 'subdomain'];
|
|
||||||
const rsaLengths: SshKeyOptions['rsaLength'][] = [2048, 3072, 4096];
|
|
||||||
const mode = stored.mode === 'username' && emailModes.includes(legacyUsername.type as EmailMode)
|
|
||||||
? 'email'
|
|
||||||
: modes.includes(stored.mode as GeneratorMode) ? stored.mode as GeneratorMode : defaultGeneratorSettings.mode;
|
|
||||||
|
|
||||||
return {
|
|
||||||
mode,
|
|
||||||
password: {
|
|
||||||
...defaultGeneratorSettings.password,
|
|
||||||
...password,
|
|
||||||
length: clampInteger(password.length, 5, 128, defaultGeneratorSettings.password.length),
|
|
||||||
minUppercase: clampInteger(password.minUppercase, 0, 9, defaultGeneratorSettings.password.minUppercase),
|
|
||||||
minLowercase: clampInteger(password.minLowercase, 0, 9, defaultGeneratorSettings.password.minLowercase),
|
|
||||||
minNumbers: clampInteger(password.minNumbers, 0, 9, defaultGeneratorSettings.password.minNumbers),
|
|
||||||
minSpecial: clampInteger(password.minSpecial, 0, 9, defaultGeneratorSettings.password.minSpecial),
|
|
||||||
},
|
|
||||||
passphrase: {
|
|
||||||
...defaultGeneratorSettings.passphrase,
|
|
||||||
...passphrase,
|
|
||||||
words: clampInteger(passphrase.words, 3, 20, defaultGeneratorSettings.passphrase.words),
|
|
||||||
separator: String(passphrase.separator ?? defaultGeneratorSettings.passphrase.separator).slice(0, 1),
|
|
||||||
wordList: passphrase.wordList === 'custom' ? 'custom' : 'eff',
|
|
||||||
customWords: String(passphrase.customWords ?? '').slice(0, 50_000),
|
|
||||||
},
|
|
||||||
pin: { length: clampInteger(pin.length, 3, 64, defaultGeneratorSettings.pin.length) },
|
|
||||||
username: {
|
|
||||||
...defaultGeneratorSettings.username,
|
|
||||||
...username,
|
|
||||||
words: clampInteger(username.words, 1, 10, defaultGeneratorSettings.username.words),
|
|
||||||
delimiter: String(username.delimiter ?? defaultGeneratorSettings.username.delimiter).slice(0, 8),
|
|
||||||
customWord: String(username.customWord ?? '').trim().slice(0, 128),
|
|
||||||
wordList: username.wordList === 'custom' ? 'custom' : 'eff',
|
|
||||||
customWords: String(username.customWords ?? '').slice(0, 50_000),
|
|
||||||
},
|
|
||||||
email: {
|
|
||||||
type: emailModes.includes(email.type as EmailMode) ? email.type as EmailMode : defaultGeneratorSettings.email.type,
|
|
||||||
email: String(email.email ?? '').trim().slice(0, 254),
|
|
||||||
domain: String(email.domain ?? '').trim().slice(0, 253),
|
|
||||||
},
|
|
||||||
sshKey: {
|
|
||||||
type: sshKey.type === 'rsa' ? 'rsa' : 'ed25519',
|
|
||||||
rsaLength: rsaLengths.includes(sshKey.rsaLength as SshKeyOptions['rsaLength']) ? sshKey.rsaLength as SshKeyOptions['rsaLength'] : defaultGeneratorSettings.sshKey.rsaLength,
|
|
||||||
comment: String(sshKey.comment ?? '').replace(/[\r\n]+/g, ' ').trim().slice(0, 256),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function randomIndex(length: number): number {
|
|
||||||
if (!Number.isSafeInteger(length) || length < 1) throw new RangeError('Random source must not be empty');
|
|
||||||
const range = 0x1_0000_0000;
|
|
||||||
const upperBound = Math.floor(range / length) * length;
|
|
||||||
const buffer = new Uint32Array(1);
|
|
||||||
do crypto.getRandomValues(buffer); while (buffer[0] >= upperBound);
|
|
||||||
return buffer[0] % length;
|
|
||||||
}
|
|
||||||
|
|
||||||
function pick(characters: string): string { return characters[randomIndex(characters.length)]; }
|
|
||||||
|
|
||||||
function shuffle(value: string[]): string[] {
|
|
||||||
for (let index = value.length - 1; index > 0; index -= 1) {
|
|
||||||
const next = randomIndex(index + 1);
|
|
||||||
[value[index], value[next]] = [value[next], value[index]];
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
function filtered(characters: string, avoidAmbiguous: boolean): string {
|
|
||||||
return avoidAmbiguous ? [...characters].filter((character) => !AMBIGUOUS.has(character)).join('') : characters;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function generatePassword(options: PasswordOptions): string {
|
|
||||||
const sets: Array<{ chars: string; minimum: number }> = [];
|
|
||||||
if (options.uppercase) sets.push({ chars: filtered(UPPERCASE, options.avoidAmbiguous), minimum: options.minUppercase });
|
|
||||||
if (options.lowercase) sets.push({ chars: filtered(LOWERCASE, options.avoidAmbiguous), minimum: options.minLowercase });
|
|
||||||
if (options.numbers) sets.push({ chars: filtered(DIGITS, options.avoidAmbiguous), minimum: options.minNumbers });
|
|
||||||
if (options.special) sets.push({ chars: SPECIAL, minimum: options.minSpecial });
|
|
||||||
if (!sets.length) sets.push({ chars: LOWERCASE, minimum: 1 });
|
|
||||||
|
|
||||||
const required = sets.reduce((total, set) => total + set.minimum, 0);
|
|
||||||
const length = Math.max(options.length, required, 5);
|
|
||||||
const allCharacters = sets.map((set) => set.chars).join('');
|
|
||||||
const result = sets.flatMap((set) => Array.from({ length: set.minimum }, () => pick(set.chars)));
|
|
||||||
while (result.length < length) result.push(pick(allCharacters));
|
|
||||||
return shuffle(result).join('');
|
|
||||||
}
|
|
||||||
|
|
||||||
export function generatePassphrase(options: PassphraseOptions): string {
|
|
||||||
const customWords = [...new Set(options.customWords.split(/[\s,;]+/).map((word) => word.trim()).filter(Boolean))];
|
|
||||||
const wordList = options.wordList === 'custom' && customWords.length >= 2 ? customWords : EFFLongWordList;
|
|
||||||
const words = Array.from({ length: options.words }, () => wordList[randomIndex(wordList.length)]);
|
|
||||||
if (options.capitalize) {
|
|
||||||
for (let index = 0; index < words.length; index += 1) words[index] = words[index][0].toUpperCase() + words[index].slice(1);
|
|
||||||
}
|
|
||||||
if (options.includeNumber) words[randomIndex(words.length)] += String(randomIndex(10));
|
|
||||||
return words.join(options.separator);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function generatePin(options: PinOptions): string {
|
|
||||||
return Array.from({ length: options.length }, () => pick(DIGITS)).join('');
|
|
||||||
}
|
|
||||||
|
|
||||||
function randomWord(options: Pick<UsernameOptions, 'words' | 'delimiter' | 'capitalize' | 'includeNumber' | 'customWord' | 'wordList' | 'customWords'>): string {
|
|
||||||
const customWords = [...new Set(options.customWords.split(/[\s,;]+/).map((word) => word.trim()).filter(Boolean))];
|
|
||||||
const wordList = options.wordList === 'custom' && customWords.length >= 2 ? customWords : EFFLongWordList;
|
|
||||||
const words = Array.from({ length: options.words }, () => wordList[randomIndex(wordList.length)]);
|
|
||||||
if (options.customWord) words[randomIndex(words.length)] = options.customWord;
|
|
||||||
if (options.capitalize) {
|
|
||||||
for (let index = 0; index < words.length; index += 1) words[index] = words[index][0].toUpperCase() + words[index].slice(1);
|
|
||||||
}
|
|
||||||
if (options.includeNumber) {
|
|
||||||
const digits = options.words === 1 ? 4 : options.words === 2 ? 3 : 2;
|
|
||||||
const minimum = 10 ** (digits - 1);
|
|
||||||
words[randomIndex(words.length)] += String(minimum + randomIndex(9 * minimum));
|
|
||||||
}
|
|
||||||
return words.join(options.delimiter);
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeDomain(value: string): string {
|
|
||||||
return value.trim().toLowerCase().replace(/^https?:\/\//, '').split('/')[0].replace(/^@/, '');
|
|
||||||
}
|
|
||||||
|
|
||||||
export function generateUsername(options: UsernameOptions): string {
|
|
||||||
return randomWord(options);
|
|
||||||
}
|
|
||||||
|
|
||||||
function validDomain(value: string): string | null {
|
|
||||||
const domain = normalizeDomain(value);
|
|
||||||
return /^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$/i.test(domain) ? domain : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function validEmail(value: string): { local: string; domain: string } | null {
|
|
||||||
const match = value.trim().match(/^([^\s@+]+)(?:\+[^\s@]*)?@([^\s@]+)$/);
|
|
||||||
const domain = match ? validDomain(match[2]) : null;
|
|
||||||
return match && domain ? { local: match[1], domain } : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function generateEmail(options: EmailOptions): string {
|
|
||||||
const suffix = randomWord({ ...defaultGeneratorSettings.username, words: 1, capitalize: false, includeNumber: true });
|
|
||||||
if (options.type === 'catchAll') {
|
|
||||||
const domain = validDomain(options.domain);
|
|
||||||
return domain ? `${suffix}@${domain}` : '';
|
|
||||||
}
|
|
||||||
const email = validEmail(options.email);
|
|
||||||
if (!email) return '';
|
|
||||||
return options.type === 'subdomain'
|
|
||||||
? `${email.local}@${suffix}.${email.domain}`
|
|
||||||
: `${email.local}+${suffix}@${email.domain}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function generateValue(settings: GeneratorSettings): string {
|
|
||||||
if (settings.mode === 'passphrase') return generatePassphrase(settings.passphrase);
|
|
||||||
if (settings.mode === 'pin') return generatePin(settings.pin);
|
|
||||||
if (settings.mode === 'username') return generateUsername(settings.username);
|
|
||||||
if (settings.mode === 'email') return generateEmail(settings.email);
|
|
||||||
if (settings.mode === 'sshKey') return '';
|
|
||||||
return generatePassword(settings.password);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function estimateStrength(mode: GeneratorMode, value: string, passphraseWordCount?: number): number {
|
|
||||||
if (mode === 'username' || mode === 'email' || mode === 'sshKey') return 0;
|
|
||||||
if (mode === 'pin') return value.length >= 10 ? 4 : value.length >= 8 ? 3 : value.length >= 6 ? 2 : 1;
|
|
||||||
if (mode === 'passphrase') {
|
|
||||||
const words = passphraseWordCount ?? value.split(/[-_. ]/).filter(Boolean).length;
|
|
||||||
return Math.min(4, Math.max(1, Math.floor(words / 2)));
|
|
||||||
}
|
|
||||||
return Math.min(4, Math.max(1, Math.floor(value.length / 5)));
|
|
||||||
}
|
|
||||||
@@ -1,141 +0,0 @@
|
|||||||
import type { SshKeyOptions } from '@/lib/password-generator';
|
|
||||||
|
|
||||||
export interface GeneratedSshKey {
|
|
||||||
type: 'ED25519' | 'RSA';
|
|
||||||
bits: number;
|
|
||||||
publicKey: string;
|
|
||||||
privateKey: string;
|
|
||||||
fingerprint: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const encoder = new TextEncoder();
|
|
||||||
|
|
||||||
function concat(...chunks: Uint8Array[]): Uint8Array {
|
|
||||||
const output = new Uint8Array(chunks.reduce((total, chunk) => total + chunk.length, 0));
|
|
||||||
let offset = 0;
|
|
||||||
for (const chunk of chunks) {
|
|
||||||
output.set(chunk, offset);
|
|
||||||
offset += chunk.length;
|
|
||||||
}
|
|
||||||
return output;
|
|
||||||
}
|
|
||||||
|
|
||||||
function uint32(value: number): Uint8Array {
|
|
||||||
const output = new Uint8Array(4);
|
|
||||||
new DataView(output.buffer).setUint32(0, value, false);
|
|
||||||
return output;
|
|
||||||
}
|
|
||||||
|
|
||||||
function sshString(value: string | Uint8Array): Uint8Array {
|
|
||||||
const bytes = typeof value === 'string' ? encoder.encode(value) : value;
|
|
||||||
return concat(uint32(bytes.length), bytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
function base64UrlBytes(value: string): Uint8Array {
|
|
||||||
const normalized = value.replace(/-/g, '+').replace(/_/g, '/');
|
|
||||||
const decoded = atob(normalized.padEnd(Math.ceil(normalized.length / 4) * 4, '='));
|
|
||||||
return Uint8Array.from(decoded, (character) => character.charCodeAt(0));
|
|
||||||
}
|
|
||||||
|
|
||||||
function base64(bytes: Uint8Array): string {
|
|
||||||
let binary = '';
|
|
||||||
for (let offset = 0; offset < bytes.length; offset += 0x8000) {
|
|
||||||
binary += String.fromCharCode(...bytes.subarray(offset, offset + 0x8000));
|
|
||||||
}
|
|
||||||
return btoa(binary);
|
|
||||||
}
|
|
||||||
|
|
||||||
function mpint(value: Uint8Array): Uint8Array {
|
|
||||||
let offset = 0;
|
|
||||||
while (offset < value.length - 1 && value[offset] === 0) offset += 1;
|
|
||||||
let bytes = value.subarray(offset);
|
|
||||||
if (bytes[0] & 0x80) bytes = concat(new Uint8Array([0]), bytes);
|
|
||||||
return sshString(bytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
function openSshPrivateKey(publicBlob: Uint8Array, fields: Uint8Array[]): string {
|
|
||||||
const check = new Uint32Array(1);
|
|
||||||
crypto.getRandomValues(check);
|
|
||||||
let privateBlock = concat(uint32(check[0]), uint32(check[0]), ...fields, sshString(''));
|
|
||||||
const paddingLength = 8 - (privateBlock.length % 8);
|
|
||||||
privateBlock = concat(privateBlock, Uint8Array.from({ length: paddingLength }, (_, index) => index + 1));
|
|
||||||
const envelope = concat(
|
|
||||||
encoder.encode('openssh-key-v1\0'),
|
|
||||||
sshString('none'),
|
|
||||||
sshString('none'),
|
|
||||||
sshString(new Uint8Array()),
|
|
||||||
uint32(1),
|
|
||||||
sshString(publicBlob),
|
|
||||||
sshString(privateBlock),
|
|
||||||
);
|
|
||||||
const body = base64(envelope).match(/.{1,70}/g)?.join('\n') || '';
|
|
||||||
return `-----BEGIN OPENSSH PRIVATE KEY-----\n${body}\n-----END OPENSSH PRIVATE KEY-----\n`;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fingerprint(publicBlob: Uint8Array): Promise<string> {
|
|
||||||
const digest = new Uint8Array(await crypto.subtle.digest('SHA-256', Uint8Array.from(publicBlob).buffer));
|
|
||||||
return `SHA256:${base64(digest).replace(/=+$/, '')}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function required(jwk: JsonWebKey, property: keyof JsonWebKey): Uint8Array {
|
|
||||||
const value = jwk[property];
|
|
||||||
if (typeof value !== 'string' || !value) throw new Error(`The generated key is missing ${String(property)}`);
|
|
||||||
return base64UrlBytes(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function generateEd25519(comment: string): Promise<GeneratedSshKey> {
|
|
||||||
const pair = await crypto.subtle.generateKey({ name: 'Ed25519' }, true, ['sign', 'verify']) as CryptoKeyPair;
|
|
||||||
const [privateJwk, publicJwk] = await Promise.all([
|
|
||||||
crypto.subtle.exportKey('jwk', pair.privateKey),
|
|
||||||
crypto.subtle.exportKey('jwk', pair.publicKey),
|
|
||||||
]);
|
|
||||||
const publicBytes = required(publicJwk, 'x');
|
|
||||||
const seed = required(privateJwk, 'd');
|
|
||||||
const publicBlob = concat(sshString('ssh-ed25519'), sshString(publicBytes));
|
|
||||||
const privateKey = openSshPrivateKey(publicBlob, [
|
|
||||||
sshString('ssh-ed25519'),
|
|
||||||
sshString(publicBytes),
|
|
||||||
sshString(concat(seed, publicBytes)),
|
|
||||||
]);
|
|
||||||
return {
|
|
||||||
type: 'ED25519',
|
|
||||||
bits: 256,
|
|
||||||
publicKey: `ssh-ed25519 ${base64(publicBlob)}${comment ? ` ${comment}` : ''}`,
|
|
||||||
privateKey,
|
|
||||||
fingerprint: await fingerprint(publicBlob),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async function generateRsa(length: SshKeyOptions['rsaLength'], comment: string): Promise<GeneratedSshKey> {
|
|
||||||
const pair = await crypto.subtle.generateKey({
|
|
||||||
name: 'RSASSA-PKCS1-v1_5',
|
|
||||||
modulusLength: length,
|
|
||||||
publicExponent: new Uint8Array([1, 0, 1]),
|
|
||||||
hash: 'SHA-256',
|
|
||||||
}, true, ['sign', 'verify']) as CryptoKeyPair;
|
|
||||||
const privateJwk = await crypto.subtle.exportKey('jwk', pair.privateKey);
|
|
||||||
const n = required(privateJwk, 'n');
|
|
||||||
const e = required(privateJwk, 'e');
|
|
||||||
const d = required(privateJwk, 'd');
|
|
||||||
const qi = required(privateJwk, 'qi');
|
|
||||||
const p = required(privateJwk, 'p');
|
|
||||||
const q = required(privateJwk, 'q');
|
|
||||||
const publicBlob = concat(sshString('ssh-rsa'), mpint(e), mpint(n));
|
|
||||||
const privateKey = openSshPrivateKey(publicBlob, [
|
|
||||||
sshString('ssh-rsa'),
|
|
||||||
mpint(n), mpint(e), mpint(d), mpint(qi), mpint(p), mpint(q),
|
|
||||||
]);
|
|
||||||
return {
|
|
||||||
type: 'RSA',
|
|
||||||
bits: length,
|
|
||||||
publicKey: `ssh-rsa ${base64(publicBlob)}${comment ? ` ${comment}` : ''}`,
|
|
||||||
privateKey,
|
|
||||||
fingerprint: await fingerprint(publicBlob),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function generateSshKey(options: SshKeyOptions): Promise<GeneratedSshKey> {
|
|
||||||
if (!crypto?.subtle) throw new Error('Web Crypto is unavailable');
|
|
||||||
const comment = options.comment.replace(/[\r\n]+/g, ' ').trim();
|
|
||||||
return options.type === 'rsa' ? generateRsa(options.rsaLength, comment) : generateEd25519(comment);
|
|
||||||
}
|
|
||||||
@@ -10,10 +10,8 @@
|
|||||||
.generator-options-card { grid-area: options; padding: 19px; }
|
.generator-options-card { grid-area: options; padding: 19px; }
|
||||||
.generator-options-card h2 { margin: 0 0 18px; font-size: 17px; }
|
.generator-options-card h2 { margin: 0 0 18px; font-size: 17px; }
|
||||||
.generator-value { display: block; min-height: 110px; margin: 18px 0 10px; padding: 18px; border: 1px solid color-mix(in srgb, var(--primary) 22%, var(--line)); border-radius: 16px; background: color-mix(in srgb, var(--primary) 5%, var(--panel)); color: var(--text); font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; font-size: clamp(19px, 2.2vw, 27px); font-weight: 700; line-height: 1.45; overflow-wrap: anywhere; user-select: all; }
|
.generator-value { display: block; min-height: 110px; margin: 18px 0 10px; padding: 18px; border: 1px solid color-mix(in srgb, var(--primary) 22%, var(--line)); border-radius: 16px; background: color-mix(in srgb, var(--primary) 5%, var(--panel)); color: var(--text); font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; font-size: clamp(19px, 2.2vw, 27px); font-weight: 700; line-height: 1.45; overflow-wrap: anywhere; user-select: all; }
|
||||||
.generator-value.empty { display: grid; place-items: center; color: var(--muted); font-family: inherit; font-size: 14px; font-weight: 500; text-align: center; user-select: none; }
|
.generator-strength-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; color: var(--muted-strong); font-size: 13px; font-weight: 700; }
|
||||||
.generator-mode-tabs { flex-wrap: wrap; }
|
.generator-strength-row > span, .generator-security-note { display: inline-flex; align-items: center; gap: 6px; }
|
||||||
.generator-meta-row { display: grid; grid-template-columns: minmax(90px, 1fr) auto auto; align-items: center; gap: 12px; color: var(--muted-strong); font-size: 13px; font-weight: 700; }
|
|
||||||
.generator-meta-row > span, .generator-security-note { display: inline-flex; align-items: center; gap: 6px; white-space: nowrap; }
|
|
||||||
.generator-strength { display: flex; flex: 1; gap: 4px; }
|
.generator-strength { display: flex; flex: 1; gap: 4px; }
|
||||||
.generator-strength span { height: 5px; flex: 1; border-radius: 999px; background: var(--line); }
|
.generator-strength span { height: 5px; flex: 1; border-radius: 999px; background: var(--line); }
|
||||||
.generator-strength span.active.level-1 { background: #e87171; }.generator-strength span.active.level-2 { background: #db9b38; }.generator-strength span.active.level-3 { background: #46936c; }.generator-strength span.active.level-4 { background: var(--primary); }
|
.generator-strength span.active.level-1 { background: #e87171; }.generator-strength span.active.level-2 { background: #db9b38; }.generator-strength span.active.level-3 { background: #46936c; }.generator-strength span.active.level-4 { background: var(--primary); }
|
||||||
@@ -22,11 +20,6 @@
|
|||||||
.generator-number-field { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: 14px; margin-bottom: 15px; color: var(--text); font-size: 14px; font-weight: 700; }.generator-number-field > label { min-width: 0; }.generator-stepper { display: grid; grid-template-columns: 38px 64px 38px; align-items: center; overflow: hidden; border: 1px solid var(--line); border-radius: 10px; background: var(--panel); }.generator-stepper button { display: grid; width: 38px; height: 40px; place-items: center; border: 0; background: transparent; color: var(--primary-strong); cursor: pointer; transition: background-color 160ms ease, color 160ms ease; }.generator-stepper button:hover:not(:disabled) { background: color-mix(in srgb, var(--primary) 10%, var(--panel)); }.generator-stepper button:active:not(:disabled) { background: color-mix(in srgb, var(--primary) 17%, var(--panel)); }.generator-stepper button:focus-visible { position: relative; z-index: 1; outline: 3px solid color-mix(in srgb, var(--primary) 35%, transparent); outline-offset: -3px; }.generator-stepper button:disabled { color: var(--muted); cursor: not-allowed; }.generator-stepper .input { width: 64px; height: 40px; min-width: 0; border: 0; border-radius: 0; padding: 0; background: transparent; text-align: center; font-variant-numeric: tabular-nums; }.generator-stepper .input:focus { box-shadow: inset 0 0 0 2px color-mix(in srgb, var(--primary) 36%, transparent); }.generator-number-field.compact { grid-template-columns: minmax(0, 1fr) auto; margin: -3px 0 2px 50px; color: var(--muted); font-size: 13px; }.generator-number-field.compact .generator-stepper { grid-template-columns: 32px 46px 32px; border-radius: 9px; }.generator-number-field.compact .generator-stepper button { width: 32px; height: 34px; }.generator-number-field.compact .generator-stepper .input { width: 46px; height: 34px; font-size: 13px; }
|
.generator-number-field { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: 14px; margin-bottom: 15px; color: var(--text); font-size: 14px; font-weight: 700; }.generator-number-field > label { min-width: 0; }.generator-stepper { display: grid; grid-template-columns: 38px 64px 38px; align-items: center; overflow: hidden; border: 1px solid var(--line); border-radius: 10px; background: var(--panel); }.generator-stepper button { display: grid; width: 38px; height: 40px; place-items: center; border: 0; background: transparent; color: var(--primary-strong); cursor: pointer; transition: background-color 160ms ease, color 160ms ease; }.generator-stepper button:hover:not(:disabled) { background: color-mix(in srgb, var(--primary) 10%, var(--panel)); }.generator-stepper button:active:not(:disabled) { background: color-mix(in srgb, var(--primary) 17%, var(--panel)); }.generator-stepper button:focus-visible { position: relative; z-index: 1; outline: 3px solid color-mix(in srgb, var(--primary) 35%, transparent); outline-offset: -3px; }.generator-stepper button:disabled { color: var(--muted); cursor: not-allowed; }.generator-stepper .input { width: 64px; height: 40px; min-width: 0; border: 0; border-radius: 0; padding: 0; background: transparent; text-align: center; font-variant-numeric: tabular-nums; }.generator-stepper .input:focus { box-shadow: inset 0 0 0 2px color-mix(in srgb, var(--primary) 36%, transparent); }.generator-number-field.compact { grid-template-columns: minmax(0, 1fr) auto; margin: -3px 0 2px 50px; color: var(--muted); font-size: 13px; }.generator-number-field.compact .generator-stepper { grid-template-columns: 32px 46px 32px; border-radius: 9px; }.generator-number-field.compact .generator-stepper button { width: 32px; height: 34px; }.generator-number-field.compact .generator-stepper .input { width: 46px; height: 34px; font-size: 13px; }
|
||||||
.generator-option-group { display: grid; gap: 9px; margin: 18px 0; padding: 0; border: 0; }.generator-option-group legend { margin-bottom: 10px; padding: 0; font-size: 14px; font-weight: 700; }
|
.generator-option-group { display: grid; gap: 9px; margin: 18px 0; padding: 0; border: 0; }.generator-option-group legend { margin-bottom: 10px; padding: 0; font-size: 14px; font-weight: 700; }
|
||||||
.generator-toggle { display: grid; grid-template-columns: 40px minmax(0, 1fr); align-items: center; gap: 10px; min-height: 32px; cursor: pointer; }.generator-toggle input { position: absolute; opacity: 0; }.generator-toggle > span { position: relative; width: 38px; height: 22px; border-radius: 999px; background: #cbd5e1; transition: background 180ms ease; }.generator-toggle > span::after { position: absolute; top: 3px; left: 3px; width: 16px; height: 16px; border-radius: 50%; background: #fff; box-shadow: 0 1px 3px rgba(15,23,42,.25); content: ''; transition: transform 180ms ease; }.generator-toggle input:checked + span { background: var(--primary); }.generator-toggle input:checked + span::after { transform: translateX(16px); }.generator-toggle input:focus-visible + span { outline: 3px solid color-mix(in srgb, var(--primary) 30%, transparent); outline-offset: 2px; }.generator-toggle strong { font-size: 14px; font-weight: 600; }
|
.generator-toggle { display: grid; grid-template-columns: 40px minmax(0, 1fr); align-items: center; gap: 10px; min-height: 32px; cursor: pointer; }.generator-toggle input { position: absolute; opacity: 0; }.generator-toggle > span { position: relative; width: 38px; height: 22px; border-radius: 999px; background: #cbd5e1; transition: background 180ms ease; }.generator-toggle > span::after { position: absolute; top: 3px; left: 3px; width: 16px; height: 16px; border-radius: 50%; background: #fff; box-shadow: 0 1px 3px rgba(15,23,42,.25); content: ''; transition: transform 180ms ease; }.generator-toggle input:checked + span { background: var(--primary); }.generator-toggle input:checked + span::after { transform: translateX(16px); }.generator-toggle input:focus-visible + span { outline: 3px solid color-mix(in srgb, var(--primary) 30%, transparent); outline-offset: 2px; }.generator-toggle strong { font-size: 14px; font-weight: 600; }
|
||||||
.generator-select-field, .generator-text-field { display: grid; gap: 8px; margin-bottom: 16px; color: var(--text); font-size: 14px; font-weight: 700; }.generator-select-field .input, .generator-text-field .input { width: 100%; }.generator-options-note { margin: 14px 0 0; color: var(--muted); font-size: 13px; line-height: 1.55; }
|
|
||||||
.generator-word-list-input { min-height: 118px; resize: vertical; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; font-size: 13px; line-height: 1.5; }
|
|
||||||
.generator-key-status { display: flex; min-height: 180px; margin: 18px 0 10px; align-items: center; justify-content: center; gap: 10px; border: 1px solid var(--line); border-radius: 16px; color: var(--muted-strong); }.generator-key-status.error { padding: 18px; color: var(--danger); text-align: center; }
|
|
||||||
.generator-spinner { animation: generator-spin 900ms linear infinite; }.generator-key-output { display: grid; gap: 12px; margin: 18px 0 10px; }.generator-key-summary, .generator-key-field, .generator-private-key { border: 1px solid var(--line); border-radius: 14px; background: color-mix(in srgb, var(--primary) 4%, var(--panel)); }.generator-key-summary { display: grid; gap: 6px; padding: 14px; }.generator-key-summary code { overflow-wrap: anywhere; color: var(--muted-strong); font-size: 12px; }.generator-key-field { display: grid; gap: 9px; padding: 14px; }.generator-key-field > span { font-size: 13px; font-weight: 700; }.generator-key-field > code, .generator-private-key > code { display: block; max-height: 150px; overflow: auto; white-space: pre-wrap; overflow-wrap: anywhere; font-size: 11px; line-height: 1.45; user-select: all; }.generator-key-field-actions { display: flex; flex-wrap: wrap; gap: 8px; }.generator-key-field-actions .btn { display: inline-flex; gap: 6px; }.generator-private-key { padding: 0 14px 14px; }.generator-private-key summary { padding: 14px 0; cursor: pointer; font-size: 13px; font-weight: 700; }.generator-private-key .generator-key-field-actions { margin-top: 10px; }
|
|
||||||
@keyframes generator-spin { to { transform: rotate(360deg); } }
|
|
||||||
.generator-inline-number { display: grid; grid-template-columns: minmax(0, 1fr) 72px; align-items: center; gap: 14px; margin: -3px 0 2px 50px; color: var(--muted); font-size: 13px; }.generator-inline-number .input { height: 34px; text-align: center; }
|
.generator-inline-number { display: grid; grid-template-columns: minmax(0, 1fr) 72px; align-items: center; gap: 14px; margin: -3px 0 2px 50px; color: var(--muted); font-size: 13px; }.generator-inline-number .input { height: 34px; text-align: center; }
|
||||||
@media (max-width: 760px) { .generator-page { width: 100%; padding: 0 0 18px; }.generator-layout { grid-template-columns: 1fr; grid-template-areas: 'output' 'options'; gap: 10px; }.generator-output-card, .generator-options-card { padding: 15px; border-radius: 16px; }.generator-value { min-height: 94px; margin: 14px 0 10px; padding: 14px; font-size: 19px; }.generator-meta-row { grid-template-columns: minmax(60px, 1fr) auto; }.generator-meta-row > span:last-child { grid-column: 1 / -1; }.generator-actions .btn { justify-content: center; padding-inline: 10px; }.generator-option-group { margin: 15px 0; }.generator-toggle { min-height: 44px; }.generator-number-field.compact { margin-left: 50px; }.generator-stepper { grid-template-columns: 40px 64px 40px; }.generator-stepper button { width: 40px; min-height: 44px; }.generator-stepper .input { height: 44px; }.generator-number-field.compact .generator-stepper { grid-template-columns: 36px 46px 36px; }.generator-number-field.compact .generator-stepper button { width: 36px; height: 40px; min-height: 40px; } }
|
@media (max-width: 760px) { .generator-page { width: 100%; padding: 0 0 18px; }.generator-layout { grid-template-columns: 1fr; grid-template-areas: 'output' 'options'; gap: 10px; }.generator-output-card, .generator-options-card { padding: 15px; border-radius: 16px; }.generator-value { min-height: 94px; margin: 14px 0 10px; padding: 14px; font-size: 19px; }.generator-actions .btn { justify-content: center; padding-inline: 10px; }.generator-option-group { margin: 15px 0; }.generator-toggle { min-height: 44px; }.generator-number-field.compact { margin-left: 50px; }.generator-stepper { grid-template-columns: 40px 64px 40px; }.generator-stepper button { width: 40px; min-height: 44px; }.generator-stepper .input { height: 44px; }.generator-number-field.compact .generator-stepper { grid-template-columns: 36px 46px 36px; }.generator-number-field.compact .generator-stepper button { width: 36px; height: 40px; min-height: 40px; } }
|
||||||
@media (prefers-reduced-motion: reduce) { .generator-toggle > span, .generator-toggle > span::after { transition: none; }.generator-spinner { animation: none; } }
|
@media (prefers-reduced-motion: reduce) { .generator-toggle > span, .generator-toggle > span::after { transition: none; } }
|
||||||
|
|||||||
+61
-22
@@ -258,12 +258,39 @@ function searchIndexPolicyPlugin(isDemo: boolean): Plugin {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resourcePriorityPlugin(isDemo: boolean): Plugin {
|
||||||
|
return {
|
||||||
|
name: 'nodewarden-resource-priority',
|
||||||
|
enforce: 'post' as const,
|
||||||
|
transformIndexHtml(html: string) {
|
||||||
|
if (isDemo || !html.includes('/assets/app-suite-')) return html;
|
||||||
|
|
||||||
|
const scriptMatch = html.match(/^\s*<script type="module" crossorigin src="\/assets\/index-[^"]+\.js"><\/script>\s*$/m);
|
||||||
|
const appSuiteMatch = html.match(/^\s*<link rel="modulepreload" crossorigin href="\/assets\/app-suite-[^"]+\.js">\s*$/m);
|
||||||
|
const stylesheetMatch = html.match(/^\s*<link rel="stylesheet" crossorigin href="\/assets\/index-[^"]+\.css">\s*$/m);
|
||||||
|
|
||||||
|
if (!scriptMatch || !appSuiteMatch || !stylesheetMatch) return html;
|
||||||
|
|
||||||
|
const prioritizedTags = [
|
||||||
|
stylesheetMatch[0].replace('rel="stylesheet"', 'rel="stylesheet" fetchpriority="high"'),
|
||||||
|
appSuiteMatch[0].replace('rel="modulepreload"', 'rel="modulepreload" fetchpriority="high"'),
|
||||||
|
scriptMatch[0].replace('type="module"', 'type="module" fetchpriority="high"'),
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
return html
|
||||||
|
.replace(scriptMatch[0], '')
|
||||||
|
.replace(appSuiteMatch[0], '')
|
||||||
|
.replace(stylesheetMatch[0], prioritizedTags);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export default defineConfig(({ mode }) => {
|
export default defineConfig(({ mode }) => {
|
||||||
const isDemo = mode === 'demo';
|
const isDemo = mode === 'demo';
|
||||||
|
|
||||||
return {
|
return {
|
||||||
root: rootDir,
|
root: rootDir,
|
||||||
plugins: [preact(), searchIndexPolicyPlugin(isDemo), pwaServiceWorkerPlugin(isDemo)],
|
plugins: [preact(), searchIndexPolicyPlugin(isDemo), resourcePriorityPlugin(isDemo), pwaServiceWorkerPlugin(isDemo)],
|
||||||
define: {
|
define: {
|
||||||
__NODEWARDEN_DEMO__: JSON.stringify(isDemo),
|
__NODEWARDEN_DEMO__: JSON.stringify(isDemo),
|
||||||
},
|
},
|
||||||
@@ -284,29 +311,41 @@ export default defineConfig(({ mode }) => {
|
|||||||
sourcemap: false,
|
sourcemap: false,
|
||||||
target: 'esnext',
|
target: 'esnext',
|
||||||
chunkSizeWarningLimit: 800,
|
chunkSizeWarningLimit: 800,
|
||||||
rolldownOptions: {
|
rollupOptions: {
|
||||||
checks: {
|
treeshake: {
|
||||||
pluginTimings: false,
|
preset: 'smallest',
|
||||||
},
|
},
|
||||||
output: {
|
output: {
|
||||||
codeSplitting: {
|
manualChunks(id) {
|
||||||
groups: [
|
const normalized = id.replace(/\\/g, '/');
|
||||||
{
|
|
||||||
name: 'shared',
|
const localeMatch = normalized.match(/\/src\/lib\/i18n\/locales\/(.+)\.ts$/);
|
||||||
minShareCount: 2,
|
if (localeMatch) {
|
||||||
minSize: 50 * 1024,
|
if (localeMatch[1] === 'en') return undefined;
|
||||||
priority: 10,
|
return `i18n-${localeMatch[1]}`;
|
||||||
},
|
}
|
||||||
{
|
|
||||||
name(id) {
|
if (
|
||||||
const normalized = id.replace(/\\/g, '/');
|
!isDemo &&
|
||||||
const localeMatch = normalized.match(/\/src\/lib\/i18n\/locales\/(.+)\.ts$/);
|
(
|
||||||
return localeMatch && localeMatch[1] !== 'en' ? `i18n-${localeMatch[1]}` : null;
|
normalized.includes('/src/components/VaultPage.tsx') ||
|
||||||
},
|
normalized.includes('/src/components/ImportPage.tsx') ||
|
||||||
test: /[\\/]src[\\/]lib[\\/]i18n[\\/]locales[\\/]/,
|
normalized.includes('/src/lib/import-') ||
|
||||||
priority: 20,
|
normalized.includes('/src/lib/export-formats.ts') ||
|
||||||
},
|
normalized.includes('/src/components/SendsPage.tsx') ||
|
||||||
],
|
normalized.includes('/src/components/TotpCodesPage.tsx') ||
|
||||||
|
normalized.includes('/src/components/DomainRulesPage.tsx') ||
|
||||||
|
normalized.includes('/src/components/BackupCenterPage.tsx') ||
|
||||||
|
normalized.includes('/src/components/backup-center/') ||
|
||||||
|
normalized.includes('/src/components/SettingsPage.tsx') ||
|
||||||
|
normalized.includes('/src/components/SecurityDevicesPage.tsx') ||
|
||||||
|
normalized.includes('/src/components/AdminPage.tsx')
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return 'app-suite';
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user