diff --git a/.gitignore b/.gitignore index 9cd77ce..f18e0e6 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,9 @@ tests/selfcheck.ts # Build output dist/ build/ +public/ +public2/ + # IDE .vscode/ diff --git a/package-lock.json b/package-lock.json index 816bc3e..82ea3ce 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,14 +8,369 @@ "name": "nodewarden", "version": "1.1.0", "license": "LGPL-3.0", + "dependencies": { + "@tanstack/react-query": "^5.90.21", + "preact": "^10.28.4", + "qrcode-generator": "^2.0.4", + "wouter": "^3.9.0" + }, "devDependencies": { "@cloudflare/workers-types": "^4.20260131.0", + "@preact/preset-vite": "^2.10.3", "@types/node": "^25.2.3", "tsx": "^4.21.0", "typescript": "^5.9.3", + "vite": "^7.3.1", "wrangler": "^4.61.1" } }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmmirror.com/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmmirror.com/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmmirror.com/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmmirror.com/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/generator/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmmirror.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.27.3", + "resolved": "https://registry.npmmirror.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", + "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmmirror.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmmirror.com/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmmirror.com/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmmirror.com/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmmirror.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmmirror.com/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.6", + "resolved": "https://registry.npmmirror.com/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.28.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz", + "integrity": "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx": { + "version": "7.28.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.28.6.tgz", + "integrity": "sha512-61bxqhiRfAACulXSLd/GxqmAedUSrRZIu/cbaT18T1CetkTmtDN15it7i80ru4DVqRK1WMxQhXs+Lf9kajm5Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/plugin-syntax-jsx": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-development": { + "version": "7.27.1", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.27.1.tgz", + "integrity": "sha512-ykDdF5yI4f1WrAolLqeF3hmYU12j9ntLQl/AOG1HAS21jxyg1Q0/J/tpREuYLfatGdGmXp/3yS0ZA76kOlVq9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-transform-react-jsx": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmmirror.com/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmmirror.com/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@cloudflare/kv-asset-handler": { "version": "0.4.2", "resolved": "https://registry.npmmirror.com/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.4.2.tgz", @@ -1091,6 +1446,50 @@ "url": "https://opencollective.com/libvips" } }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmmirror.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/gen-mapping/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmmirror.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmmirror.com/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmmirror.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", "resolved": "https://registry.npmmirror.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", @@ -1148,6 +1547,469 @@ "dev": true, "license": "MIT" }, + "node_modules/@preact/preset-vite": { + "version": "2.10.3", + "resolved": "https://registry.npmmirror.com/@preact/preset-vite/-/preset-vite-2.10.3.tgz", + "integrity": "sha512-1SiS+vFItpkNdBs7q585PSAIln0wBeBdcpJYbzPs1qipsb/FssnkUioNXuRsb8ZnU8YEQHr+3v8+/mzWSnTQmg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-transform-react-jsx": "^7.27.1", + "@babel/plugin-transform-react-jsx-development": "^7.27.1", + "@prefresh/vite": "^2.4.11", + "@rollup/pluginutils": "^5.0.0", + "babel-plugin-transform-hook-names": "^1.0.2", + "debug": "^4.4.3", + "picocolors": "^1.1.1", + "vite-prerender-plugin": "^0.5.8" + }, + "peerDependencies": { + "@babel/core": "7.x", + "vite": "2.x || 3.x || 4.x || 5.x || 6.x || 7.x" + } + }, + "node_modules/@prefresh/babel-plugin": { + "version": "0.5.3", + "resolved": "https://registry.npmmirror.com/@prefresh/babel-plugin/-/babel-plugin-0.5.3.tgz", + "integrity": "sha512-57LX2SHs4BX2s1IwCjNzTE2OJeEepRCNf1VTEpbNcUyHfMO68eeOWGDIt4ob9aYlW6PEWZ1SuwNikuoIXANDtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@prefresh/core": { + "version": "1.5.9", + "resolved": "https://registry.npmmirror.com/@prefresh/core/-/core-1.5.9.tgz", + "integrity": "sha512-IKBKCPaz34OFVC+adiQ2qaTF5qdztO2/4ZPf4KsRTgjKosWqxVXmEbxCiUydYZRY8GVie+DQlKzQr9gt6HQ+EQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "preact": "^10.0.0 || ^11.0.0-0" + } + }, + "node_modules/@prefresh/utils": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/@prefresh/utils/-/utils-1.2.1.tgz", + "integrity": "sha512-vq/sIuN5nYfYzvyayXI4C2QkprfNaHUQ9ZX+3xLD8nL3rWyzpxOm1+K7RtMbhd+66QcaISViK7amjnheQ/4WZw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@prefresh/vite": { + "version": "2.4.12", + "resolved": "https://registry.npmmirror.com/@prefresh/vite/-/vite-2.4.12.tgz", + "integrity": "sha512-FY1fzXpUjiuosznMV0YM7XAOPZjB5FIdWS0W24+XnlxYkt9hNAwwsiKYn+cuTEoMtD/ZVazS5QVssBr9YhpCQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.22.1", + "@prefresh/babel-plugin": "^0.5.2", + "@prefresh/core": "^1.5.0", + "@prefresh/utils": "^1.2.0", + "@rollup/pluginutils": "^4.2.1" + }, + "peerDependencies": { + "preact": "^10.4.0 || ^11.0.0-0", + "vite": ">=2.0.0" + } + }, + "node_modules/@prefresh/vite/node_modules/@rollup/pluginutils": { + "version": "4.2.1", + "resolved": "https://registry.npmmirror.com/@rollup/pluginutils/-/pluginutils-4.2.1.tgz", + "integrity": "sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "estree-walker": "^2.0.1", + "picomatch": "^2.2.2" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/@prefresh/vite/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@rollup/pluginutils": { + "version": "5.3.0", + "resolved": "https://registry.npmmirror.com/@rollup/pluginutils/-/pluginutils-5.3.0.tgz", + "integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", + "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", + "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", + "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", + "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", + "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", + "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", + "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", + "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", + "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", + "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", + "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", + "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", + "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", + "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", + "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", + "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", + "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", + "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", + "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", + "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", + "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", + "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@sindresorhus/is": { "version": "7.2.0", "resolved": "https://registry.npmmirror.com/@sindresorhus/is/-/is-7.2.0.tgz", @@ -1168,16 +2030,73 @@ "dev": true, "license": "CC0-1.0" }, + "node_modules/@tanstack/query-core": { + "version": "5.90.20", + "resolved": "https://registry.npmmirror.com/@tanstack/query-core/-/query-core-5.90.20.tgz", + "integrity": "sha512-OMD2HLpNouXEfZJWcKeVKUgQ5n+n3A2JFmBaScpNDUqSrQSjiveC7dKMe53uJUg1nDG16ttFPz2xfilz6i2uVg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.90.21", + "resolved": "https://registry.npmmirror.com/@tanstack/react-query/-/react-query-5.90.21.tgz", + "integrity": "sha512-0Lu6y5t+tvlTJMTO7oh5NSpJfpg/5D41LlThfepTixPYkJ0sE2Jj0m0f6yYqujBwIXlId87e234+MxG3D3g7kg==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.90.20" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "25.2.3", "resolved": "https://registry.npmmirror.com/@types/node/-/node-25.2.3.tgz", "integrity": "sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~7.16.0" } }, + "node_modules/babel-plugin-transform-hook-names": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/babel-plugin-transform-hook-names/-/babel-plugin-transform-hook-names-1.0.2.tgz", + "integrity": "sha512-5gafyjyyBTTdX/tQQ0hRgu4AhNHG/hqWi0ZZmg2xvs2FgRkJXzDNKBZCyoYqgFkovfDrgM8OoKg8karoUvWeCw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@babel/core": "^7.12.10" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.0", + "resolved": "https://registry.npmmirror.com/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz", + "integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/blake3-wasm": { "version": "2.1.5", "resolved": "https://registry.npmmirror.com/blake3-wasm/-/blake3-wasm-2.1.5.tgz", @@ -1185,6 +2104,76 @@ "dev": true, "license": "MIT" }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true, + "license": "ISC" + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmmirror.com/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001774", + "resolved": "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001774.tgz", + "integrity": "sha512-DDdwPGz99nmIEv216hKSgLD+D4ikHQHjBC/seF98N9CPqRX4M5mSxT9eTV6oyisnJcuzxtZy4n17yKKQYmYQOA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, "node_modules/cookie": { "version": "1.1.1", "resolved": "https://registry.npmmirror.com/cookie/-/cookie-1.1.1.tgz", @@ -1199,6 +2188,54 @@ "url": "https://opencollective.com/express" } }, + "node_modules/css-select": { + "version": "5.2.2", + "resolved": "https://registry.npmmirror.com/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmmirror.com/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmmirror.com/detect-libc/-/detect-libc-2.1.2.tgz", @@ -1209,6 +2246,85 @@ "node": ">=8" } }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmmirror.com/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmmirror.com/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.302", + "resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.302.tgz", + "integrity": "sha512-sM6HAN2LyK82IyPBpznDRqlTQAtuSaO+ShzFiWTvoMJLHyZ+Y39r8VMfHzwbU8MVBzQ4Wdn85+wlZl2TLGIlwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmmirror.com/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/error-stack-parser-es": { "version": "1.0.5", "resolved": "https://registry.npmmirror.com/error-stack-parser-es/-/error-stack-parser-es-1.0.5.tgz", @@ -1261,6 +2377,41 @@ "@esbuild/win32-x64": "0.27.0" } }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", @@ -1276,6 +2427,16 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmmirror.com/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/get-tsconfig": { "version": "4.13.6", "resolved": "https://registry.npmmirror.com/get-tsconfig/-/get-tsconfig-4.13.6.tgz", @@ -1289,6 +2450,49 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmmirror.com/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/kleur": { "version": "4.1.5", "resolved": "https://registry.npmmirror.com/kleur/-/kleur-4.1.5.tgz", @@ -1299,6 +2503,33 @@ "node": ">=6" } }, + "node_modules/kolorist": { + "version": "1.8.0", + "resolved": "https://registry.npmmirror.com/kolorist/-/kolorist-1.8.0.tgz", + "integrity": "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, "node_modules/miniflare": { "version": "4.20260128.0", "resolved": "https://registry.npmmirror.com/miniflare/-/miniflare-4.20260128.0.tgz", @@ -1320,6 +2551,69 @@ "node": ">=18.0.0" } }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-html-parser": { + "version": "6.1.13", + "resolved": "https://registry.npmmirror.com/node-html-parser/-/node-html-parser-6.1.13.tgz", + "integrity": "sha512-qIsTMOY4C/dAa5Q5vsobRpOOvPfC4pB61UVW2uSwZNUp0QU/jCekTal1vMmbO0DgdHeLUJpv/ARmDqErVxA3Sg==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-select": "^5.1.0", + "he": "1.2.0" + } + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, "node_modules/path-to-regexp": { "version": "6.3.0", "resolved": "https://registry.npmmirror.com/path-to-regexp/-/path-to-regexp-6.3.0.tgz", @@ -1334,6 +2628,91 @@ "dev": true, "license": "MIT" }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/preact": { + "version": "10.28.4", + "resolved": "https://registry.npmmirror.com/preact/-/preact-10.28.4.tgz", + "integrity": "sha512-uKFfOHWuSNpRFVTnljsCluEFq57OKT+0QdOiQo8XWnQ/pSvg7OpX5eNOejELXJMWy+BwM2nobz0FkvzmnpCNsQ==", + "license": "MIT", + "peer": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, + "node_modules/qrcode-generator": { + "version": "2.0.4", + "resolved": "https://registry.npmmirror.com/qrcode-generator/-/qrcode-generator-2.0.4.tgz", + "integrity": "sha512-mZSiP6RnbHl4xL2Ap5HfkjLnmxfKcPWpWe/c+5XxCuetEenqmNFf1FH/ftXPCtFG5/TDobjsjz6sSNL0Sr8Z9g==", + "license": "MIT" + }, + "node_modules/react": { + "version": "19.2.4", + "resolved": "https://registry.npmmirror.com/react/-/react-19.2.4.tgz", + "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/regexparam": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/regexparam/-/regexparam-3.0.0.tgz", + "integrity": "sha512-RSYAtP31mvYLkAHrOlh25pCNQ5hWnT106VukGaaFfuJrZFkGRX5GhUAdPqpSDXxOhA2c4akmRuplv1mRqnBn6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/resolve-pkg-maps": { "version": "1.0.0", "resolved": "https://registry.npmmirror.com/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", @@ -1344,6 +2723,52 @@ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, + "node_modules/rollup": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.59.0.tgz", + "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.59.0", + "@rollup/rollup-android-arm64": "4.59.0", + "@rollup/rollup-darwin-arm64": "4.59.0", + "@rollup/rollup-darwin-x64": "4.59.0", + "@rollup/rollup-freebsd-arm64": "4.59.0", + "@rollup/rollup-freebsd-x64": "4.59.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", + "@rollup/rollup-linux-arm-musleabihf": "4.59.0", + "@rollup/rollup-linux-arm64-gnu": "4.59.0", + "@rollup/rollup-linux-arm64-musl": "4.59.0", + "@rollup/rollup-linux-loong64-gnu": "4.59.0", + "@rollup/rollup-linux-loong64-musl": "4.59.0", + "@rollup/rollup-linux-ppc64-gnu": "4.59.0", + "@rollup/rollup-linux-ppc64-musl": "4.59.0", + "@rollup/rollup-linux-riscv64-gnu": "4.59.0", + "@rollup/rollup-linux-riscv64-musl": "4.59.0", + "@rollup/rollup-linux-s390x-gnu": "4.59.0", + "@rollup/rollup-linux-x64-gnu": "4.59.0", + "@rollup/rollup-linux-x64-musl": "4.59.0", + "@rollup/rollup-openbsd-x64": "4.59.0", + "@rollup/rollup-openharmony-arm64": "4.59.0", + "@rollup/rollup-win32-arm64-msvc": "4.59.0", + "@rollup/rollup-win32-ia32-msvc": "4.59.0", + "@rollup/rollup-win32-x64-gnu": "4.59.0", + "@rollup/rollup-win32-x64-msvc": "4.59.0", + "fsevents": "~2.3.2" + } + }, "node_modules/semver": { "version": "7.7.3", "resolved": "https://registry.npmmirror.com/semver/-/semver-7.7.3.tgz", @@ -1402,6 +2827,46 @@ "@img/sharp-win32-x64": "0.34.5" } }, + "node_modules/simple-code-frame": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/simple-code-frame/-/simple-code-frame-1.3.0.tgz", + "integrity": "sha512-MB4pQmETUBlNs62BBeRjIFGeuy/x6gGKh7+eRUemn1rCFhqo7K+4slPqsyizCbcbYLnaYqaoZ2FWsZ/jN06D8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "kolorist": "^1.6.0" + } + }, + "node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmmirror.com/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stack-trace": { + "version": "1.0.0-pre2", + "resolved": "https://registry.npmmirror.com/stack-trace/-/stack-trace-1.0.0-pre2.tgz", + "integrity": "sha512-2ztBJRek8IVofG9DBJqdy2N5kulaacX30Nz7xmkYF6ale9WBVmIy6mFBchvGX7Vx/MyjBhx+Rcxqrj+dbOnQ6A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + } + }, "node_modules/supports-color": { "version": "10.2.2", "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-10.2.2.tgz", @@ -1415,6 +2880,23 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz", @@ -1429,6 +2911,7 @@ "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" @@ -1485,6 +2968,140 @@ "pathe": "^2.0.3" } }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmmirror.com/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/vite": { + "version": "7.3.1", + "resolved": "https://registry.npmmirror.com/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-prerender-plugin": { + "version": "0.5.12", + "resolved": "https://registry.npmmirror.com/vite-prerender-plugin/-/vite-prerender-plugin-0.5.12.tgz", + "integrity": "sha512-EiwhbMn+flg14EysbLTmZSzq8NGTxhytgK3bf4aGRF1evWLGwZiHiUJ1KZDvbxgKbMf2pG6fJWGEa3UZXOnR1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "kolorist": "^1.8.0", + "magic-string": "0.x >= 0.26.0", + "node-html-parser": "^6.1.12", + "simple-code-frame": "^1.3.0", + "source-map": "^0.7.4", + "stack-trace": "^1.0.0-pre2" + }, + "peerDependencies": { + "vite": "5.x || 6.x || 7.x" + } + }, "node_modules/workerd": { "version": "1.20260128.0", "resolved": "https://registry.npmmirror.com/workerd/-/workerd-1.20260128.0.tgz", @@ -1507,6 +3124,20 @@ "@cloudflare/workerd-windows-64": "1.20260128.0" } }, + "node_modules/wouter": { + "version": "3.9.0", + "resolved": "https://registry.npmmirror.com/wouter/-/wouter-3.9.0.tgz", + "integrity": "sha512-sF/od/PIgqEQBQcrN7a2x3MX6MQE6nW0ygCfy9hQuUkuB28wEZuu/6M5GyqkrrEu9M6jxdkgE12yDFsQMKos4Q==", + "license": "Unlicense", + "dependencies": { + "mitt": "^3.0.1", + "regexparam": "^3.0.0", + "use-sync-external-store": "^1.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, "node_modules/wrangler": { "version": "4.61.1", "resolved": "https://registry.npmmirror.com/wrangler/-/wrangler-4.61.1.tgz", @@ -1564,6 +3195,13 @@ } } }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, "node_modules/youch": { "version": "4.1.0-beta.10", "resolved": "https://registry.npmmirror.com/youch/-/youch-4.1.0-beta.10.tgz", diff --git a/package.json b/package.json index 213095a..f36b490 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,11 @@ "main": "src/index.ts", "type": "module", "scripts": { - "dev": "wrangler dev -c wrangler.toml", + "dev": "npm run web:build && wrangler dev -c wrangler.toml", + "dev:worker": "wrangler dev -c wrangler.toml", + "web:dev": "vite --config webapp/vite.config.ts", + "web:build": "vite build --config webapp/vite.config.ts", + "web:typecheck": "tsc -p webapp/tsconfig.json --noEmit", "deploymy": "wrangler deploy -c wrangler.my.toml", "deploy": "wrangler deploy" }, @@ -33,9 +37,17 @@ }, "devDependencies": { "@cloudflare/workers-types": "^4.20260131.0", + "@preact/preset-vite": "^2.10.3", "@types/node": "^25.2.3", "tsx": "^4.21.0", "typescript": "^5.9.3", + "vite": "^7.3.1", "wrangler": "^4.61.1" + }, + "dependencies": { + "@tanstack/react-query": "^5.90.21", + "preact": "^10.28.4", + "qrcode-generator": "^2.0.4", + "wouter": "^3.9.0" } } diff --git a/public/index.html b/public/index.html index 7b1ecd6..be3c367 100644 --- a/public/index.html +++ b/public/index.html @@ -1,14 +1,13 @@ - - - - NodeWarden Web - - - -
- - - + + + + NodeWarden + + + + +
+ diff --git a/public/web/app.js b/public/web/app.js deleted file mode 100644 index 1dad3d1..0000000 --- a/public/web/app.js +++ /dev/null @@ -1,2 +0,0 @@ -export { startNodewardenApp } from './main.js'; - diff --git a/public/web/crypto.js b/public/web/crypto.js deleted file mode 100644 index 88130e7..0000000 --- a/public/web/crypto.js +++ /dev/null @@ -1,150 +0,0 @@ -export function bytesToBase64(bytes) { - var s = ''; - for (var i = 0; i < bytes.length; i++) s += String.fromCharCode(bytes[i]); - return btoa(s); -} - -export function base64ToBytes(b64) { - var bin = atob(b64); - var bytes = new Uint8Array(bin.length); - for (var i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i); - return bytes; -} - -export function concatBytes(a, b) { - var o = new Uint8Array(a.length + b.length); - o.set(a, 0); - o.set(b, a.length); - return o; -} - -export async function pbkdf2(passwordOrBytes, saltOrBytes, iterations, keyLen) { - var pwdBytes = typeof passwordOrBytes === 'string' ? new TextEncoder().encode(passwordOrBytes) : passwordOrBytes; - var saltBytes = typeof saltOrBytes === 'string' ? new TextEncoder().encode(saltOrBytes) : saltOrBytes; - var key = await crypto.subtle.importKey('raw', pwdBytes, 'PBKDF2', false, ['deriveBits']); - var bits = await crypto.subtle.deriveBits({ name: 'PBKDF2', hash: 'SHA-256', salt: saltBytes, iterations: iterations }, key, keyLen * 8); - return new Uint8Array(bits); -} - -export async function hkdfExpand(prk, info, length) { - var enc = new TextEncoder(); - var key = await crypto.subtle.importKey('raw', prk, { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']); - var infoBytes = enc.encode(info || ''); - var result = new Uint8Array(length); - var prev = new Uint8Array(0); - var off = 0; - var cnt = 1; - while (off < length) { - var inp = new Uint8Array(prev.length + infoBytes.length + 1); - inp.set(prev, 0); - inp.set(infoBytes, prev.length); - inp[inp.length - 1] = cnt & 0xff; - prev = new Uint8Array(await crypto.subtle.sign('HMAC', key, inp)); - var c = Math.min(prev.length, length - off); - result.set(prev.slice(0, c), off); - off += c; - cnt += 1; - } - return result; -} - -export async function hmacSha256(keyBytes, dataBytes) { - var key = await crypto.subtle.importKey('raw', keyBytes, { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']); - return new Uint8Array(await crypto.subtle.sign('HMAC', key, dataBytes)); -} - -export async function encryptAesCbc(data, key, iv) { - var ck = await crypto.subtle.importKey('raw', key, { name: 'AES-CBC' }, false, ['encrypt']); - return new Uint8Array(await crypto.subtle.encrypt({ name: 'AES-CBC', iv: iv }, ck, data)); -} - -export async function decryptAesCbc(data, key, iv) { - var ck = await crypto.subtle.importKey('raw', key, { name: 'AES-CBC' }, false, ['decrypt']); - return new Uint8Array(await crypto.subtle.decrypt({ name: 'AES-CBC', iv: iv }, ck, data)); -} - -export async function encryptBw(data, encKey, macKey) { - var iv = crypto.getRandomValues(new Uint8Array(16)); - var cipher = await encryptAesCbc(data, encKey, iv); - var mac = await hmacSha256(macKey, concatBytes(iv, cipher)); - return '2.' + bytesToBase64(iv) + '|' + bytesToBase64(cipher) + '|' + bytesToBase64(mac); -} - -export function parseCipherString(s) { - if (!s || typeof s !== 'string') throw new Error('invalid encrypted string'); - if (s === 'null' || s === 'undefined') throw new Error('invalid encrypted string'); - var p = s.indexOf('.'); - if (p <= 0) throw new Error('invalid encrypted string'); - var type = Number(s.slice(0, p)); - var body = s.slice(p + 1); - var parts = body.split('|'); - if (type === 2 && parts.length === 3) return { type: 2, iv: base64ToBytes(parts[0]), ct: base64ToBytes(parts[1]), mac: base64ToBytes(parts[2]) }; - if ((type === 0 || type === 1 || type === 4) && parts.length >= 2) return { type: type, iv: base64ToBytes(parts[0]), ct: base64ToBytes(parts[1]), mac: null }; - throw new Error('unsupported enc type or format'); -} - -export async function decryptBw(cipherString, encKey, macKey) { - var parsed = parseCipherString(cipherString); - if (parsed.type === 2 && macKey && parsed.mac) { - var expect = await hmacSha256(macKey, concatBytes(parsed.iv, parsed.ct)); - if (bytesToBase64(expect) !== bytesToBase64(parsed.mac)) throw new Error('MAC mismatch'); - } - return decryptAesCbc(parsed.ct, encKey, parsed.iv); -} - -export async function decryptStr(cipherString, encKey, macKey) { - if (!cipherString || typeof cipherString !== 'string') return ''; - var plain = await decryptBw(cipherString, encKey, macKey); - return new TextDecoder().decode(plain); -} - -export function extractTotpSecret(raw) { - if (!raw) return ''; - var s = String(raw).trim(); - if (!s) return ''; - if (/^otpauth:\/\//i.test(s)) { - try { - var u = new URL(s); - var qp = u.searchParams.get('secret') || ''; - return qp.toUpperCase().replace(/[\s-]/g, '').replace(/=+$/g, ''); - } catch (_) {} - } - return s.toUpperCase().replace(/[\s-]/g, '').replace(/=+$/g, ''); -} - -export function base32ToBytes(input) { - var alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; - var clean = String(input || '').toUpperCase().replace(/[^A-Z2-7]/g, ''); - var bits = 0, value = 0, out = []; - for (var i = 0; i < clean.length; i++) { - var idx = alphabet.indexOf(clean.charAt(i)); - if (idx < 0) continue; - value = (value << 5) | idx; - bits += 5; - if (bits >= 8) { - out.push((value >>> (bits - 8)) & 0xff); - bits -= 8; - } - } - return new Uint8Array(out); -} - -export async function calcTotpNow(rawSecret) { - var secret = extractTotpSecret(rawSecret); - if (!secret) return null; - var keyBytes = base32ToBytes(secret); - if (!keyBytes.length) return null; - var step = 30; - var epoch = Math.floor(Date.now() / 1000); - var counter = Math.floor(epoch / step); - var remain = step - (epoch % step); - var msg = new Uint8Array(8); - var c = counter; - for (var i = 7; i >= 0; i--) { msg[i] = c & 0xff; c = Math.floor(c / 256); } - var key = await crypto.subtle.importKey('raw', keyBytes, { name: 'HMAC', hash: 'SHA-1' }, false, ['sign']); - var hs = new Uint8Array(await crypto.subtle.sign('HMAC', key, msg)); - var off = hs[hs.length - 1] & 0x0f; - var bin = ((hs[off] & 0x7f) << 24) | ((hs[off + 1] & 0xff) << 16) | ((hs[off + 2] & 0xff) << 8) | (hs[off + 3] & 0xff); - var code = (bin % 1000000).toString().padStart(6, '0'); - return { code: code, remain: remain }; -} diff --git a/public/web/i18n.js b/public/web/i18n.js deleted file mode 100644 index 61e2e14..0000000 --- a/public/web/i18n.js +++ /dev/null @@ -1,216 +0,0 @@ -export const I18N = { - en: { - brand: 'NodeWarden', - subtitle: 'Open Source Password Manager', - login: 'Log In', - register: 'Create Account', - email: 'Email Address', - masterPwd: 'Master Password', - confirmPwd: 'Confirm Master Password', - name: 'Name', - inviteCode: 'Invite Code (Optional)', - loginBtn: 'Log In', - registerBtn: 'Create Account', - backToLogin: 'Back to Log In', - vault: 'Vault', - settings: 'Settings', - admin: 'Admin', - help: 'Help', - logout: 'Log Out', - folders: 'Folders', - allItems: 'All Items', - noFolder: 'No Folder', - searchVault: 'Search vault', - filter: 'Search', - typeAll: 'All items', - typeLogin: 'Logins', - typeCard: 'Cards', - typeIdentity: 'Identities', - typeNote: 'Secure notes', - typeOther: 'Other', - addWebsite: '+ Add website', - addField: '+ Add field', - fieldType: 'Field type', - fieldLabel: 'Field label', - fieldValue: 'Field value', - fieldText: 'Text', - fieldHidden: 'Hidden', - fieldBoolean: 'Boolean', - fieldLinked: 'Linked', - add: 'Add', - newTypeLogin: 'Login', - newTypeCard: 'Card', - newTypeIdentity: 'Identity', - newTypeNote: 'Note', - newTypeSsh: 'SSH key', - refresh: 'Sync', - move: 'Move', - delete: 'Delete', - selectAll: 'Select All', - clear: 'Cancel', - noItems: 'There are no items to list.', - selectItem: 'Select an item to view details.', - profile: 'Profile', - saveProfile: 'Save Profile', - changePwd: 'Change Master Password', - currentPwd: 'Current Master Password', - newPwd: 'New Master Password', - totpSetup: 'Two-Step Login (TOTP)', - totpLiveIn: 'Refresh in', - enableTotp: 'Enable TOTP', - disableTotp: 'Disable TOTP', - secret: 'Authenticator Key', - verifyCode: 'Verification Code', - credentials: 'Login credentials', - autofillOptions: 'Autofill', - itemHistory: 'Item history', - website: 'Website', - folder: 'Folder', - createdAt: 'Created', - updatedAt: 'Last edited', - open: 'Open', - copy: 'Copy', - reveal: 'Reveal', - hide: 'Hide', - users: 'Users', - invites: 'Invites', - createInvite: 'Create Invite', - expiresIn: 'Expires in (hours)', - copyLink: 'Copy Link', - revoke: 'Revoke', - ban: 'Ban', - unban: 'Unban', - status: 'Status', - role: 'Role', - action: 'Options', - loading: 'Loading NodeWarden...', - totpVerify: 'Two-step verification', - totpVerifySub: 'Password is already verified.', - totpCode: 'TOTP Code', - verify: 'Verify', - cancel: 'Cancel', - totpDisableSub: 'Enter master password to disable two-step verification.', - helpSync: 'Upstream Sync', - helpSync1: 'Track upstream with a fork and scheduled sync workflow (recommended).', - helpSync2: 'Before merge: compare API routes, migration files, and auth logic changes.', - helpSync3: 'After merge: run local dev migration tests, then deploy Worker after validation.', - helpErr: 'Common Errors', - helpErr1: '401 Unauthorized: token expired or revoked, login again.', - helpErr2: '403 Account disabled: admin must unban user in User Management.', - helpErr3: '403 Invite invalid: invite expired/used/revoked, create a new invite.', - helpErr4: '429 Too many requests: wait retry seconds and avoid burst writes.', - helpTb: 'Troubleshooting', - helpTb1: 'Login OK but encrypted values shown: verify profile key and KDF settings are consistent.', - helpTb2: 'TOTP fails repeatedly: sync device time and re-scan QR using latest secret.', - helpTb3: 'Password change failed: ensure current password is correct and new password has at least 12 chars.', - helpTb4: 'Sync conflicts: refresh vault and retry one operation at a time.', - langSwitch: '中文', - }, - zh: { - brand: 'NodeWarden', - subtitle: '开源密码管理器', - login: '登录', - register: '创建账号', - email: '电子邮件地址', - masterPwd: '主密码', - confirmPwd: '确认主密码', - name: '姓名', - inviteCode: '邀请码 (可选)', - loginBtn: '登录', - registerBtn: '创建账号', - backToLogin: '返回登录', - vault: '密码库', - settings: '设置', - admin: '管理', - help: '帮助', - logout: '退出登录', - folders: '文件夹', - allItems: '所有项目', - noFolder: '无文件夹', - searchVault: '搜索密码库', - filter: '筛选', - typeAll: '所有项目', - typeLogin: '登录', - typeCard: '支付卡', - typeIdentity: '身份', - typeNote: '备注', - typeOther: '其他', - addWebsite: '+ 添加网站', - addField: '+ 添加字段', - fieldType: '字段类型', - fieldLabel: '字段标签', - fieldValue: '字段值', - fieldText: '文本型', - fieldHidden: '隐藏型', - fieldBoolean: '复选框型', - fieldLinked: '链接型', - add: '添加', - newTypeLogin: '登录', - newTypeCard: '支付卡', - newTypeIdentity: '身份', - newTypeNote: '笔记', - newTypeSsh: 'SSH 密钥', - refresh: '同步', - move: '移动', - delete: '删除', - selectAll: '全选', - clear: '取消', - noItems: '没有可列出的项目。', - selectItem: '选择一个项目以查看详细信息。', - profile: '个人资料', - saveProfile: '保存个人资料', - changePwd: '更改主密码', - currentPwd: '当前主密码', - newPwd: '新主密码', - totpSetup: '两步登录 (TOTP)', - totpLiveIn: '刷新剩余', - enableTotp: '启用 TOTP', - disableTotp: '禁用 TOTP', - secret: '身份验证器密钥', - verifyCode: '验证码', - credentials: '登录凭据', - autofillOptions: '自动填充', - itemHistory: '项目历史记录', - website: '网站', - folder: '文件夹', - createdAt: '创建于', - updatedAt: '最后编辑', - open: '打开', - copy: '复制', - reveal: '显示', - hide: '隐藏', - users: '用户', - invites: '邀请', - createInvite: '创建邀请', - expiresIn: '过期时间 (小时)', - copyLink: '复制链接', - revoke: '撤销', - ban: '封禁', - unban: '解封', - status: '状态', - role: '角色', - action: '选项', - loading: '正在加载 NodeWarden...', - totpVerify: '两步验证', - totpVerifySub: '密码已验证。', - totpCode: 'TOTP 验证码', - verify: '验证', - cancel: '取消', - totpDisableSub: '输入主密码以禁用两步验证。', - helpSync: '上游同步', - helpSync1: '建议通过 fork 和定时同步工作流跟踪上游。', - helpSync2: '合并前:比较 API 路由、迁移文件和认证逻辑的更改。', - helpSync3: '合并后:运行本地开发迁移测试,验证后部署 Worker。', - helpErr: '常见错误', - helpErr1: '401 未授权:令牌过期或被撤销,请重新登录。', - helpErr2: '403 账号被禁用:管理员必须在用户管理中解封用户。', - helpErr3: '403 邀请无效:邀请已过期/已使用/被撤销,请创建新邀请。', - helpErr4: '429 请求过多:等待重试时间,避免突发写入。', - helpTb: '排障指南', - helpTb1: '登录成功但显示密文:检查 profile key 和 KDF 参数是否一致。', - helpTb2: 'TOTP 持续失败:同步设备时间并使用最新密钥重新扫码。', - helpTb3: '修改密码失败:确认当前密码正确且新密码至少 12 位。', - helpTb4: '同步冲突:先刷新密码库,再逐个操作重试。', - langSwitch: 'English', - }, -}; diff --git a/public/web/main.js b/public/web/main.js deleted file mode 100644 index 63462d1..0000000 --- a/public/web/main.js +++ /dev/null @@ -1,1522 +0,0 @@ -import { I18N } from './i18n.js'; -import { bytesToBase64, base64ToBytes, concatBytes, pbkdf2, hkdfExpand, encryptBw, decryptBw, decryptStr, extractTotpSecret, calcTotpNow } from './crypto.js'; -import { parseFieldType as parseFieldTypeUtil, selectedCount as selectedCountUtil, cipherTypeKey as cipherTypeKeyUtil, firstCipherUri as firstCipherUriUtil, hostFromUri as hostFromUriUtil } from './vault-utils.js'; - -export function startNodewardenApp(runtimeConfig) { - var app = document.getElementById('app'); - var defaultKdfIterations = Number(runtimeConfig && runtimeConfig.defaultKdfIterations) || 600000; - var state = { - phase: 'loading', - lang: (navigator.language || '').toLowerCase().startsWith('zh') ? 'zh' : 'en', - msg: '', - msgType: 'ok', - inviteCode: '', - registerName: '', - registerEmail: '', - registerPassword: '', - registerPassword2: '', - registerShowPassword: false, - registerShowPassword2: false, - session: null, - profile: null, - tab: 'vault', - ciphers: [], - folders: [], - folderFilterId: '', - vaultQuery: '', - vaultType: 'all', - showSelectedPassword: false, - vaultSearchComposing: false, - vaultSearchTimer: 0, - totpTicking: false, - totpTickBusy: false, - detailMode: 'view', - detailDraft: null, - createMenuOpen: false, - fieldModalOpen: false, - fieldModalType: 'text', - fieldModalLabel: '', - fieldModalValue: '', - selectedCipherId: '', - selectedMap: {}, - users: [], - invites: [], - loginEmail: '', - loginPassword: '', - loginShowPassword: false, - loginTotpToken: '', - loginTotpError: '', - pendingLogin: null, - totpSetupSecret: '', - totpSetupToken: '', - totpDisableOpen: false, - totpDisablePassword: '', - totpDisableError: '', - unlockPassword: '', - unlockError: '', - unlockShowPassword: false, - lockTimeoutMinutes: 15, - lockLastActiveTs: Date.now(), - lockCheckTimer: 0, - lockChannel: null, - toasts: [], - toastSeq: 0, - dialog: null - }; - var NO_FOLDER_FILTER = '__none__'; - var i18n = I18N; - - function t(key) { return i18n[state.lang][key] || key; } - - function esc(v) { - return String(v == null ? '' : v).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"').replace(/'/g, '''); - } - function sessionKey() { return 'nodewarden.web.session.v2'; } - function lockSettingsKey() { return 'nodewarden.web.lock.v1'; } - function dismissToast(id) { - var next = []; - for (var i = 0; i < state.toasts.length; i++) if (state.toasts[i].id !== id) next.push(state.toasts[i]); - if (next.length === state.toasts.length) return; - state.toasts = next; - render(); - } - function setMsg(t, ty) { - var text = String(t || '').trim(); - if (!text) return; - var id = 'toast-' + (++state.toastSeq); - var level = ty === 'err' ? 'error' : (ty === 'warn' ? 'warning' : 'success'); - state.toasts.push({ id: id, text: text, level: level }); - if (state.toasts.length > 4) state.toasts = state.toasts.slice(state.toasts.length - 4); - render(); - setTimeout(function () { dismissToast(id); }, 4500); - } - function clearMsg() {} - function renderMsg() { return ''; } - function renderToasts() { - if (!state.toasts || state.toasts.length === 0) return ''; - var items = ''; - for (var i = 0; i < state.toasts.length; i++) { - var x = state.toasts[i]; - items += '
  • ' - + '
    ' + esc(x.text) + '
    ' - + '' - + '
    ' - + '
  • '; - } - return ''; - } - function askConfirm(opts) { - return new Promise(function (resolve) { - state.dialog = { - type: 'confirm', - title: String(opts && opts.title || 'Confirm'), - message: String(opts && opts.message || ''), - okText: String(opts && opts.okText || 'Yes'), - cancelText: String(opts && opts.cancelText || 'No'), - danger: !!(opts && opts.danger), - resolve: resolve - }; - render(); - }); - } - function askMoveFolder() { - return new Promise(function (resolve) { - state.dialog = { - type: 'move', - title: 'Move selected items', - message: 'Choose destination folder.', - selectedFolderId: '__none__', - resolve: resolve - }; - render(); - }); - } - function closeDialog(result) { - var d = state.dialog; - state.dialog = null; - render(); - if (d && typeof d.resolve === 'function') d.resolve(result); - } - function renderDialog() { - var d = state.dialog; - if (!d) return ''; - if (d.type === 'move') { - var options = ''; - for (var i = 0; i < state.folders.length; i++) { - var f = state.folders[i]; - var id = String(f.id || ''); - options += ''; - } - return '

    ' + esc(d.title) + '

    ' + esc(d.message) + '
    '; - } - return '

    ' + esc(d.title) + '

    ' + esc(d.message) + '
    '; - } - function saveSession() { - if (!state.session) { localStorage.removeItem(sessionKey()); return; } - var persisted = { - accessToken: state.session.accessToken || '', - refreshToken: state.session.refreshToken || '', - email: state.session.email || '' - }; - localStorage.setItem(sessionKey(), JSON.stringify(persisted)); - } - function loadSession() { - try { - var r = localStorage.getItem(sessionKey()); - if (!r) return null; - var p = JSON.parse(r); - if (!p || !p.accessToken || !p.refreshToken) return null; - return { accessToken: p.accessToken, refreshToken: p.refreshToken, email: p.email || '' }; - } catch (e) { return null; } - } - function saveLockSettings() { - localStorage.setItem(lockSettingsKey(), JSON.stringify({ lockTimeoutMinutes: Number(state.lockTimeoutMinutes) || 0 })); - } - function loadLockSettings() { - try { - var r = localStorage.getItem(lockSettingsKey()); - if (!r) return; - var p = JSON.parse(r); - var mins = Number(p && p.lockTimeoutMinutes); - if (Number.isFinite(mins) && mins >= 0) state.lockTimeoutMinutes = mins; - } catch (_) {} - } - async function jsonOrNull(resp){ var t=await resp.text(); if(!t) return null; try{ return JSON.parse(t);} catch(e){ return null; } } - async function decryptVault(){ - if(!state.session||!state.session.symEncKey||!state.session.symMacKey) return; - var encKey=base64ToBytes(state.session.symEncKey); var macKey=base64ToBytes(state.session.symMacKey); - for(var i=0;i= mins * 60 * 1000) { - lockVault(true, true); - } - }, 5000); - } - - function lockVault(showMsg, broadcast) { - if (state.session) { - delete state.session.symEncKey; - delete state.session.symMacKey; - } - clearVaultMemory(); - state.pendingLogin = null; - state.loginTotpToken = ''; - state.loginTotpError = ''; - state.unlockPassword = ''; - state.unlockError = ''; - state.unlockShowPassword = false; - state.phase = 'locked'; - if (broadcast !== false && state.lockChannel) { - try { state.lockChannel.postMessage({ type: 'lock', at: Date.now() }); } catch (_) {} - } - if (showMsg) setMsg('Vault locked.', 'ok'); - else render(); - } - - async function onUnlock(form) { - clearMsg(); - state.unlockError = ''; - var fd = new FormData(form); - state.unlockPassword = String(fd.get('password') || ''); - if (!state.unlockPassword) { - state.unlockError = 'Please input master password.'; - render(); - return; - } - try { - var email = String(state.profile && state.profile.email ? state.profile.email : state.session && state.session.email ? state.session.email : '').toLowerCase(); - if (!email) throw new Error('email missing'); - var d = await deriveLoginHash(email, state.unlockPassword); - var ek = await hkdfExpand(d.masterKey, 'enc', 32); - var em = await hkdfExpand(d.masterKey, 'mac', 32); - var symKeyBytes = await decryptBw(state.profile.key, ek, em); - if (!symKeyBytes || symKeyBytes.length < 64) throw new Error('invalid key'); - state.session.symEncKey = bytesToBase64(symKeyBytes.slice(0, 32)); - state.session.symMacKey = bytesToBase64(symKeyBytes.slice(32, 64)); - state.unlockPassword = ''; - state.unlockError = ''; - state.unlockShowPassword = false; - await loadVault(); - await loadAdminData(); - state.phase = 'app'; - state.tab = 'vault'; - state.lockLastActiveTs = Date.now(); - render(); - setMsg('Unlocked.', 'ok'); - } catch (e) { - state.unlockError = 'Unlock failed. Master password is incorrect.'; - render(); - } - } - - function logout(){ - state.session=null; state.profile=null; state.ciphers=[]; state.folders=[]; state.users=[]; state.invites=[]; state.folderFilterId=''; state.selectedCipherId=''; state.selectedMap={}; state.pendingLogin=null; state.loginTotpToken=''; state.loginTotpError=''; state.totpDisableOpen=false; state.totpDisablePassword=''; state.totpDisableError=''; state.unlockPassword=''; state.unlockError=''; state.unlockShowPassword=false; state.phase='login'; saveSession(); clearMsg(); render(); - } - - async function authFetch(path, options){ - var opts=options||{}; if(!state.session||!state.session.accessToken) throw new Error('unauthorized'); - var h=opts.headers?Object.assign({},opts.headers):{}; h.Authorization='Bearer '+state.session.accessToken; - var r=await fetch(path,Object.assign({},opts,{headers:h})); if(r.status!==401) return r; if(!state.session.refreshToken) return r; - var f=new URLSearchParams(); f.set('grant_type','refresh_token'); f.set('refresh_token',state.session.refreshToken); - var rr=await fetch('/identity/connect/token',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:f.toString()}); - if(!rr.ok){ logout(); return r; } - var tj=await rr.json(); state.session.accessToken=tj.access_token; state.session.refreshToken=tj.refresh_token||state.session.refreshToken; saveSession(); - h.Authorization='Bearer '+state.session.accessToken; return fetch(path,Object.assign({},opts,{headers:h})); - } - - async function loadProfile(){ var r=await authFetch('/api/accounts/profile',{method:'GET'}); if(!r.ok) throw new Error('profile'); state.profile=await r.json(); } - async function loadVault(){ var cr=await authFetch('/api/ciphers',{method:'GET'}); var fr=await authFetch('/api/folders',{method:'GET'}); if(!cr.ok||!fr.ok) throw new Error('vault'); var cj=await cr.json(); var fj=await fr.json(); state.ciphers=cj.data||[]; state.folders=fj.data||[]; if(!state.selectedCipherId&&state.ciphers.length>0) state.selectedCipherId=state.ciphers[0].id; await decryptVault(); } - async function loadAdminData(){ if(!state.profile||state.profile.role!=='admin') return; var u=await authFetch('/api/admin/users',{method:'GET'}); if(u.ok){ var uj=await u.json(); state.users=uj.data||[]; } var i=await authFetch('/api/admin/invites?includeInactive=true',{method:'GET'}); if(i.ok){ var ij=await i.json(); state.invites=ij.data||[]; } } - - function selectedCount(){ return selectedCountUtil(state.selectedMap); } - function cipherTypeKey(c){ return cipherTypeKeyUtil(c); } - function cipherTypeLabel(c){ - var k=cipherTypeKey(c); - if(k==='login') return t('typeLogin'); - if(k==='card') return t('typeCard'); - if(k==='identity') return t('typeIdentity'); - if(k==='note') return t('typeNote'); - return t('typeOther'); - } - function folderNameById(id){ - for(var i=0;i'; - }catch(_){ - box.innerHTML='
    QR unavailable
    Use secret key below
    '; - } - } - function buildSymmetricKeyBytes(){ - if(!state.session||!state.session.symEncKey||!state.session.symMacKey) return null; - try{ - var enc=base64ToBytes(state.session.symEncKey); - var mac=base64ToBytes(state.session.symMacKey); - if(enc.length!==32||mac.length!==32) return null; - var out=new Uint8Array(64); - out.set(enc,0); - out.set(mac,32); - return out; - }catch(e){ - return null; - } - } - function getUserCryptoKeys(){ - var sym=buildSymmetricKeyBytes(); - if(!sym||sym.length<64) return null; - return { enc: sym.slice(0,32), mac: sym.slice(32,64) }; - } - async function getCipherCryptoKeys(cipher){ - var user=getUserCryptoKeys(); - if(!user) return null; - if(cipher&&cipher.key){ - try{ - var raw=await decryptBw(cipher.key,user.enc,user.mac); - if(raw&&raw.length>=64) return { enc: raw.slice(0,32), mac: raw.slice(32,64), key: cipher.key }; - }catch(e){} - } - return { enc: user.enc, mac: user.mac, key: null }; - } - async function encryptTextValue(v, enc, mac){ - var s=String(v==null?'':v); - if(!s) return null; - return encryptBw(new TextEncoder().encode(s), enc, mac); - } - function openCreateDraft(){ - state.detailMode='create'; - state.showSelectedPassword=true; - state.createMenuOpen=false; - state.detailDraft={ - id: '', - type: 1, - name: '', - folderId: state.folderFilterId&&state.folderFilterId!==NO_FOLDER_FILTER?state.folderFilterId:'', - reprompt: false, - loginUsername: '', - loginPassword: '', - loginTotp: '', - websites: [''], - cardholderName: '', - cardNumber: '', - cardBrand: '', - cardExpMonth: '', - cardExpYear: '', - cardCode: '', - identTitle: '', - identFirstName: '', - identMiddleName: '', - identLastName: '', - identUsername: '', - identCompany: '', - identSsn: '', - identPassportNumber: '', - identLicenseNumber: '', - identEmail: '', - identPhone: '', - identAddress1: '', - identAddress2: '', - identAddress3: '', - identCity: '', - identState: '', - identPostalCode: '', - identCountry: '', - sshPrivateKey: '', - sshPublicKey: '', - sshFingerprint: '', - customFields: [], - notes: '' - }; - } - function openEditDraft(cipher){ - if(!cipher) return; - var login=cipher.login||{}; - var uris=Array.isArray(login.uris)?login.uris:[]; - var ws=[]; for(var i=0;i'+l+''; } - return opt('text',t('fieldText'))+opt('hidden',t('fieldHidden'))+opt('boolean',t('fieldBoolean'))+opt('linked',t('fieldLinked')); - } - function renderCreateMenu(){ - if(!state.createMenuOpen) return ''; - return '
    '; - } - function renderFieldModal(){ - if(!state.fieldModalOpen) return ''; - return '' - + '

    '+t('addField')+'

    ' - + '
    ' - + '
    ' - + '
    ' - + '
    ' - + '
    '; - } - function fieldTypeTextByNum(n){ - var x=parseFieldType(n); - if(x===1) return t('fieldHidden'); - if(x===2) return t('fieldBoolean'); - if(x===3) return t('fieldLinked'); - return t('fieldText'); - } - function renderCardBrandOptions(selected){ - var s=String(selected||'').toLowerCase(); - var brands=['','visa','mastercard','amex','discover','jcb','unionpay','dinersclub','maestro']; - var labels={ '':'-- Select --', visa:'Visa', mastercard:'Mastercard', amex:'American Express', discover:'Discover', jcb:'JCB', unionpay:'UnionPay', dinersclub:'Diners Club', maestro:'Maestro' }; - var out=''; - for(var i=0;i'+labels[b]+''; - } - return out; - } - function renderMonthOptions(selected){ - var s=String(selected||''); - var out=''; - for(var m=1;m<=12;m++){ - var mm=m<10?('0'+m):String(m); - out += ''; - } - return out; - } - function renderDraftTypeCards(d){ - var typeNum=Number(d&&d.type||1); - if(typeNum===3){ - return '' - + '
    Card details
    ' - + '
    Cardholder name
    ' - + '
    Number
    ' - + '
    Brand
    ' - + '
    Exp month
    Exp year
    ' - + '
    Security code (CVV)
    ' - + '
    '; - } - if(typeNum===4){ - return '' - + '
    Personal details
    ' - + '
    Title
    ' - + '
    First name
    ' - + '
    Middle name
    ' - + '
    Last name
    ' - + '
    Username
    ' - + '
    Company
    ' - + '
    ' - + '
    Identity
    ' - + '
    SSN
    ' - + '
    Passport number
    ' - + '
    License number
    ' - + '
    ' - + '
    Contact information
    ' - + '
    Email
    ' - + '
    Phone
    ' - + '
    ' - + '
    Address
    ' - + '
    Address 1
    ' - + '
    Address 2
    ' - + '
    Address 3
    ' - + '
    City / Town
    ' - + '
    State / Province
    ' - + '
    ZIP / Postal code
    ' - + '
    Country
    ' - + '
    '; - } - if(typeNum===5){ - return '' - + '
    SSH key
    ' - + '
    Private key
    ' - + '
    Public key
    ' - + '
    Fingerprint
    ' - + '
    '; - } - if(typeNum===2){ - return ''; - } - return '' - + '
    '+t('credentials')+'
    ' - + '
    Username
    ' - + '
    Password
    ' - + '
    TOTP Secret
    ' - + '
    '; - } - function renderReadOnlyCustomFields(cipher){ - var fs=Array.isArray(cipher&&cipher.fields)?cipher.fields:[]; - if(!fs.length) return ''; - var rows=''; - for(var i=0;i
    '+esc(value||'')+'
    '; - } - return '
    Fields
    '+rows+'
    '; - } - function renderReadOnlyTypeDetails(c0, folderLabel, created, updated){ - var typeNum=Number(c0&&c0.type||1); - var notes=c0&&((c0.decNotes||c0.notes)||''); - var baseHead='' - + '
    ' - + '
    '+esc(c0.decName||c0.name||'')+'
    ' - + '
    '+t('folder')+': '+esc(folderLabel||t('noFolder'))+'
    ' - + '
    '; - var history='' - + '
    '+t('itemHistory')+'
    ' - + '
    '+t('updatedAt')+': '+esc(updated)+'
    ' - + '
    '+t('createdAt')+': '+esc(created)+'
    ' - + '
    '; - if(typeNum===3){ - var c=c0.card||{}; - return baseHead - + '
    Card details
    ' - + '
    Cardholder name
    '+esc(c.decCardholderName||c.cardholderName||'')+'
    ' - + '
    Number
    '+esc(c.decNumber||c.number||'')+'
    ' - + '
    Brand
    '+esc(c.decBrand||c.brand||'')+'
    ' - + '
    Exp month/year
    '+esc((c.decExpMonth||c.expMonth||'')+' / '+(c.decExpYear||c.expYear||''))+'
    ' - + '
    Security code (CVV)
    '+esc(c.decCode||c.code||'')+'
    ' - + '
    ' - + '
    Additional options
    Notes
    '+esc(notes)+'
    ' - + renderReadOnlyCustomFields(c0) - + history; - } - if(typeNum===4){ - var id=c0.identity||{}; - return baseHead - + '
    Personal details
    ' - + '
    Title
    '+esc(id.decTitle||id.title||'')+'
    ' - + '
    First name
    '+esc(id.decFirstName||id.firstName||'')+'
    ' - + '
    Middle name
    '+esc(id.decMiddleName||id.middleName||'')+'
    ' - + '
    Last name
    '+esc(id.decLastName||id.lastName||'')+'
    ' - + '
    Username
    '+esc(id.decUsername||id.username||'')+'
    ' - + '
    Company
    '+esc(id.decCompany||id.company||'')+'
    ' - + '
    ' - + '
    Identity
    ' - + '
    SSN
    '+esc(id.decSsn||id.ssn||'')+'
    ' - + '
    Passport number
    '+esc(id.decPassportNumber||id.passportNumber||'')+'
    ' - + '
    License number
    '+esc(id.decLicenseNumber||id.licenseNumber||'')+'
    ' - + '
    ' - + '
    Contact information
    ' - + '
    Email
    '+esc(id.decEmail||id.email||'')+'
    ' - + '
    Phone
    '+esc(id.decPhone||id.phone||'')+'
    ' - + '
    ' - + '
    Address
    ' - + '
    Address 1
    '+esc(id.decAddress1||id.address1||'')+'
    ' - + '
    Address 2
    '+esc(id.decAddress2||id.address2||'')+'
    ' - + '
    Address 3
    '+esc(id.decAddress3||id.address3||'')+'
    ' - + '
    City / Town
    '+esc(id.decCity||id.city||'')+'
    ' - + '
    State / Province
    '+esc(id.decState||id.state||'')+'
    ' - + '
    ZIP / Postal code
    '+esc(id.decPostalCode||id.postalCode||'')+'
    ' - + '
    Country
    '+esc(id.decCountry||id.country||'')+'
    ' - + '
    ' - + '
    Additional options
    Notes
    '+esc(notes)+'
    ' - + renderReadOnlyCustomFields(c0) - + history; - } - if(typeNum===5){ - var ssh=c0.sshKey||{}; - var privateKey=ssh.decPrivateKey||ssh.privateKey||''; - return baseHead - + '
    SSH key
    ' - + '
    Private key
    '+esc(privateKey?new Array(Math.max(String(privateKey).length,12)+1).join('•'):'')+'
    ' - + '
    Public key
    '+esc(ssh.decPublicKey||ssh.publicKey||'')+'
    ' - + '
    Fingerprint
    '+esc(ssh.decFingerprint||ssh.fingerprint||'')+'
    ' - + '
    ' - + '
    Additional options
    Notes
    '+esc(notes)+'
    ' - + renderReadOnlyCustomFields(c0) - + history; - } - if(typeNum===2){ - return baseHead - + '
    Additional options
    Notes
    '+esc(notes)+'
    ' - + renderReadOnlyCustomFields(c0) - + history; - } - var login=c0.login||{}; - var username=login.decUsername||login.username||''; - var rawPwd=login.decPassword||login.password||''; - var masked=rawPwd?new Array(Math.max(rawPwd.length,12)+1).join('•'):''; - var pwdText=state.showSelectedPassword?rawPwd:masked; - var totp=login.decTotp||login.totp||''; - var uri0=firstCipherUri(c0); - return baseHead - + '
    '+t('credentials')+'
    ' - + '
    Username
    '+esc(username)+'
    ' - + '
    Password
    '+esc(pwdText)+'
    ' - + (totp?('
    TOTP
    ...
    '):'') - + '
    ' - + '
    '+t('autofillOptions')+'
    ' - + '
    '+t('website')+'
    '+esc(uri0||'')+'
    '+(uri0?(''):'')+(uri0?(''):'')+'
    ' - + '
    ' - + renderReadOnlyCustomFields(c0) - + history; - } - function renderLoginScreen(){ - return '' - + '
    ' - + '
    '+t('langSwitch')+'
    ' - + '
    ' - + '
    ' - + '
    '+t('login')+'
    ' - + '
    '+t('brand')+'
    ' - + '
    ' - + renderMsg() - + '
    ' - + '
    ' - + '
    ' - + ' ' - + '
    ' - + '
    or
    ' - + '
    ' - + ' ' - + '
    ' - + (state.pendingLogin ? '' - + '

    '+t('totpVerify')+'

    '+t('totpVerifySub')+'
    ' - + (state.loginTotpError?'
    '+esc(state.loginTotpError)+'
    ':'') - + '
    ' - + '
    ' - : '') - + '
    ' - + '
    '; - } - - function renderRegisterScreen(){ - return '' - + '
    ' - + '
    '+t('langSwitch')+'
    ' - + '
    ' - + '
    ' - + '
    '+t('register')+'
    ' - + '
    '+t('brand')+'
    ' - + '
    ' - + renderMsg() - + '
    ' - + '
    ' - + '
    ' - + '
    ' - + '
    ' - + '
    ' - + ' ' - + '
    ' - + '
    or
    ' - + '
    ' - + ' ' - + '
    ' - + '
    ' - + '
    '; - } - - function renderLockedScreen(){ - var email = String(state.profile && state.profile.email ? state.profile.email : state.session && state.session.email ? state.session.email : ''); - return '' - + '
    ' - + '
    '+t('langSwitch')+'
    ' - + '
    ' - + '
    ' - + '
    Unlock Vault
    ' - + '
    '+esc(email)+'
    ' - + '
    ' - + renderMsg() - + (state.unlockError?('
    '+esc(state.unlockError)+'
    '):'') - + '
    ' - + '
    ' - + ' ' - + '
    ' - + '
    or
    ' - + '
    ' - + ' ' - + '
    ' - + '
    ' - + '
    '; - } - - function renderVaultTab(){ - var list=filteredCiphers(); - function renderFolderOptions(selectedId){ - var html=''; - for(var fi=0;fi'+esc(ff.decName||ff.name||ff.id)+''; - } - return html; - } - var rows=''; - for(var i=0;i') - : '🌐'; - rows += '' - + '
    ' - + '' - + '
    '+icon+'
    '+esc(nameText)+'
    '+esc(subtitle||'')+'
    ' - + '
    '; - } - if(!rows) rows='
    '+t('noItems')+'
    '; - - var c0=selectedCipher(); - var detail='
    '+t('selectItem')+'
    '; - if(state.detailMode==='create'){ - var dc=state.detailDraft||{}; - var wsHtml=''; var cws=Array.isArray(dc.websites)?dc.websites:['']; - for(var wci=0;wci'+(cws.length>1?'':'')+''; - } - var cfHtml=''; var cfs=Array.isArray(dc.customFields)?dc.customFields:[]; - for(var cfi=0;cfi
    '+esc(cf.value||'')+'
    '; - } - detail='' - + '
    '+t('folder')+':
    ' - + renderDraftTypeCards(dc) - + (Number(dc.type||1)===1?('
    '+t('autofillOptions')+'
    '+wsHtml+'' - + '
    '):'') - + '
    Additional options
    ' - + '' - + '
    ' - + '
    ' - + '
    Fields
    '+cfHtml+'
    ' - + '
    '; - } else if(c0){ - var folderLabel=c0.folderId?folderNameById(c0.folderId):t('noFolder'); - var updated=c0.revisionDate||c0.updatedAt||''; - var created=c0.creationDate||c0.createdAt||''; - if(state.detailMode==='edit'){ - var de=state.detailDraft||{}; - var ewsHtml=''; var ews=Array.isArray(de.websites)?de.websites:['']; - for(var wei=0;wei'+(ews.length>1?'':'')+''; - } - var efsHtml=''; var efs=Array.isArray(de.customFields)?de.customFields:[]; - for(var efi=0;efi
    '+esc(ef.value||'')+'
    '; - } - detail='' - + '
    '+t('folder')+':
    ' - + renderDraftTypeCards(de) - + (Number(de.type||1)===1?('
    '+t('autofillOptions')+'
    '+ewsHtml+'' - + '
    '):'') - + '
    Additional options
    ' - + '' - + '
    ' - + '
    ' - + '
    Fields
    '+efsHtml+'
    ' - + '
    '; - } else detail=renderReadOnlyTypeDetails(c0, folderLabel, created, updated) - + '
    '; - } - - return '' - + renderMsg() - + '
    '+renderCreateMenu()+'
    '+rows+'
    '+detail+renderFieldModal()+'
    '; - } - - function renderSettingsTab(){ - var p=state.profile||{}; - var secret=currentTotpSecret(); - var lockMins = Number(state.lockTimeoutMinutes)||0; - return '' - + renderMsg() - + '

    '+t('settings')+'

    ' - + '

    Vault Lock

    ' - + '

    '+t('profile')+'

    ' - + '

    '+t('changePwd')+'

    After success, current sessions are revoked and you must log in again.
    ' - + '

    '+t('totpSetup')+'

    QR loading...
    Use secret key below
    Disable action prompts for master password.
    '; - } - function renderTotpDisableModal(){ - if(!state.totpDisableOpen) return ''; - return '' - + '

    '+t('disableTotp')+'

    '+t('totpDisableSub')+'
    ' - + (state.totpDisableError?'
    '+esc(state.totpDisableError)+'
    ':'') - + '
    ' - + '
    '; - } - - function renderHelpTab(){ - return '' - + '

    '+t('help')+'

    ' - + '

    '+t('helpSync')+'

    • '+t('helpSync1')+'
    • '+t('helpSync2')+'
    • '+t('helpSync3')+'
    ' - + '

    '+t('helpErr')+'

    • '+t('helpErr1')+'
    • '+t('helpErr2')+'
    • '+t('helpErr3')+'
    • '+t('helpErr4')+'
    ' - + '

    '+t('helpTb')+'

    • '+t('helpTb1')+'
    • '+t('helpTb2')+'
    • '+t('helpTb3')+'
    • '+t('helpTb4')+'
    '; - } - - function renderAdminTab(){ - var usersRows=''; - for(var i=0;i'+esc(u.name||'')+''+esc(u.role)+''+esc(u.status)+'' - + (canAct?'':'') - + (canAct?' ':'') - + ''; - } - if(!usersRows) usersRows='No users found.'; - - var inviteRows=''; - for(var j=0;j'+esc(inv.status)+''+esc(inv.expiresAt)+'' - + '' - + (inv.status==='active'?' ':'') - + ''; - } - if(!inviteRows) inviteRows='No invites found.'; - - return '' - + renderMsg() - + '
    ' - + '

    '+t('admin')+'

    ' - + '' - + '
    ' - + '

    '+t('createInvite')+'

    ' - + '

    '+t('users')+'

    '+usersRows+'
    '+t('email')+''+t('name')+''+t('role')+''+t('status')+''+t('action')+'
    ' - + '

    '+t('invites')+'

    '+inviteRows+'
    Code'+t('status')+'Expires At'+t('action')+'
    '; - } - - function renderApp(){ - var isAdmin=state.profile&&state.profile.role==='admin'; - var showFolders=state.tab==='vault'; - var folders='' - + '' - + ''; - for(var i=0;i📁'+esc(folderName)+''; } - var typeTree='' - + '' - + '' - + '' - + '' - + '' - + ''; - var content = state.tab==='vault'?renderVaultTab():state.tab==='settings'?renderSettingsTab():(state.tab==='admin'&&isAdmin)?renderAdminTab():renderHelpTab(); - - return '' - + '' - + '
    ' - + (showFolders?(' '):'') - + '
    '+content+'
    ' - + '
    '+renderTotpDisableModal(); - } - - function render(){ - var active = document.activeElement; - var keepSearchFocus = false; - var keepSearchSelStart = 0; - var keepSearchSelEnd = 0; - if (active instanceof HTMLInputElement && active.getAttribute('data-action') === 'vault-search') { - keepSearchFocus = true; - keepSearchSelStart = active.selectionStart == null ? 0 : active.selectionStart; - keepSearchSelEnd = active.selectionEnd == null ? keepSearchSelStart : active.selectionEnd; - } - if(state.phase==='loading'){ app.innerHTML='
    '+t('loading')+'
    ' + renderToasts() + renderDialog(); return; } - if(state.phase==='register'){ app.innerHTML=renderRegisterScreen() + renderToasts() + renderDialog(); return; } - if(state.phase==='login'){ app.innerHTML=renderLoginScreen() + renderToasts() + renderDialog(); return; } - if(state.phase==='locked'){ app.innerHTML=renderLockedScreen() + renderToasts() + renderDialog(); return; } - var prevContent = app.querySelector('.content'); - var prevSidebar = app.querySelector('.sidebar'); - var prevVaultList = app.querySelector('.vault-list'); - var contentTop = prevContent ? prevContent.scrollTop : 0; - var sidebarTop = prevSidebar ? prevSidebar.scrollTop : 0; - var vaultListTop = prevVaultList ? prevVaultList.scrollTop : 0; - app.innerHTML=renderApp() + renderToasts() + renderDialog(); - var nextContent = app.querySelector('.content'); - var nextSidebar = app.querySelector('.sidebar'); - var nextVaultList = app.querySelector('.vault-list'); - if(nextContent) nextContent.scrollTop = contentTop; - if(nextSidebar) nextSidebar.scrollTop = sidebarTop; - if(nextVaultList) nextVaultList.scrollTop = vaultListTop; - if (keepSearchFocus) { - var nextSearch = app.querySelector('input[data-action="vault-search"]'); - if (nextSearch instanceof HTMLInputElement) { - nextSearch.focus(); - try { nextSearch.setSelectionRange(keepSearchSelStart, keepSearchSelEnd); } catch (_) {} - } - } - updateLiveTotpDisplay(); - renderTotpSetupQr(); - } - - async function init(){ - var url=new URL(window.location.href); state.inviteCode=(url.searchParams.get('invite')||'').trim(); state.session=loadSession(); - loadLockSettings(); - ensureTotpTicker(); - ensureLockChannel(); - ensureAutoLockTicker(); - var st=await fetch('/setup/status'); var setup=await jsonOrNull(st); var registered=!!(setup&&setup.registered); - if(state.session){ - try{ await loadProfile(); state.phase='locked'; state.tab='vault'; render(); return; } catch(e){ state.session=null; saveSession(); } - } - state.phase=registered?'login':'register'; render(); - } - - async function onRegister(form){ - clearMsg(); - var fd=new FormData(form); var name=String(fd.get('name')||'').trim(); var email=String(fd.get('email')||'').trim().toLowerCase(); var p=String(fd.get('password')||''); var p2=String(fd.get('password2')||''); var invite=String(fd.get('inviteCode')||'').trim(); - state.registerName=name; state.registerEmail=email; state.registerPassword=p; state.registerPassword2=p2; state.inviteCode=invite; - if(!email||!p) return setMsg('Please input email and password.', 'err'); - if(p.length<12) return setMsg('Master password must be at least 12 chars.', 'err'); - if(p!==p2) return setMsg('Passwords do not match.', 'err'); - try{ - var it=defaultKdfIterations; var mk=await pbkdf2(p,email,it,32); var hash=await pbkdf2(mk,p,1,32); var ek=await hkdfExpand(mk,'enc',32); var em=await hkdfExpand(mk,'mac',32); var sym=crypto.getRandomValues(new Uint8Array(64)); var encKey=await encryptBw(sym,ek,em); - var kp=await crypto.subtle.generateKey({name:'RSA-OAEP', modulusLength:2048, publicExponent:new Uint8Array([1,0,1]), hash:'SHA-1'}, true, ['encrypt','decrypt']); - var pub=new Uint8Array(await crypto.subtle.exportKey('spki',kp.publicKey)); var prv=new Uint8Array(await crypto.subtle.exportKey('pkcs8',kp.privateKey)); var encPrv=await encryptBw(prv,sym.slice(0,32),sym.slice(32,64)); - var resp=await fetch('/api/accounts/register',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({email:email,name:name,masterPasswordHash:bytesToBase64(hash),key:encKey,kdf:0,kdfIterations:it,inviteCode:invite||undefined,keys:{publicKey:bytesToBase64(pub),encryptedPrivateKey:encPrv}})}); - var j=await jsonOrNull(resp); if(!resp.ok) return setMsg((j&&(j.error||j.error_description))||'Register failed.', 'err'); - state.registerName=''; state.registerEmail=''; state.registerPassword=''; state.registerPassword2=''; state.inviteCode=''; - state.phase='login'; state.loginEmail=email; state.loginPassword=''; setMsg('Registration succeeded. Please sign in.', 'ok'); - }catch(e){ setMsg(e&&e.message?e.message:String(e), 'err'); } - } - - async function onLoginPassword(form){ - clearMsg(); - var fd=new FormData(form); state.loginEmail=String(fd.get('email')||'').trim().toLowerCase(); state.loginPassword=String(fd.get('password')||''); - if(!state.loginEmail||!state.loginPassword) return setMsg('Please input email and password.', 'err'); - try{ - var d=await deriveLoginHash(state.loginEmail,state.loginPassword); - var body=new URLSearchParams(); body.set('grant_type','password'); body.set('username',state.loginEmail); body.set('password',d.hash); body.set('scope','api offline_access'); - var resp=await fetch('/identity/connect/token',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:body.toString()}); - var j=await jsonOrNull(resp); - if(!resp.ok){ - if(j&&j.TwoFactorProviders){ state.pendingLogin={email:state.loginEmail,passwordHash:d.hash,masterKey:d.masterKey}; state.loginTotpToken=''; state.loginTotpError=''; clearMsg(); render(); return; } - return setMsg((j&&(j.error_description||j.error))||'Login failed.', 'err'); - } - await onLoginSuccess(j,d.masterKey,state.loginEmail,state.loginPassword); - }catch(e){ setMsg(e&&e.message?e.message:String(e), 'err'); } - } - - async function onLoginTotp(form){ - if(!state.pendingLogin) return setMsg('TOTP flow is not ready.', 'err'); - var fd=new FormData(form); state.loginTotpToken=String(fd.get('totpToken')||'').trim(); if(!state.loginTotpToken){ state.loginTotpError='Please input TOTP code.'; render(); return; } - var b=new URLSearchParams(); b.set('grant_type','password'); b.set('username',state.pendingLogin.email); b.set('password',state.pendingLogin.passwordHash); b.set('scope','api offline_access'); b.set('twoFactorProvider','0'); b.set('twoFactorToken',state.loginTotpToken); - var resp=await fetch('/identity/connect/token',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:b.toString()}); - var j=await jsonOrNull(resp); if(!resp.ok){ state.loginTotpError=(j&&(j.error_description||j.error))||'TOTP verification failed.'; render(); return; } - state.loginTotpError=''; - await onLoginSuccess(j,state.pendingLogin.masterKey,state.pendingLogin.email,state.loginPassword); - } - - async function onLoginSuccess(tokenJson, masterKey, email, password){ - state.session={accessToken:tokenJson.access_token,refreshToken:tokenJson.refresh_token,email:email}; saveSession(); state.pendingLogin=null; state.loginTotpToken=''; state.loginTotpError=''; - await loadProfile(); - try{ - var ek=await hkdfExpand(masterKey,'enc',32); var em=await hkdfExpand(masterKey,'mac',32); - var symKeyBytes=await decryptBw(state.profile.key,ek,em); - if(symKeyBytes){ state.session.symEncKey=bytesToBase64(symKeyBytes.slice(0,32)); state.session.symMacKey=bytesToBase64(symKeyBytes.slice(32,64)); } - }catch(e){ console.warn('Key derivation failed:',e); } - await loadVault(); await loadAdminData(); state.phase='app'; state.tab='vault'; state.lockLastActiveTs=Date.now(); - setMsg('Login success.', 'ok'); - } - async function onSaveLockSettings(form){ - var fd=new FormData(form); - var mins=Number(fd.get('lockTimeoutMinutes')||0); - if(!Number.isFinite(mins)||mins<0) mins=15; - state.lockTimeoutMinutes=mins; - saveLockSettings(); - state.lockLastActiveTs=Date.now(); - setMsg('Lock settings saved.', 'ok'); - } - async function onSaveProfile(form){ var fd=new FormData(form); var n=String(fd.get('name')||'').trim(); var em=String(fd.get('email')||'').trim().toLowerCase(); var r=await authFetch('/api/accounts/profile',{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify({name:n,email:em})}); var j=await jsonOrNull(r); if(!r.ok) return setMsg((j&&(j.error||j.error_description))||'Save profile failed.', 'err'); state.profile=j; render(); setMsg('Profile updated.', 'ok'); } - async function onChangePassword(form){ - var fd=new FormData(form); - var currentPassword=String(fd.get('currentPassword')||''); - var newPassword=String(fd.get('newPassword')||''); - var newPassword2=String(fd.get('newPassword2')||''); - if(!currentPassword||!newPassword) return setMsg('Current/new password is required.', 'err'); - if(newPassword.length<12) return setMsg('New master password must be at least 12 chars.', 'err'); - if(newPassword!==newPassword2) return setMsg('New passwords do not match.', 'err'); - if(newPassword===currentPassword) return setMsg('New password must be different.', 'err'); - var email=String(state.profile&&state.profile.email?state.profile.email:'').toLowerCase(); - if(!email) return setMsg('Profile email missing.', 'err'); - try{ - var current=await deriveLoginHash(email,currentPassword); - var userSym=buildSymmetricKeyBytes(); - if(!userSym){ - var oldEk=await hkdfExpand(current.masterKey,'enc',32); - var oldEm=await hkdfExpand(current.masterKey,'mac',32); - userSym=await decryptBw(state.profile.key,oldEk,oldEm); - } - if(!userSym||userSym.length<64) return setMsg('Unable to load vault key for password rotation.', 'err'); - var nextMasterKey=await pbkdf2(newPassword,email,current.kdfIterations,32); - var nextHash=await pbkdf2(nextMasterKey,newPassword,1,32); - var nextEk=await hkdfExpand(nextMasterKey,'enc',32); - var nextEm=await hkdfExpand(nextMasterKey,'mac',32); - var newKey=await encryptBw(userSym.slice(0,64),nextEk,nextEm); - var r=await authFetch('/api/accounts/password',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({currentPasswordHash:current.hash,newMasterPasswordHash:bytesToBase64(nextHash),newKey:newKey,kdf:0,kdfIterations:current.kdfIterations})}); - var j=await jsonOrNull(r); - if(!r.ok) return setMsg((j&&(j.error||j.error_description))||'Change master password failed.', 'err'); - logout(); - setMsg('Master password changed. Please log in again.', 'ok'); - }catch(e){ - setMsg('Change master password failed: '+(e&&e.message?e.message:String(e)), 'err'); - } - } - async function onEnableTotp(form){ var fd=new FormData(form); state.totpSetupSecret=String(fd.get('secret')||'').toUpperCase().replace(/[\s-]/g,'').replace(/=+$/g,''); state.totpSetupToken=String(fd.get('token')||'').trim(); if(!state.totpSetupSecret) return setMsg('TOTP secret is required.', 'err'); if(!state.totpSetupToken) return setMsg('TOTP token is required.', 'err'); var r=await authFetch('/api/accounts/totp',{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify({enabled:true,secret:state.totpSetupSecret,token:state.totpSetupToken})}); var j=await jsonOrNull(r); if(!r.ok) return setMsg((j&&(j.error||j.error_description))||'Enable TOTP failed.', 'err'); state.totpSetupToken=''; render(); setMsg('TOTP enabled.', 'ok'); } - function onDisableTotp(){ state.totpDisableOpen=true; state.totpDisablePassword=''; state.totpDisableError=''; render(); } - async function onDisableTotpSubmit(form){ - var fd=new FormData(form); state.totpDisablePassword=String(fd.get('masterPassword')||''); - if(!state.totpDisablePassword){ state.totpDisableError='Please input master password.'; render(); return; } - try{ - var d=await deriveLoginHash(state.profile.email,state.totpDisablePassword); - var r=await authFetch('/api/accounts/totp',{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify({enabled:false,masterPasswordHash:d.hash})}); - var j=await jsonOrNull(r); - if(!r.ok){ state.totpDisableError=(j&&(j.error||j.error_description))||'Disable TOTP failed.'; render(); return; } - state.totpDisableOpen=false; state.totpDisablePassword=''; state.totpDisableError=''; - render(); setMsg('TOTP disabled.', 'ok'); - }catch(e){ - state.totpDisableError='Disable TOTP failed: '+(e&&e.message?e.message:String(e)); - render(); - } - } - - async function onBulkDelete(){ var ids=[]; for(var k in state.selectedMap){ if(state.selectedMap[k]) ids.push(k);} if(ids.length===0) return setMsg('Select items first.', 'err'); var ok=await askConfirm({title:'Delete items',message:'Are you sure you want to delete '+ids.length+' selected items?',okText:'Yes',cancelText:'No',danger:true}); if(!ok) return; for(var i=0;i=0&&wi=0&&fi=0&&wi { - const s = document.createElement('script'); - s.src = '/web/vendor/qrcode-generator.min.js'; - s.async = true; - s.onload = () => resolve(null); - s.onerror = () => resolve(null); - document.head.appendChild(s); - }); -} - -async function loadRuntimeConfig() { - try { - const resp = await fetch('/api/web/config', { method: 'GET' }); - if (!resp.ok) throw new Error('runtime config request failed'); - return await resp.json(); - } catch { - return { defaultKdfIterations: 600000 }; - } -} - -await ensureQrLibrary(); -const cfg = await loadRuntimeConfig(); -startNodewardenApp(cfg || { defaultKdfIterations: 600000 }); diff --git a/public/web/styles.css b/public/web/styles.css deleted file mode 100644 index 324f78b..0000000 --- a/public/web/styles.css +++ /dev/null @@ -1,812 +0,0 @@ - -:root { - --bg: #F3F5F8; - --panel: #FFFFFF; - --line: #DEE2E6; - --text-primary: #212529; - --text-secondary: #6C757D; - --primary: #175DDC; - --primary-hover: #144eb8; - --danger: #DC3545; - --danger-hover: #C82333; - --danger-bg: #F8D7DA; - --success: #198754; - --success-bg: #D1E7DD; - --border-color: #DEE2E6; - --radius: 6px; - --radius-sm: 4px; - --shadow-sm: 0 1px 2px rgba(0,0,0,0.05); - --shadow: 0 4px 12px rgba(0,0,0,0.08); - --shadow-lg: 0 8px 24px rgba(0,0,0,0.12); - --font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; - } - * { box-sizing: border-box; } - html, body { height: 100%; margin: 0; } - body { - color: var(--text-primary); - font-family: var(--font-sans); - background-color: var(--bg); - -webkit-font-smoothing: antialiased; - } - #app { height: 100%; display: flex; flex-direction: column; } - - /* Auth Pages */ - .auth-page { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - min-height: 100vh; - padding: 20px; - position: relative; - } - .lang-switch { - position: absolute; - top: 24px; - right: 24px; - cursor: pointer; - color: var(--text-secondary); - font-size: 14px; - font-weight: 500; - } - .lang-switch:hover { color: var(--primary); } - .auth-card { - width: 100%; - max-width: 420px; - background: var(--panel); - border: 1px solid var(--border-color); - border-radius: var(--radius); - padding: 40px; - box-shadow: var(--shadow); - } - .auth-header { - text-align: center; - margin-bottom: 32px; - } - .auth-logo { - width: 48px; - height: 48px; - background: var(--primary); - border-radius: 12px; - margin: 0 auto 16px; - display: flex; - align-items: center; - justify-content: center; - color: white; - font-weight: bold; - font-size: 24px; - } - .auth-logo::after { content: "NW"; } - .auth-title { - font-size: 24px; - font-weight: 700; - color: var(--text-primary); - margin-bottom: 8px; - } - .auth-subtitle { - color: var(--text-secondary); - font-size: 15px; - } - .auth-footer { - margin-top: 24px; - text-align: center; - font-size: 14px; - } - .auth-footer a { - color: var(--primary); - text-decoration: none; - font-weight: 500; - } - .auth-footer a:hover { text-decoration: underline; } - - /* Forms */ - .form-group { margin-bottom: 20px; } - .form-label { - display: block; - margin-bottom: 8px; - font-size: 14px; - font-weight: 600; - color: var(--text-primary); - } - .form-input { - width: 100%; - height: 42px; - padding: 8px 12px; - font-size: 15px; - border: 1px solid var(--border-color); - border-radius: var(--radius-sm); - background: #fff; - color: var(--text-primary); - transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; - } - .form-input:focus { - outline: none; - border-color: var(--primary); - box-shadow: 0 0 0 3px rgba(23, 93, 220, 0.15); - } - - /* Buttons */ - .btn { - display: inline-flex; - align-items: center; - justify-content: center; - height: 42px; - padding: 0 20px; - font-size: 15px; - font-weight: 600; - border-radius: var(--radius-sm); - border: 1px solid transparent; - cursor: pointer; - transition: all 0.15s ease-in-out; - } - .btn-primary { - background: var(--primary); - color: #fff; - } - .btn-primary:hover { background: var(--primary-hover); } - .btn-secondary { - background: #fff; - border-color: var(--border-color); - color: var(--text-primary); - } - .btn-secondary:hover { background: #F8F9FA; } - .btn-danger { - background: var(--danger); - color: #fff; - } - .btn-danger:hover { background: var(--danger-hover); } - - /* Alerts */ - .alert { - padding: 12px 16px; - border-radius: var(--radius-sm); - font-size: 14px; - margin-bottom: 24px; - border: 1px solid transparent; - } - .alert-success { background: var(--success-bg); color: var(--success); border-color: #BADBCC; } - .alert-danger { background: var(--danger-bg); color: var(--danger); border-color: #F5C2C7; } - .toast-stack { - position: fixed; - top: 16px; - right: 16px; - z-index: 1200; - list-style: none; - margin: 0; - padding: 0; - display: flex; - flex-direction: column; - gap: 10px; - width: min(420px, calc(100vw - 24px)); - } - .toast-item { - position: relative; - display: flex; - align-items: center; - justify-content: space-between; - gap: 10px; - border-radius: 10px; - box-shadow: var(--shadow); - border: 1px solid #c9e9d6; - background: #dff4e5; - color: #0f5132; - padding: 14px 14px; - overflow: hidden; - } - .toast-item.error { - border-color: #f5c2c7; - background: #f8d7da; - color: #842029; - } - .toast-item.warning { - border-color: #ffe69c; - background: #fff3cd; - color: #664d03; - } - .toast-text { - font-size: 15px; - font-weight: 600; - padding-right: 10px; - } - .toast-close { - border: none; - background: transparent; - color: inherit; - font-size: 22px; - cursor: pointer; - line-height: 1; - opacity: 0.8; - } - .toast-close:hover { opacity: 1; } - .toast-bar { - position: absolute; - left: 0; - bottom: 0; - height: 3px; - width: 100%; - background: rgba(0,0,0,0.12); - transform-origin: left center; - animation: toastBar 4.5s linear forwards; - } - @keyframes toastBar { from { transform: scaleX(1); } to { transform: scaleX(0); } } - .dialog-mask { - position: fixed; - inset: 0; - background: rgba(17, 24, 39, 0.45); - z-index: 1300; - display: flex; - align-items: center; - justify-content: center; - padding: 20px; - } - .dialog-card { - width: min(540px, 100%); - background: #fff; - border: 1px solid var(--border-color); - border-radius: 20px; - box-shadow: var(--shadow-lg); - padding: 24px 24px; - text-align: center; - } - .dialog-icon { - font-size: 34px; - line-height: 1; - color: #f4b400; - margin-bottom: 12px; - } - .dialog-title { - margin: 0 0 8px 0; - font-size: 34px; - line-height: 1.15; - color: #0f172a; - font-weight: 700; - } - .dialog-msg { - margin: 0 auto 18px auto; - color: #334155; - font-size: 20px; - max-width: 90%; - } - .dialog-btn { - width: 100%; - height: 56px; - border-radius: 999px; - font-size: 28px; - margin-bottom: 10px; - } - .form-dialog { - text-align: left; - } - .form-dialog .dialog-title { - font-size: 30px; - margin-bottom: 8px; - text-align: center; - } - .form-dialog .dialog-msg { - font-size: 16px; - max-width: 100%; - margin-bottom: 14px; - text-align: center; - } - .form-dialog .dialog-btn { - font-size: 22px; - } - .dialog-error { - background: #f8d7da; - border: 1px solid #f5c2c7; - color: #842029; - border-radius: 10px; - padding: 10px 12px; - font-size: 14px; - margin: 0 0 12px 0; - } - .unlock-card { - max-width: 620px; - padding: 30px 34px; - } - .unlock-pwd-wrap { - position: relative; - margin-bottom: 14px; - } - .unlock-pwd-input { - padding-right: 88px; - height: 48px; - border-radius: 10px; - border-color: #3f5b9e; - } - .auth-page .form-input { - height: 48px; - border-radius: 10px; - border-color: #3f5b9e; - padding: 10px 12px; - } - .auth-page .form-input:focus { - border-color: #3f5b9e; - box-shadow: none; - } - .unlock-eye-btn { - position: absolute; - right: 42px; - bottom: 8px; - width: 30px; - height: 30px; - border: none; - background: transparent; - color: #233a72; - font-size: 17px; - cursor: pointer; - } - .unlock-main-btn { - width: 100%; - margin-top: 8px; - height: 44px; - border-radius: 999px; - } - .unlock-secondary-btn { - width: 100%; - height: 44px; - border-radius: 999px; - border-color: var(--primary); - color: var(--primary); - background: #fff; - } - .unlock-or { - text-align: center; - color: #1f2f4f; - font-size: 16px; - margin: 10px 0; - line-height: 1; - } - .totp-qr-card { - background:#fff; - padding:16px; - border:1px solid var(--border-color); - border-radius:8px; - width: 200px; - min-height: 200px; - display:flex; - align-items:center; - justify-content:center; - } - .totp-qr-fallback { - width:100%; - min-height:168px; - border:1px dashed var(--border-color); - border-radius:8px; - display:flex; - flex-direction:column; - align-items:center; - justify-content:center; - color:var(--text-secondary); - text-align:center; - padding:8px; - } - - /* App Layout */ - .navbar { - height: 64px; - background: var(--primary); - color: #fff; - display: flex; - align-items: center; - justify-content: space-between; - padding: 0 24px; - flex-shrink: 0; - } - .nav-brand { - display: flex; - align-items: center; - gap: 12px; - font-size: 20px; - font-weight: 700; - } - .nav-logo { - width: 32px; - height: 32px; - background: #fff; - border-radius: 8px; - display: flex; - align-items: center; - justify-content: center; - color: var(--primary); - font-weight: bold; - font-size: 16px; - } - .nav-logo::after { content: "NW"; } - .nav-links { - display: flex; - gap: 8px; - } - .nav-link { - color: rgba(255,255,255,0.8); - text-decoration: none; - padding: 8px 16px; - border-radius: var(--radius-sm); - font-weight: 500; - font-size: 15px; - transition: all 0.15s; - } - .nav-link:hover { color: #fff; background: rgba(255,255,255,0.1); } - .nav-link.active { color: #fff; background: rgba(255,255,255,0.2); } - .nav-user { - display: flex; - align-items: center; - } - .nav-user .lang-switch { - color: rgba(255,255,255,0.8); - } - .nav-user .lang-switch:hover { color: #fff; } - .nav-user .btn-secondary { - height: 32px; - padding: 0 12px; - font-size: 13px; - background: rgba(255,255,255,0.1); - border-color: transparent; - color: #fff; - } - .nav-user .btn-secondary:hover { background: rgba(255,255,255,0.2); } - - .app-body { - display: flex; - flex: 1; - overflow: hidden; - width: min(1520px, calc(100vw - 40px)); - margin: 14px auto 16px; - border: 1px solid var(--border-color); - border-radius: 12px; - background: #fff; - box-shadow: var(--shadow); - height: calc(100vh - 64px - 30px); - } - .sidebar { - width: 300px; - background: #fff; - border-right: 1px solid var(--border-color); - padding: 14px; - overflow-y: auto; - } - .sidebar-block { - border: 1px solid var(--border-color); - border-radius: var(--radius); - padding: 10px; - background: #fff; - margin-bottom: 12px; - } - .sidebar-title { - font-size: 12px; - font-weight: 700; - color: var(--text-secondary); - text-transform: uppercase; - letter-spacing: 0.05em; - margin-bottom: 8px; - } - .search-input { - width: 100%; - height: 38px; - border: 1px solid #2f6fec; - border-radius: 9px; - padding: 0 12px; - font-size: 15px; - outline: none; - } - .tree-btn { - width: 100%; - text-align: left; - padding: 8px 10px; - background: transparent; - border: none; - color: var(--text-primary); - font-size: 14px; - font-weight: 500; - border-radius: var(--radius-sm); - cursor: pointer; - margin-bottom: 2px; - display: flex; - align-items: center; - gap: 8px; - } - .tree-btn:hover { background: var(--bg); } - .tree-btn.active { color: var(--primary); font-weight: 700; background: #eef4ff; } - - .content { - flex: 1; - padding: 16px 18px; - overflow-y: auto; - background: #F8FAFC; - } - .content .btn { - height: 36px; - padding: 0 16px; - border-radius: 15px; - } - .content .btn-primary { - background: var(--primary); - border-color: var(--primary); - color: #fff; - } - .content .btn-primary:hover { background: var(--primary-hover); border-color: var(--primary-hover); } - .content .btn-secondary { - background: #fff; - border-color: var(--primary); - color: var(--primary); - } - .content .btn-secondary:hover { background: #edf3ff; } - .content .btn-danger { - background: #fff; - border-color: #e11d48; - color: #e11d48; - } - .content .btn-danger:hover { background: #fff1f2; } - .content .btn-danger-icon { - width: 42px; - padding: 0; - border: none; - background: transparent; - color: #e11d48; - font-size: 26px; - line-height: 1; - } - .content .btn-danger-icon:hover { - border: 1px solid #fecdd3; - background: #fff1f2; - } - - /* Vault Grid */ - .vault-grid { - display: grid; - grid-template-columns: minmax(380px, 44%) 1fr; - gap: 16px; - height: calc(100vh - 145px); - } - .vault-list-col { min-width: 0; display:flex; flex-direction:column; } - .vault-list-head { - display: flex; - gap: 8px; - flex-wrap: wrap; - margin-bottom: 8px; - justify-content: flex-start; - align-items: center; - } - .vault-list { - background: #fff; - border: 1px solid var(--border-color); - border-radius: var(--radius); - overflow-y: auto; - flex: 1; - } - .vault-item { - padding: 13px 14px; - border-bottom: 1px solid var(--border-color); - display: flex; - align-items: center; - gap: 10px; - cursor: pointer; - background: #fff; - } - .vault-item:hover { background: #f6f9ff; } - .vault-item.active { background: #ecf3ff; } - .vault-item:last-child { border-bottom: none; } - .vault-item-check { width:18px; height:18px; } - .vault-item-main { display:flex; align-items:center; gap:12px; min-width:0; } - .vault-item-icon { - width: 24px; - height: 24px; - border-radius: 5px; - object-fit: contain; - flex-shrink: 0; - background: #fff; - } - .vault-item-icon-wrap { width:24px; height:24px; position:relative; flex-shrink:0; display:inline-flex; align-items:center; justify-content:center; } - .vault-item-icon-fallback { - display:inline-flex; - align-items:center; - justify-content:center; - font-size: 24px; - color: #6b7a90; - border: none; - background: transparent; - } - .vault-item-text { min-width:0; } - .vault-item-title { - color: #1457d6; - font-size: 16px; - font-weight: 700; - line-height: 1.1; - white-space: nowrap; - text-overflow: ellipsis; - overflow: hidden; - } - .vault-item-sub { - color: #64748b; - font-size: 13px; - margin-top: 4px; - white-space: nowrap; - text-overflow: ellipsis; - overflow: hidden; - } - .vault-detail { - overflow-y: auto; - padding-right: 2px; - } - .card { - background: #fff; - border: 1px solid var(--border-color); - border-radius: 14px; - padding: 16px 18px; - margin-bottom: 14px; - box-shadow: var(--shadow-sm); - } - .vault-detail-head .vault-detail-title { font-size: 24px; font-weight: 700; margin-bottom: 8px; } - .vault-detail-folder { color: #334155; font-size: 14px; } - .card-title { - font-size: 15px; - font-weight: 700; - color: #334155; - margin-bottom: 10px; - } - .field-row { - display: flex; - justify-content: space-between; - align-items: center; - gap: 10px; - padding: 10px 0; - border-bottom: 1px solid #e8edf4; - } - .field-row:last-child { border-bottom: none; } - .field-label { color: #64748b; font-size: 13px; margin-bottom: 3px; } - .field-value { color: #0f172a; font-size: 17px; word-break: break-all; } - .field-sub { color: #64748b; font-size: 12px; margin-top: 2px; } - .icon-btn { - border: 1px solid var(--border-color); - background: #fff; - border-radius: 8px; - height: 30px; - padding: 0 10px; - font-size: 12px; - font-weight: 600; - cursor: pointer; - margin-left: 6px; - } - .icon-btn:hover { background: #f8fafc; } - .link-btn { - border: none; - background: transparent; - color: #1457d6; - font-weight: 700; - font-size: 16px; - cursor: pointer; - padding: 2px 0; - } - .link-btn:hover { text-decoration: underline; } - .create-menu-wrap { position: relative; } - .create-menu { - position: absolute; - top: calc(100% + 4px); - left: 0; - min-width: 180px; - background: #fff; - border: 1px solid var(--border-color); - border-radius: 10px; - box-shadow: var(--shadow); - z-index: 30; - padding: 6px; - } - .create-menu-item { - width: 100%; - text-align: left; - border: none; - background: transparent; - padding: 8px 10px; - border-radius: 8px; - font-size: 15px; - cursor: pointer; - } - .create-menu-item:hover { background: #eff5ff; } - .field-modal { max-width: 560px; } - .field-modal-head { display:flex; justify-content:space-between; align-items:center; margin-bottom: 8px; } - .field-modal-head h3 { margin: 0; } - .history-line { color: #64748b; font-size: 13px; line-height: 1.8; } - .detail-input { - width: 100%; - min-height: 36px; - border: 1px solid var(--border-color); - border-radius: 8px; - padding: 7px 10px; - font-size: 14px; - background: #fff; - color: #0f172a; - } - .detail-input:focus { outline: none; border-color: #2f6fec; box-shadow: 0 0 0 3px rgba(47,111,236,0.12); } - .detail-textarea { min-height: 100px; resize: vertical; } - .detail-actions { - display: flex; - justify-content: space-between; - align-items: center; - margin-top: 8px; - gap: 8px; - } - .detail-actions .btn { margin-right: 8px; } - .detail-actions .btn:last-child { margin-right: 0; } - .vault-empty { - min-height: 120px; - display: flex; - align-items: center; - justify-content: center; - color: var(--text-secondary); - padding: 20px; - text-align: center; - background: #fff; - border: 1px solid var(--border-color); - border-radius: var(--radius); - } - - /* Common Components */ - .panel { - background: #fff; - border: 1px solid var(--border-color); - border-radius: var(--radius); - padding: 24px; - margin-bottom: 24px; - box-shadow: var(--shadow-sm); - } - .panel h3 { margin: 0 0 20px 0; font-size: 18px; font-weight: 600; border-bottom: 1px solid var(--border-color); padding-bottom: 16px; } - - .table { width: 100%; border-collapse: collapse; font-size: 14px; } - .table th, .table td { padding: 12px 16px; border-bottom: 1px solid var(--border-color); text-align: left; } - .table th { font-weight: 600; color: var(--text-secondary); background: var(--bg); } - - .badge { - padding: 4px 8px; - border-radius: 4px; - font-size: 12px; - font-weight: 600; - background: var(--bg); - color: var(--text-secondary); - } - .badge.success { background: var(--success-bg); color: var(--success); } - .badge.danger { background: var(--danger-bg); color: var(--danger); } - - .kv { margin-bottom: 12px; font-size: 14px; line-height: 1.5; display: flex; } - .kv b { color: var(--text-secondary); font-weight: 600; width: 120px; flex-shrink: 0; } - - .totp-mask { - position: fixed; - inset: 0; - background: rgba(0,0,0,0.5); - display: flex; - align-items: center; - justify-content: center; - z-index: 1000; - } - .totp-box { - width: 100%; - max-width: 400px; - background: #fff; - border-radius: var(--radius); - padding: 32px; - box-shadow: var(--shadow-lg); - } - @media (max-width: 980px) { - .app-body { - width: calc(100vw - 16px); - margin: 8px auto; - border-radius: 10px; - } - .sidebar { width: 280px; } - } - @media (max-width: 760px) { - .app-body { - width: 100%; - margin: 0; - border: none; - border-radius: 0; - box-shadow: none; - height: calc(100vh - 64px); - } - .sidebar { - width: 100%; - border-right: none; - border-bottom: 1px solid var(--border-color); - } - .vault-grid { grid-template-columns: 1fr; } - } diff --git a/public/web/vault-utils.js b/public/web/vault-utils.js deleted file mode 100644 index 8aafd4d..0000000 --- a/public/web/vault-utils.js +++ /dev/null @@ -1,44 +0,0 @@ -export function parseFieldType(v) { - if (v === null || v === undefined) return 0; - if (typeof v === 'number' && isFinite(v)) return v === 1 || v === 2 || v === 3 ? v : 0; - var s = String(v).trim().toLowerCase(); - if (s === '1' || s === 'hidden') return 1; - if (s === '2' || s === 'boolean' || s === 'checkbox') return 2; - if (s === '3' || s === 'linked' || s === 'link') return 3; - return 0; -} - -export function selectedCount(selectedMap) { - var n = 0; - for (var k in selectedMap) if (selectedMap[k]) n++; - return n; -} - -export function cipherTypeKey(c) { - var tnum = Number(c && c.type || 1); - if (tnum === 1) return 'login'; - if (tnum === 3) return 'card'; - if (tnum === 4) return 'identity'; - if (tnum === 2) return 'note'; - return 'other'; -} - -export function hostFromUri(uri) { - if (!uri) return ''; - try { - var normalized = /^https?:\/\//i.test(uri) ? uri : ('https://' + uri); - return new URL(normalized).hostname || ''; - } catch (_) { - return ''; - } -} - -export function firstCipherUri(c) { - var uris = c && c.login && Array.isArray(c.login.uris) ? c.login.uris : []; - for (var i = 0; i < uris.length; i++) { - var u = uris[i] && (uris[i].decUri || uris[i].uri); - if (u) return u; - } - return ''; -} - diff --git a/public/web/vendor/qrcode-generator.min.js b/public/web/vendor/qrcode-generator.min.js deleted file mode 100644 index 1434c6f..0000000 --- a/public/web/vendor/qrcode-generator.min.js +++ /dev/null @@ -1 +0,0 @@ -var qrcode=function(){function i(t,r){function a(t,r){g=function(t){for(var r=new Array(t),e=0;e>e&1);g[Math.floor(e/3)][e%3+l-8-3]=n}for(e=0;e<18;e+=1){n=!t&&1==(r>>e&1);g[e%3+l-8-3][Math.floor(e/3)]=n}},v=function(t,r){for(var e=f<<3|r,n=B.getBCHTypeInfo(e),o=0;o<15;o+=1){var i=!t&&1==(n>>o&1);o<6?g[o][8]=i:o<8?g[o+1][8]=i:g[l-15+o][8]=i}for(o=0;o<15;o+=1){i=!t&&1==(n>>o&1);o<8?g[8][l-o-1]=i:o<9?g[8][15-o-1+1]=i:g[8][15-o-1]=i}g[l-8][8]=!t},d=function(t,r){for(var e=-1,n=l-1,o=7,i=0,a=B.getMaskFunction(r),u=l-1;0>>o&1)),a(n,u-f)&&(c=!c),g[n][u-f]=c,-1==(o-=1)&&(i+=1,o=7)}if((n+=e)<0||l<=n){n-=e,e=-e;break}}},w=function(t,r,e){for(var n=b.getRSBlocks(t,r),o=M(),i=0;i8*u)throw"code length overflow. ("+o.getLengthInBits()+">"+8*u+")";for(o.getLengthInBits()+4<=8*u&&o.put(0,4);o.getLengthInBits()%8!=0;)o.putBit(!1);for(;!(o.getLengthInBits()>=8*u||(o.put(236,8),o.getLengthInBits()>=8*u));)o.put(17,8);return function(t,r){for(var e=0,n=0,o=0,i=new Array(r.length),a=new Array(r.length),u=0;u',e+="";for(var n=0;n";for(var o=0;o';e+=""}return e+="",e+=""},s.createSvgTag=function(t,r,e,n){var o={};"object"==typeof t&&(t=(o=t).cellSize,r=o.margin,e=o.alt,n=o.title),t=t||2,r=void 0===r?4*t:r,(e="string"==typeof e?{text:e}:e||{}).text=e.text||null,e.id=e.text?e.id||"qrcode-description":null,(n="string"==typeof n?{text:n}:n||{}).text=n.text||null,n.id=n.text?n.id||"qrcode-title":null;var i,a,u,f,c=s.getModuleCount()*t+2*r,g="";for(f="l"+t+",0 0,"+t+" -"+t+",0 0,-"+t+"z ",g+=''+p(n.text)+"":"",g+=e.text?''+p(e.text)+"":"",g+='',g+='":r+=">";break;case"&":r+="&";break;case'"':r+=""";break;default:r+=n}}return r};return s.createASCII=function(t,r){if((t=t||1)<2)return function(t){t=void 0===t?2:t;var r,e,n,o,i,a=1*s.getModuleCount()+2*t,u=t,f=a-t,c={"██":"█","█ ":"▀"," █":"▄"," ":" "},g={"██":"▀","█ ":"▀"," █":" "," ":" "},l="";for(r=0;r>>8),r.push(255&o)):r.push(a)}}return r}};var r,t,a=1,u=2,o=4,f=8,y={L:1,M:0,Q:3,H:2},e=0,n=1,c=2,g=3,l=4,h=5,s=6,v=7,B=(r=[[],[6,18],[6,22],[6,26],[6,30],[6,34],[6,22,38],[6,24,42],[6,26,46],[6,28,50],[6,30,54],[6,32,58],[6,34,62],[6,26,46,66],[6,26,48,70],[6,26,50,74],[6,30,54,78],[6,30,56,82],[6,30,58,86],[6,34,62,90],[6,28,50,72,94],[6,26,50,74,98],[6,30,54,78,102],[6,28,54,80,106],[6,32,58,84,110],[6,30,58,86,114],[6,34,62,90,118],[6,26,50,74,98,122],[6,30,54,78,102,126],[6,26,52,78,104,130],[6,30,56,82,108,134],[6,34,60,86,112,138],[6,30,58,86,114,142],[6,34,62,90,118,146],[6,30,54,78,102,126,150],[6,24,50,76,102,128,154],[6,28,54,80,106,132,158],[6,32,58,84,110,136,162],[6,26,54,82,110,138,166],[6,30,58,86,114,142,170]],(t={}).getBCHTypeInfo=function(t){for(var r=t<<10;0<=d(r)-d(1335);)r^=1335<>>=1;return r}var w=function(){for(var r=new Array(256),e=new Array(256),t=0;t<8;t+=1)r[t]=1<>>8)},writeBytes:function(t,r,e){r=r||0,e=e||t.length;for(var n=0;n>>7-t%8&1)},put:function(t,r){for(var e=0;e>>r-e-1&1))},getLengthInBits:function(){return n},putBit:function(t){var r=Math.floor(n/8);e.length<=r&&e.push(0),t&&(e[r]|=128>>>n%8),n+=1}};return o},x=function(t){var r=a,n=t,e={getMode:function(){return r},getLength:function(t){return n.length},write:function(t){for(var r=n,e=0;e+2>>8&255)+(255&n),t.put(n,13),e+=2}if(e=e.length){if(0==i)return-1;throw"unexpected end of file./"+i}var t=e.charAt(n);if(n+=1,"="==t)return i=0,-1;t.match(/^\s$/)||(o=o<<6|a(t.charCodeAt(0)),i+=6)}var r=o>>>i-8&255;return i-=8,r}},a=function(t){if(65<=t&&t<=90)return t-65;if(97<=t&&t<=122)return t-97+26;if(48<=t&&t<=57)return t-48+52;if(43==t)return 62;if(47==t)return 63;throw"c:"+t};return r},I=function(t,r,e){for(var n=function(t,r){var n=t,o=r,l=new Array(t*r),e={setPixel:function(t,r,e){l[r*n+t]=e},write:function(t){t.writeString("GIF87a"),t.writeShort(n),t.writeShort(o),t.writeByte(128),t.writeByte(0),t.writeByte(0),t.writeByte(0),t.writeByte(0),t.writeByte(0),t.writeByte(255),t.writeByte(255),t.writeByte(255),t.writeString(","),t.writeShort(0),t.writeShort(0),t.writeShort(n),t.writeShort(o),t.writeByte(0);var r=i(2);t.writeByte(2);for(var e=0;255>>r!=0)throw"length over";for(;8<=n+r;)e.writeByte(255&(t<>>=8-n,n=o=0;o|=t<>>o-6),o-=6},t.flush=function(){if(0>6,128|63&n):n<55296||57344<=n?r.push(224|n>>12,128|n>>6&63,128|63&n):(e++,n=65536+((1023&n)<<10|1023&t.charCodeAt(e)),r.push(240|n>>18,128|n>>12&63,128|n>>6&63,128|63&n))}return r}(t)},function(t){"function"==typeof define&&define.amd?define([],t):"object"==typeof exports&&(module.exports=t())}(function(){return qrcode}); \ No newline at end of file diff --git a/webapp/index.html b/webapp/index.html new file mode 100644 index 0000000..9945f47 --- /dev/null +++ b/webapp/index.html @@ -0,0 +1,12 @@ + + + + + + NodeWarden + + +
    + + + diff --git a/webapp/src/App.tsx b/webapp/src/App.tsx new file mode 100644 index 0000000..502094e --- /dev/null +++ b/webapp/src/App.tsx @@ -0,0 +1,756 @@ +import { useEffect, useMemo, useState } from 'preact/hooks'; +import { Link, Route, Switch, useLocation } from 'wouter'; +import { useQuery } from '@tanstack/react-query'; +import AuthViews from '@/components/AuthViews'; +import ConfirmDialog from '@/components/ConfirmDialog'; +import ToastHost from '@/components/ToastHost'; +import VaultPage from '@/components/VaultPage'; +import SettingsPage from '@/components/SettingsPage'; +import AdminPage from '@/components/AdminPage'; +import HelpPage from '@/components/HelpPage'; +import { + changeMasterPassword, + createCipher, + createAuthedFetch, + createInvite, + deleteCipher, + deleteUser, + deriveLoginHash, + bulkMoveCiphers, + getCiphers, + getFolders, + getProfile, + getSetupStatus, + getWebConfig, + listAdminInvites, + listAdminUsers, + loadSession, + loginWithPassword, + registerAccount, + revokeInvite, + saveSession, + setTotp, + setUserStatus, + updateCipher, + unlockVaultKey, + updateProfile, +} from '@/lib/api'; +import { base64ToBytes, decryptBw, decryptStr } from '@/lib/crypto'; +import type { AppPhase, Cipher, Folder, Profile, SessionState, ToastMessage, VaultDraft } from '@/lib/types'; + +interface PendingTotp { + email: string; + passwordHash: string; + masterKey: Uint8Array; +} + +export default function App() { + const [location, navigate] = useLocation(); + const [phase, setPhase] = useState('loading'); + const [session, setSessionState] = useState(null); + const [profile, setProfile] = useState(null); + const [defaultKdfIterations, setDefaultKdfIterations] = useState(600000); + const [setupRegistered, setSetupRegistered] = useState(true); + + const [loginValues, setLoginValues] = useState({ email: '', password: '' }); + const [registerValues, setRegisterValues] = useState({ + name: '', + email: '', + password: '', + password2: '', + inviteCode: '', + }); + const [unlockPassword, setUnlockPassword] = useState(''); + const [pendingTotp, setPendingTotp] = useState(null); + const [totpCode, setTotpCode] = useState(''); + + const [disableTotpOpen, setDisableTotpOpen] = useState(false); + const [disableTotpPassword, setDisableTotpPassword] = useState(''); + + const [confirm, setConfirm] = useState<{ + title: string; + message: string; + danger?: boolean; + onConfirm: () => void; + } | null>(null); + + const [toasts, setToasts] = useState([]); + const [decryptedFolders, setDecryptedFolders] = useState([]); + const [decryptedCiphers, setDecryptedCiphers] = useState([]); + + function setSession(next: SessionState | null) { + setSessionState(next); + saveSession(next); + } + + function pushToast(type: ToastMessage['type'], text: string) { + const id = `${Date.now()}-${Math.random().toString(36).slice(2)}`; + setToasts((prev) => [...prev.slice(-3), { id, type, text }]); + window.setTimeout(() => { + setToasts((prev) => prev.filter((x) => x.id !== id)); + }, 4500); + } + + const authedFetch = useMemo( + () => + createAuthedFetch( + () => session, + (next) => { + setSession(next); + if (!next) { + setProfile(null); + setPhase(setupRegistered ? 'login' : 'register'); + } + } + ), + [session, setupRegistered] + ); + + useEffect(() => { + let mounted = true; + (async () => { + const [setup, config] = await Promise.all([getSetupStatus(), getWebConfig()]); + if (!mounted) return; + setSetupRegistered(setup.registered); + setDefaultKdfIterations(Number(config.defaultKdfIterations || 600000)); + + const loaded = loadSession(); + if (!loaded) { + setPhase(setup.registered ? 'login' : 'register'); + return; + } + setSession(loaded); + + try { + const profileResp = await getProfile( + createAuthedFetch( + () => loaded, + (next) => { + if (!next) return; + setSession(next); + } + ) + ); + if (!mounted) return; + setProfile(profileResp); + setPhase('locked'); + } catch { + setSession(null); + setPhase(setup.registered ? 'login' : 'register'); + } + })(); + + return () => { + mounted = false; + }; + }, []); + + async function finalizeLogin(tokenAccess: string, tokenRefresh: string, email: string, masterKey: Uint8Array) { + const baseSession: SessionState = { accessToken: tokenAccess, refreshToken: tokenRefresh, email }; + const tempFetch = createAuthedFetch( + () => baseSession, + () => {} + ); + const profileResp = await getProfile(tempFetch); + const keys = await unlockVaultKey(profileResp.key, masterKey); + const nextSession = { ...baseSession, ...keys }; + setSession(nextSession); + setProfile(profileResp); + setPendingTotp(null); + setTotpCode(''); + setPhase('app'); + if (location === '/' || location === '/login' || location === '/register' || location === '/lock') { + navigate('/vault'); + } + pushToast('success', 'Login success'); + } + + async function handleLogin() { + if (!loginValues.email || !loginValues.password) { + pushToast('error', 'Please input email and password'); + return; + } + try { + const derived = await deriveLoginHash(loginValues.email, loginValues.password, defaultKdfIterations); + const token = await loginWithPassword(loginValues.email, derived.hash); + if ('access_token' in token && token.access_token) { + await finalizeLogin(token.access_token, token.refresh_token, loginValues.email.toLowerCase(), derived.masterKey); + return; + } + const tokenError = token as { TwoFactorProviders?: unknown; error_description?: string; error?: string }; + if (tokenError.TwoFactorProviders) { + setPendingTotp({ + email: loginValues.email.toLowerCase(), + passwordHash: derived.hash, + masterKey: derived.masterKey, + }); + setTotpCode(''); + return; + } + pushToast('error', tokenError.error_description || tokenError.error || 'Login failed'); + } catch (error) { + pushToast('error', error instanceof Error ? error.message : 'Login failed'); + } + } + + async function handleTotpVerify() { + if (!pendingTotp) return; + if (!totpCode.trim()) { + pushToast('error', 'Please input TOTP code'); + return; + } + const token = await loginWithPassword(pendingTotp.email, pendingTotp.passwordHash, totpCode.trim()); + if ('access_token' in token && token.access_token) { + await finalizeLogin(token.access_token, token.refresh_token, pendingTotp.email, pendingTotp.masterKey); + return; + } + const tokenError = token as { error_description?: string; error?: string }; + pushToast('error', tokenError.error_description || tokenError.error || 'TOTP verify failed'); + } + + async function handleRegister() { + if (!registerValues.email || !registerValues.password) { + pushToast('error', 'Please input email and password'); + return; + } + if (registerValues.password.length < 12) { + pushToast('error', 'Master password must be at least 12 chars'); + return; + } + if (registerValues.password !== registerValues.password2) { + pushToast('error', 'Passwords do not match'); + return; + } + const resp = await registerAccount({ + email: registerValues.email.toLowerCase(), + name: registerValues.name.trim(), + password: registerValues.password, + inviteCode: registerValues.inviteCode.trim(), + fallbackIterations: defaultKdfIterations, + }); + if (!resp.ok) { + pushToast('error', resp.message); + return; + } + setLoginValues({ email: registerValues.email.toLowerCase(), password: '' }); + setPhase('login'); + pushToast('success', 'Registration succeeded. Please sign in.'); + } + + async function handleUnlock() { + if (!session || !profile) return; + if (!unlockPassword) { + pushToast('error', 'Please input master password'); + return; + } + try { + const derived = await deriveLoginHash(profile.email || session.email, unlockPassword, defaultKdfIterations); + const keys = await unlockVaultKey(profile.key, derived.masterKey); + setSession({ ...session, ...keys }); + setUnlockPassword(''); + setPhase('app'); + if (location === '/' || location === '/lock') navigate('/vault'); + pushToast('success', 'Unlocked'); + } catch { + pushToast('error', 'Unlock failed. Master password is incorrect.'); + } + } + + function handleLock() { + if (!session) return; + const nextSession = { ...session }; + delete nextSession.symEncKey; + delete nextSession.symMacKey; + setSession(nextSession); + setPhase('locked'); + navigate('/lock'); + } + + function handleLogout() { + setConfirm({ + title: 'Log Out', + message: 'Are you sure you want to log out?', + onConfirm: () => { + setConfirm(null); + setSession(null); + setProfile(null); + setPendingTotp(null); + setPhase(setupRegistered ? 'login' : 'register'); + navigate('/login'); + }, + }); + } + + const ciphersQuery = useQuery({ + queryKey: ['ciphers', session?.accessToken], + queryFn: () => getCiphers(authedFetch), + enabled: phase === 'app' && !!session?.symEncKey && !!session?.symMacKey, + }); + const foldersQuery = useQuery({ + queryKey: ['folders', session?.accessToken], + queryFn: () => getFolders(authedFetch), + enabled: phase === 'app' && !!session?.symEncKey && !!session?.symMacKey, + }); + const usersQuery = useQuery({ + queryKey: ['admin-users', session?.accessToken], + queryFn: () => listAdminUsers(authedFetch), + enabled: phase === 'app' && profile?.role === 'admin', + }); + const invitesQuery = useQuery({ + queryKey: ['admin-invites', session?.accessToken], + queryFn: () => listAdminInvites(authedFetch), + enabled: phase === 'app' && profile?.role === 'admin', + }); + + useEffect(() => { + if (!session?.symEncKey || !session?.symMacKey) { + setDecryptedFolders([]); + setDecryptedCiphers([]); + return; + } + if (!foldersQuery.data || !ciphersQuery.data) return; + + let active = true; + (async () => { + try { + const encKey = base64ToBytes(session.symEncKey!); + const macKey = base64ToBytes(session.symMacKey!); + + const folders = await Promise.all( + foldersQuery.data.map(async (folder) => ({ + ...folder, + decName: await decryptStr(folder.name, encKey, macKey), + })) + ); + + const ciphers = await Promise.all( + ciphersQuery.data.map(async (cipher) => { + let itemEnc = encKey; + let itemMac = macKey; + if (cipher.key) { + try { + const itemKey = await decryptBw(cipher.key, encKey, macKey); + itemEnc = itemKey.slice(0, 32); + itemMac = itemKey.slice(32, 64); + } catch { + // keep user key when item key decrypt fails + } + } + + const nextCipher: Cipher = { + ...cipher, + decName: await decryptStr(cipher.name || '', itemEnc, itemMac), + decNotes: await decryptStr(cipher.notes || '', itemEnc, itemMac), + }; + if (cipher.login) { + nextCipher.login = { + ...cipher.login, + decUsername: await decryptStr(cipher.login.username || '', itemEnc, itemMac), + decPassword: await decryptStr(cipher.login.password || '', itemEnc, itemMac), + decTotp: await decryptStr(cipher.login.totp || '', itemEnc, itemMac), + uris: await Promise.all( + (cipher.login.uris || []).map(async (u) => ({ + ...u, + decUri: await decryptStr(u.uri || '', itemEnc, itemMac), + })) + ), + }; + } + if (cipher.card) { + nextCipher.card = { + ...cipher.card, + decCardholderName: await decryptStr(cipher.card.cardholderName || '', itemEnc, itemMac), + decNumber: await decryptStr(cipher.card.number || '', itemEnc, itemMac), + decBrand: await decryptStr(cipher.card.brand || '', itemEnc, itemMac), + decExpMonth: await decryptStr(cipher.card.expMonth || '', itemEnc, itemMac), + decExpYear: await decryptStr(cipher.card.expYear || '', itemEnc, itemMac), + decCode: await decryptStr(cipher.card.code || '', itemEnc, itemMac), + }; + } + if (cipher.identity) { + nextCipher.identity = { + ...cipher.identity, + decTitle: await decryptStr(cipher.identity.title || '', itemEnc, itemMac), + decFirstName: await decryptStr(cipher.identity.firstName || '', itemEnc, itemMac), + decMiddleName: await decryptStr(cipher.identity.middleName || '', itemEnc, itemMac), + decLastName: await decryptStr(cipher.identity.lastName || '', itemEnc, itemMac), + decUsername: await decryptStr(cipher.identity.username || '', itemEnc, itemMac), + decCompany: await decryptStr(cipher.identity.company || '', itemEnc, itemMac), + decSsn: await decryptStr(cipher.identity.ssn || '', itemEnc, itemMac), + decPassportNumber: await decryptStr(cipher.identity.passportNumber || '', itemEnc, itemMac), + decLicenseNumber: await decryptStr(cipher.identity.licenseNumber || '', itemEnc, itemMac), + decEmail: await decryptStr(cipher.identity.email || '', itemEnc, itemMac), + decPhone: await decryptStr(cipher.identity.phone || '', itemEnc, itemMac), + decAddress1: await decryptStr(cipher.identity.address1 || '', itemEnc, itemMac), + decAddress2: await decryptStr(cipher.identity.address2 || '', itemEnc, itemMac), + decAddress3: await decryptStr(cipher.identity.address3 || '', itemEnc, itemMac), + decCity: await decryptStr(cipher.identity.city || '', itemEnc, itemMac), + decState: await decryptStr(cipher.identity.state || '', itemEnc, itemMac), + decPostalCode: await decryptStr(cipher.identity.postalCode || '', itemEnc, itemMac), + decCountry: await decryptStr(cipher.identity.country || '', itemEnc, itemMac), + }; + } + if (cipher.sshKey) { + nextCipher.sshKey = { + ...cipher.sshKey, + decPrivateKey: await decryptStr(cipher.sshKey.privateKey || '', itemEnc, itemMac), + decPublicKey: await decryptStr(cipher.sshKey.publicKey || '', itemEnc, itemMac), + decFingerprint: await decryptStr(cipher.sshKey.fingerprint || '', itemEnc, itemMac), + }; + } + if (cipher.fields) { + nextCipher.fields = await Promise.all( + cipher.fields.map(async (field) => ({ + ...field, + decName: await decryptStr(field.name || '', itemEnc, itemMac), + decValue: await decryptStr(field.value || '', itemEnc, itemMac), + })) + ); + } + return nextCipher; + }) + ); + + if (!active) return; + setDecryptedFolders(folders); + setDecryptedCiphers(ciphers); + } catch (error) { + if (!active) return; + pushToast('error', error instanceof Error ? error.message : 'Decrypt failed'); + } + })(); + + return () => { + active = false; + }; + }, [session?.symEncKey, session?.symMacKey, foldersQuery.data, ciphersQuery.data]); + + async function saveProfileAction(name: string, email: string) { + try { + const updated = await updateProfile(authedFetch, { name: name.trim(), email: email.trim().toLowerCase() }); + setProfile(updated); + pushToast('success', 'Profile updated'); + } catch (error) { + pushToast('error', error instanceof Error ? error.message : 'Save profile failed'); + } + } + + async function changePasswordAction(currentPassword: string, nextPassword: string, nextPassword2: string) { + if (!profile) return; + if (!currentPassword || !nextPassword) { + pushToast('error', 'Current/new password is required'); + return; + } + if (nextPassword.length < 12) { + pushToast('error', 'New password must be at least 12 chars'); + return; + } + if (nextPassword !== nextPassword2) { + pushToast('error', 'New passwords do not match'); + return; + } + try { + await changeMasterPassword(authedFetch, { + email: profile.email, + currentPassword, + newPassword: nextPassword, + currentIterations: defaultKdfIterations, + profileKey: profile.key, + }); + handleLogout(); + pushToast('success', 'Master password changed. Please login again.'); + } catch (error) { + pushToast('error', error instanceof Error ? error.message : 'Change password failed'); + } + } + + async function enableTotpAction(secret: string, token: string) { + if (!secret.trim() || !token.trim()) { + pushToast('error', 'Secret and code are required'); + return; + } + try { + await setTotp(authedFetch, { enabled: true, secret: secret.trim(), token: token.trim() }); + pushToast('success', 'TOTP enabled'); + } catch (error) { + pushToast('error', error instanceof Error ? error.message : 'Enable TOTP failed'); + } + } + + async function disableTotpAction() { + if (!profile) return; + if (!disableTotpPassword) { + pushToast('error', 'Please input master password'); + return; + } + try { + const derived = await deriveLoginHash(profile.email, disableTotpPassword, defaultKdfIterations); + await setTotp(authedFetch, { enabled: false, masterPasswordHash: derived.hash }); + setDisableTotpOpen(false); + setDisableTotpPassword(''); + pushToast('success', 'TOTP disabled'); + } catch (error) { + pushToast('error', error instanceof Error ? error.message : 'Disable TOTP failed'); + } + } + + async function refreshVault() { + await Promise.all([ciphersQuery.refetch(), foldersQuery.refetch()]); + pushToast('success', 'Vault synced'); + } + + async function createVaultItem(draft: VaultDraft) { + if (!session) return; + try { + await createCipher(authedFetch, session, draft); + await Promise.all([ciphersQuery.refetch(), foldersQuery.refetch()]); + pushToast('success', 'Item created'); + } catch (error) { + pushToast('error', error instanceof Error ? error.message : 'Create item failed'); + throw error; + } + } + + async function updateVaultItem(cipher: Cipher, draft: VaultDraft) { + if (!session) return; + try { + await updateCipher(authedFetch, session, cipher, draft); + await Promise.all([ciphersQuery.refetch(), foldersQuery.refetch()]); + pushToast('success', 'Item updated'); + } catch (error) { + pushToast('error', error instanceof Error ? error.message : 'Update item failed'); + throw error; + } + } + + async function deleteVaultItem(cipher: Cipher) { + try { + await deleteCipher(authedFetch, cipher.id); + await Promise.all([ciphersQuery.refetch(), foldersQuery.refetch()]); + pushToast('success', 'Item deleted'); + } catch (error) { + pushToast('error', error instanceof Error ? error.message : 'Delete item failed'); + throw error; + } + } + + async function bulkDeleteVaultItems(ids: string[]) { + try { + for (const id of ids) { + await deleteCipher(authedFetch, id); + } + await Promise.all([ciphersQuery.refetch(), foldersQuery.refetch()]); + pushToast('success', 'Deleted selected items'); + } catch (error) { + pushToast('error', error instanceof Error ? error.message : 'Bulk delete failed'); + throw error; + } + } + + async function bulkMoveVaultItems(ids: string[], folderId: string | null) { + try { + await bulkMoveCiphers(authedFetch, ids, folderId); + await Promise.all([ciphersQuery.refetch(), foldersQuery.refetch()]); + pushToast('success', 'Moved selected items'); + } catch (error) { + pushToast('error', error instanceof Error ? error.message : 'Bulk move failed'); + throw error; + } + } + + useEffect(() => { + if (phase === 'app' && location === '/') navigate('/vault'); + }, [phase, location, navigate]); + + if (phase === 'loading') { + return ( + <> +
    Loading NodeWarden...
    + setToasts((prev) => prev.filter((x) => x.id !== id))} /> + + ); + } + + if (phase === 'register' || phase === 'login' || phase === 'locked') { + return ( + <> + void handleLogin()} + onSubmitRegister={() => void handleRegister()} + onSubmitUnlock={() => void handleUnlock()} + onGotoLogin={() => setPhase('login')} + onGotoRegister={() => setPhase('register')} + onLogout={handleLogout} + /> + setToasts((prev) => prev.filter((x) => x.id !== id))} /> + + void handleTotpVerify()} + onCancel={() => { + setPendingTotp(null); + setTotpCode(''); + }} + > + + + + ); + } + + return ( + <> +
    +
    +
    NodeWarden
    + +
    + {profile?.email} + + +
    +
    +
    + + + + + + {profile && ( + setDisableTotpOpen(true)} + /> + )} + + + { + void usersQuery.refetch(); + void invitesQuery.refetch(); + }} + onCreateInvite={async (hours) => { + await createInvite(authedFetch, hours); + await invitesQuery.refetch(); + pushToast('success', 'Invite created'); + }} + onToggleUserStatus={async (userId, status) => { + await setUserStatus(authedFetch, userId, status === 'active' ? 'banned' : 'active'); + await usersQuery.refetch(); + pushToast('success', 'User status updated'); + }} + onDeleteUser={async (userId) => { + setConfirm({ + title: 'Delete user', + message: 'Delete this user and all user data?', + danger: true, + onConfirm: () => { + setConfirm(null); + void (async () => { + await deleteUser(authedFetch, userId); + await usersQuery.refetch(); + pushToast('success', 'User deleted'); + })(); + }, + }); + }} + onRevokeInvite={async (code) => { + await revokeInvite(authedFetch, code); + await invitesQuery.refetch(); + pushToast('success', 'Invite revoked'); + }} + /> + + + + + +
    +
    + + confirm?.onConfirm()} + onCancel={() => setConfirm(null)} + /> + + void disableTotpAction()} + onCancel={() => { + setDisableTotpOpen(false); + setDisableTotpPassword(''); + }} + > + + + + setToasts((prev) => prev.filter((x) => x.id !== id))} /> + + ); +} diff --git a/webapp/src/components/AdminPage.tsx b/webapp/src/components/AdminPage.tsx new file mode 100644 index 0000000..3284e5a --- /dev/null +++ b/webapp/src/components/AdminPage.tsx @@ -0,0 +1,116 @@ +import { useState } from 'preact/hooks'; +import type { AdminInvite, AdminUser } from '@/lib/types'; + +interface AdminPageProps { + users: AdminUser[]; + invites: AdminInvite[]; + onRefresh: () => void; + onCreateInvite: (hours: number) => Promise; + onToggleUserStatus: (userId: string, currentStatus: string) => Promise; + onDeleteUser: (userId: string) => Promise; + onRevokeInvite: (code: string) => Promise; +} + +export default function AdminPage(props: AdminPageProps) { + const [inviteHours, setInviteHours] = useState(168); + + return ( +
    +
    +
    +

    Invites

    + +
    +
    + setInviteHours(Number((e.currentTarget as HTMLInputElement).value || 168))} + /> + +
    + + + + + + + + + + {props.invites.map((invite) => ( + + + + + + ))} + +
    CodeStatusActions
    {invite.code}{invite.status} +
    + + {invite.status === 'active' && ( + + )} +
    +
    +
    + +
    +

    Users

    + + + + + + + + + + + + {props.users.map((user) => ( + + + + + + + + ))} + +
    EmailNameRoleStatusActions
    {user.email}{user.name || '-'}{user.role}{user.status} +
    + + {user.role !== 'admin' && ( + + )} +
    +
    +
    +
    + ); +} diff --git a/webapp/src/components/AuthViews.tsx b/webapp/src/components/AuthViews.tsx new file mode 100644 index 0000000..900dfbb --- /dev/null +++ b/webapp/src/components/AuthViews.tsx @@ -0,0 +1,173 @@ +import { useState } from 'preact/hooks'; + +interface LoginValues { + email: string; + password: string; +} + +interface RegisterValues { + name: string; + email: string; + password: string; + password2: string; + inviteCode: string; +} + +interface AuthViewsProps { + mode: 'login' | 'register' | 'locked'; + loginValues: LoginValues; + registerValues: RegisterValues; + unlockPassword: string; + emailForLock: string; + onChangeLogin: (next: LoginValues) => void; + onChangeRegister: (next: RegisterValues) => void; + onChangeUnlock: (password: string) => void; + onSubmitLogin: () => void; + onSubmitRegister: () => void; + onSubmitUnlock: () => void; + onGotoLogin: () => void; + onGotoRegister: () => void; + onLogout: () => void; +} + +function PasswordField(props: { + label: string; + value: string; + onInput: (v: string) => void; + autoFocus?: boolean; +}) { + const [show, setShow] = useState(false); + return ( + + ); +} + +export default function AuthViews(props: AuthViewsProps) { + if (props.mode === 'locked') { + return ( +
    +
    +

    Unlock Vault

    +

    {props.emailForLock}

    + + +
    or
    + +
    +
    + ); + } + + if (props.mode === 'register') { + return ( +
    +
    +

    Create Account

    +

    NodeWarden

    + + + props.onChangeRegister({ ...props.registerValues, password: v })} + /> + props.onChangeRegister({ ...props.registerValues, password2: v })} + /> + + +
    or
    + +
    +
    + ); + } + + return ( +
    +
    +

    Log In

    +

    NodeWarden

    + + props.onChangeLogin({ ...props.loginValues, password: v })} + autoFocus + /> + +
    or
    + +
    +
    + ); +} diff --git a/webapp/src/components/ConfirmDialog.tsx b/webapp/src/components/ConfirmDialog.tsx new file mode 100644 index 0000000..e179007 --- /dev/null +++ b/webapp/src/components/ConfirmDialog.tsx @@ -0,0 +1,37 @@ +import type { ComponentChildren } from 'preact'; + +interface ConfirmDialogProps { + open: boolean; + title: string; + message: string; + confirmText?: string; + cancelText?: string; + danger?: boolean; + onConfirm: () => void; + onCancel: () => void; + children?: ComponentChildren; +} + +export default function ConfirmDialog(props: ConfirmDialogProps) { + if (!props.open) return null; + return ( +
    +
    +
    !
    +

    {props.title}

    +
    {props.message}
    + {props.children} + + +
    +
    + ); +} diff --git a/webapp/src/components/HelpPage.tsx b/webapp/src/components/HelpPage.tsx new file mode 100644 index 0000000..45b2845 --- /dev/null +++ b/webapp/src/components/HelpPage.tsx @@ -0,0 +1,23 @@ +export default function HelpPage() { + return ( +
    +
    +

    Upstream Sync

    +
      +
    • Use fork + scheduled sync workflow.
    • +
    • Before merging, compare API routes and auth flow changes.
    • +
    • After merging, run migration tests in local dev before deploy.
    • +
    +
    +
    +

    Common Errors

    +
      +
    • 401 Unauthorized: token expired, log in again.
    • +
    • 403 Account disabled: admin must unban your account.
    • +
    • 403 Invite invalid: invite expired or revoked.
    • +
    • 429 Too many requests: wait and retry.
    • +
    +
    +
    + ); +} diff --git a/webapp/src/components/SettingsPage.tsx b/webapp/src/components/SettingsPage.tsx new file mode 100644 index 0000000..e2670df --- /dev/null +++ b/webapp/src/components/SettingsPage.tsx @@ -0,0 +1,128 @@ +import { useMemo, useState } from 'preact/hooks'; +import qrcode from 'qrcode-generator'; +import type { Profile } from '@/lib/types'; + +interface SettingsPageProps { + profile: Profile; + onSaveProfile: (name: string, email: string) => Promise; + onChangePassword: (currentPassword: string, nextPassword: string, nextPassword2: string) => Promise; + onEnableTotp: (secret: string, token: string) => Promise; + onOpenDisableTotp: () => void; +} + +function randomBase32Secret(length: number): string { + const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; + const random = crypto.getRandomValues(new Uint8Array(length)); + let out = ''; + for (const x of random) out += alphabet[x % alphabet.length]; + return out; +} + +function buildOtpUri(email: string, secret: string): string { + const issuer = 'NodeWarden'; + return `otpauth://totp/${encodeURIComponent(`${issuer}:${email}`)}?secret=${encodeURIComponent(secret)}&issuer=${encodeURIComponent(issuer)}&algorithm=SHA1&digits=6&period=30`; +} + +export default function SettingsPage(props: SettingsPageProps) { + const [name, setName] = useState(props.profile.name || ''); + const [email, setEmail] = useState(props.profile.email || ''); + const [currentPassword, setCurrentPassword] = useState(''); + const [newPassword, setNewPassword] = useState(''); + const [newPassword2, setNewPassword2] = useState(''); + const [secret, setSecret] = useState(randomBase32Secret(32)); + const [token, setToken] = useState(''); + + const qrSvg = useMemo(() => { + const qr = qrcode(0, 'M'); + qr.addData(buildOtpUri(email || props.profile.email, secret)); + qr.make(); + return qr.createSvgTag({ scalable: true, margin: 0 }); + }, [email, props.profile.email, secret]); + + return ( +
    +
    +

    Profile

    +
    + + +
    + +
    + +
    +

    Change Master Password

    + +
    + + +
    + +
    + +
    +

    TOTP

    +
    +
    +
    + + +
    + + + +
    +
    +
    + +
    +
    + ); +} diff --git a/webapp/src/components/ToastHost.tsx b/webapp/src/components/ToastHost.tsx new file mode 100644 index 0000000..3c5ef0b --- /dev/null +++ b/webapp/src/components/ToastHost.tsx @@ -0,0 +1,23 @@ +import type { ToastMessage } from '@/lib/types'; + +interface ToastHostProps { + toasts: ToastMessage[]; + onClose: (id: string) => void; +} + +export default function ToastHost({ toasts, onClose }: ToastHostProps) { + if (!toasts.length) return null; + return ( +
      + {toasts.map((toast) => ( +
    • +
      {toast.text}
      + +
      +
    • + ))} +
    + ); +} diff --git a/webapp/src/components/VaultPage.tsx b/webapp/src/components/VaultPage.tsx new file mode 100644 index 0000000..426817a --- /dev/null +++ b/webapp/src/components/VaultPage.tsx @@ -0,0 +1,1045 @@ +import { useEffect, useMemo, useState } from 'preact/hooks'; +import ConfirmDialog from '@/components/ConfirmDialog'; +import { calcTotpNow } from '@/lib/crypto'; +import type { Cipher, CustomFieldType, Folder, VaultDraft, VaultDraftField } from '@/lib/types'; + +interface VaultPageProps { + ciphers: Cipher[]; + folders: Folder[]; + loading: boolean; + onRefresh: () => Promise; + onCreate: (draft: VaultDraft) => Promise; + onUpdate: (cipher: Cipher, draft: VaultDraft) => Promise; + onDelete: (cipher: Cipher) => Promise; + onBulkDelete: (ids: string[]) => Promise; + onBulkMove: (ids: string[], folderId: string | null) => Promise; +} + +type TypeFilter = 'all' | 'favorite' | 'login' | 'card' | 'identity' | 'note' | 'ssh'; + +interface TypeOption { + type: number; + label: string; +} + +const CREATE_TYPE_OPTIONS: TypeOption[] = [ + { type: 1, label: 'Login' }, + { type: 3, label: 'Card' }, + { type: 4, label: 'Identity' }, + { type: 2, label: 'Note' }, + { type: 5, label: 'SSH Key' }, +]; + +const FIELD_TYPE_OPTIONS: Array<{ value: CustomFieldType; label: string }> = [ + { value: 0, label: 'Text' }, + { value: 1, label: 'Hidden' }, + { value: 2, label: 'Boolean' }, + { value: 3, label: 'Linked' }, +]; + +function cipherTypeKey(type: number): TypeFilter { + if (type === 1) return 'login'; + if (type === 3) return 'card'; + if (type === 4) return 'identity'; + if (type === 2) return 'note'; + return 'ssh'; +} + +function cipherTypeLabel(type: number): string { + if (type === 1) return 'Login'; + if (type === 3) return 'Card'; + if (type === 4) return 'Identity'; + if (type === 2) return 'Secure Note'; + if (type === 5) return 'SSH Key'; + return 'Item'; +} + +function typeIconText(type: number): string { + if (type === 1) return 'L'; + if (type === 3) return 'C'; + if (type === 4) return 'I'; + if (type === 2) return 'N'; + if (type === 5) return 'S'; + return 'V'; +} + +function parseFieldType(value: number | string | null | undefined): CustomFieldType { + if (value === 1 || value === 2 || value === 3) return value; + if (value === '1' || String(value).toLowerCase() === 'hidden') return 1; + if (value === '2' || String(value).toLowerCase() === 'boolean') return 2; + if (value === '3' || String(value).toLowerCase() === 'linked') return 3; + return 0; +} + +function fieldTypeLabel(type: CustomFieldType): string { + const found = FIELD_TYPE_OPTIONS.find((x) => x.value === type); + return found ? found.label : 'Text'; +} + +function firstCipherUri(cipher: Cipher): string { + const uris = cipher.login?.uris || []; + for (const uri of uris) { + const raw = uri.decUri || uri.uri || ''; + if (raw.trim()) return raw.trim(); + } + return ''; +} + +function hostFromUri(uri: string): string { + if (!uri.trim()) return ''; + try { + const normalized = /^https?:\/\//i.test(uri) ? uri : `https://${uri}`; + return new URL(normalized).hostname || ''; + } catch { + return ''; + } +} + +function createEmptyDraft(type: number): VaultDraft { + return { + type, + name: '', + folderId: '', + notes: '', + reprompt: false, + loginUsername: '', + loginPassword: '', + loginTotp: '', + loginUris: [''], + cardholderName: '', + cardNumber: '', + cardBrand: '', + cardExpMonth: '', + cardExpYear: '', + cardCode: '', + identTitle: '', + identFirstName: '', + identMiddleName: '', + identLastName: '', + identUsername: '', + identCompany: '', + identSsn: '', + identPassportNumber: '', + identLicenseNumber: '', + identEmail: '', + identPhone: '', + identAddress1: '', + identAddress2: '', + identAddress3: '', + identCity: '', + identState: '', + identPostalCode: '', + identCountry: '', + sshPrivateKey: '', + sshPublicKey: '', + sshFingerprint: '', + customFields: [], + }; +} + +function draftFromCipher(cipher: Cipher): VaultDraft { + const draft = createEmptyDraft(Number(cipher.type || 1)); + draft.id = cipher.id; + draft.name = cipher.decName || ''; + draft.folderId = cipher.folderId || ''; + draft.notes = cipher.decNotes || ''; + draft.reprompt = Number(cipher.reprompt || 0) === 1; + + if (cipher.login) { + draft.loginUsername = cipher.login.decUsername || ''; + draft.loginPassword = cipher.login.decPassword || ''; + draft.loginTotp = cipher.login.decTotp || ''; + draft.loginUris = (cipher.login.uris || []).map((x) => x.decUri || x.uri || ''); + if (!draft.loginUris.length) draft.loginUris = ['']; + } + if (cipher.card) { + draft.cardholderName = cipher.card.decCardholderName || ''; + draft.cardNumber = cipher.card.decNumber || ''; + draft.cardBrand = cipher.card.decBrand || ''; + draft.cardExpMonth = cipher.card.decExpMonth || ''; + draft.cardExpYear = cipher.card.decExpYear || ''; + draft.cardCode = cipher.card.decCode || ''; + } + if (cipher.identity) { + draft.identTitle = cipher.identity.decTitle || ''; + draft.identFirstName = cipher.identity.decFirstName || ''; + draft.identMiddleName = cipher.identity.decMiddleName || ''; + draft.identLastName = cipher.identity.decLastName || ''; + draft.identUsername = cipher.identity.decUsername || ''; + draft.identCompany = cipher.identity.decCompany || ''; + draft.identSsn = cipher.identity.decSsn || ''; + draft.identPassportNumber = cipher.identity.decPassportNumber || ''; + draft.identLicenseNumber = cipher.identity.decLicenseNumber || ''; + draft.identEmail = cipher.identity.decEmail || ''; + draft.identPhone = cipher.identity.decPhone || ''; + draft.identAddress1 = cipher.identity.decAddress1 || ''; + draft.identAddress2 = cipher.identity.decAddress2 || ''; + draft.identAddress3 = cipher.identity.decAddress3 || ''; + draft.identCity = cipher.identity.decCity || ''; + draft.identState = cipher.identity.decState || ''; + draft.identPostalCode = cipher.identity.decPostalCode || ''; + draft.identCountry = cipher.identity.decCountry || ''; + } + if (cipher.sshKey) { + draft.sshPrivateKey = cipher.sshKey.decPrivateKey || ''; + draft.sshPublicKey = cipher.sshKey.decPublicKey || ''; + draft.sshFingerprint = cipher.sshKey.decFingerprint || ''; + } + draft.customFields = (cipher.fields || []).map((field) => ({ + type: parseFieldType(field.type), + label: field.decName || '', + value: field.decValue || '', + })); + + return draft; +} + +function matchesTypeFilter(cipher: Cipher, typeFilter: TypeFilter): boolean { + if (typeFilter === 'all') return true; + if (typeFilter === 'favorite') return !!cipher.favorite; + return cipherTypeKey(Number(cipher.type || 1)) === typeFilter; +} + +function maskSecret(value: string): string { + if (!value) return ''; + return '*'.repeat(Math.max(8, Math.min(24, value.length))); +} + +function formatTotp(code: string): string { + if (!code || code.length < 6) return code; + return `${code.slice(0, 3)} ${code.slice(3, 6)}`; +} + +function VaultListIcon({ cipher }: { cipher: Cipher }) { + const uri = firstCipherUri(cipher); + const host = hostFromUri(uri); + const [errored, setErrored] = useState(false); + if (host && !errored) { + return ( + setErrored(true)} + /> + ); + } + return {typeIconText(Number(cipher.type || 1))}; +} + +function copyToClipboard(value: string): void { + if (!value.trim()) return; + void navigator.clipboard.writeText(value); +} + +function openUri(raw: string): void { + const value = raw.trim(); + if (!value) return; + const url = /^https?:\/\//i.test(value) ? value : `https://${value}`; + window.open(url, '_blank', 'noopener'); +} + +export default function VaultPage(props: VaultPageProps) { + const [searchInput, setSearchInput] = useState(''); + const [searchQuery, setSearchQuery] = useState(''); + const [searchComposing, setSearchComposing] = useState(false); + const [typeFilter, setTypeFilter] = useState('all'); + const [folderFilter, setFolderFilter] = useState('all'); + const [selectedCipherId, setSelectedCipherId] = useState(''); + const [selectedMap, setSelectedMap] = useState>({}); + const [showPassword, setShowPassword] = useState(false); + const [createMenuOpen, setCreateMenuOpen] = useState(false); + const [isEditing, setIsEditing] = useState(false); + const [isCreating, setIsCreating] = useState(false); + const [draft, setDraft] = useState(null); + const [fieldModalOpen, setFieldModalOpen] = useState(false); + const [fieldType, setFieldType] = useState(0); + const [fieldLabel, setFieldLabel] = useState(''); + const [fieldValue, setFieldValue] = useState(''); + const [localError, setLocalError] = useState(''); + const [pendingDelete, setPendingDelete] = useState(null); + const [bulkDeleteOpen, setBulkDeleteOpen] = useState(false); + const [moveOpen, setMoveOpen] = useState(false); + const [moveFolderId, setMoveFolderId] = useState('__none__'); + const [totpLive, setTotpLive] = useState<{ code: string; remain: number } | null>(null); + const [busy, setBusy] = useState(false); + + useEffect(() => { + if (searchComposing) return; + const timer = window.setTimeout(() => setSearchQuery(searchInput.trim().toLowerCase()), 90); + return () => window.clearTimeout(timer); + }, [searchInput, searchComposing]); + + const filteredCiphers = useMemo(() => { + return props.ciphers.filter((cipher) => { + if (!matchesTypeFilter(cipher, typeFilter)) return false; + if (folderFilter === 'none' && cipher.folderId) return false; + if (folderFilter !== 'none' && folderFilter !== 'all' && cipher.folderId !== folderFilter) return false; + if (!searchQuery) return true; + const name = (cipher.decName || '').toLowerCase(); + const username = (cipher.login?.decUsername || '').toLowerCase(); + const uri = firstCipherUri(cipher).toLowerCase(); + return name.includes(searchQuery) || username.includes(searchQuery) || uri.includes(searchQuery); + }); + }, [props.ciphers, folderFilter, typeFilter, searchQuery]); + + useEffect(() => { + if (isCreating) return; + if (!filteredCiphers.length) { + if (selectedCipherId) setSelectedCipherId(''); + return; + } + if (!selectedCipherId || !filteredCiphers.some((x) => x.id === selectedCipherId)) { + setSelectedCipherId(filteredCiphers[0].id); + } + }, [filteredCiphers, selectedCipherId, isCreating]); + + const selectedCipher = useMemo( + () => props.ciphers.find((x) => x.id === selectedCipherId) || null, + [props.ciphers, selectedCipherId] + ); + + useEffect(() => { + const raw = selectedCipher?.login?.decTotp || ''; + if (!raw) { + setTotpLive(null); + return; + } + let stopped = false; + let timer = 0; + const tick = async () => { + try { + const now = await calcTotpNow(raw); + if (!stopped) setTotpLive(now); + } catch { + if (!stopped) setTotpLive(null); + } + }; + void tick(); + timer = window.setInterval(() => void tick(), 1000); + return () => { + stopped = true; + window.clearInterval(timer); + }; + }, [selectedCipher?.id, selectedCipher?.login?.decTotp]); + + const selectedCount = useMemo( + () => Object.values(selectedMap).reduce((sum, v) => sum + (v ? 1 : 0), 0), + [selectedMap] + ); + + function folderName(id: string | null | undefined): string { + if (!id) return 'No Folder'; + const folder = props.folders.find((x) => x.id === id); + return folder?.decName || folder?.name || id; + } + + function listSubtitle(cipher: Cipher): string { + if (Number(cipher.type || 1) === 1) { + return cipher.login?.decUsername || firstCipherUri(cipher) || ''; + } + return cipherTypeLabel(Number(cipher.type || 1)); + } + + function startCreate(type: number): void { + setDraft(createEmptyDraft(type)); + setIsCreating(true); + setIsEditing(true); + setCreateMenuOpen(false); + setSelectedCipherId(''); + setShowPassword(false); + setLocalError(''); + } + + function startEdit(): void { + if (!selectedCipher) return; + setDraft(draftFromCipher(selectedCipher)); + setIsCreating(false); + setIsEditing(true); + setShowPassword(false); + setLocalError(''); + } + + function cancelEdit(): void { + setDraft(null); + setIsEditing(false); + setIsCreating(false); + setLocalError(''); + } + + function updateDraft(patch: Partial): void { + setDraft((prev) => (prev ? { ...prev, ...patch } : prev)); + } + + function updateDraftCustomFields(nextFields: VaultDraftField[]): void { + setDraft((prev) => (prev ? { ...prev, customFields: nextFields } : prev)); + } + + function updateDraftLoginUri(index: number, value: string): void { + setDraft((prev) => { + if (!prev) return prev; + const next = [...prev.loginUris]; + next[index] = value; + return { ...prev, loginUris: next }; + }); + } + + async function saveDraft(): Promise { + if (!draft) return; + if (!draft.name.trim()) { + setLocalError('Item name is required.'); + return; + } + setBusy(true); + try { + if (isCreating) { + await props.onCreate(draft); + } else if (selectedCipher) { + await props.onUpdate(selectedCipher, draft); + } + setIsCreating(false); + setIsEditing(false); + setDraft(null); + setLocalError(''); + } finally { + setBusy(false); + } + } + + async function deleteSelected(): Promise { + if (!pendingDelete) return; + setBusy(true); + try { + await props.onDelete(pendingDelete); + setPendingDelete(null); + cancelEdit(); + } finally { + setBusy(false); + } + } + + async function confirmBulkDelete(): Promise { + const ids = Object.entries(selectedMap) + .filter(([, selected]) => selected) + .map(([id]) => id); + if (!ids.length) return; + setBusy(true); + try { + await props.onBulkDelete(ids); + setSelectedMap({}); + setBulkDeleteOpen(false); + } finally { + setBusy(false); + } + } + + async function confirmBulkMove(): Promise { + const ids = Object.entries(selectedMap) + .filter(([, selected]) => selected) + .map(([id]) => id); + if (!ids.length) return; + const folderId = moveFolderId === '__none__' ? null : moveFolderId; + setBusy(true); + try { + await props.onBulkMove(ids, folderId); + setSelectedMap({}); + setMoveOpen(false); + } finally { + setBusy(false); + } + } + + async function syncVault(): Promise { + setBusy(true); + try { + await props.onRefresh(); + } finally { + setBusy(false); + } + } + + return ( + <> +
    + + +
    +
    + + + + + +
    + + {createMenuOpen && ( +
    + {CREATE_TYPE_OPTIONS.map((option) => ( + + ))} +
    + )} +
    +
    + +
    + {filteredCiphers.map((cipher) => ( +
    + + setSelectedMap((prev) => ({ + ...prev, + [cipher.id]: (e.currentTarget as HTMLInputElement).checked, + })) + } + /> + +
    + ))} + {!filteredCiphers.length &&
    No items
    } +
    +
    + +
    + {isEditing && draft && ( + <> +
    +

    {isCreating ? `New ${cipherTypeLabel(draft.type)}` : `Edit ${cipherTypeLabel(draft.type)}`}

    +
    + + +
    + +
    + + {draft.type === 1 && ( +
    +

    Login Credentials

    +
    + + +
    + +
    +

    Websites

    + +
    + {draft.loginUris.map((uri, index) => ( +
    + updateDraftLoginUri(index, (e.currentTarget as HTMLInputElement).value)} /> + {draft.loginUris.length > 1 && ( + + )} +
    + ))} +
    + )} + + {draft.type === 3 && ( +
    +

    Card Details

    +
    + + + + + + +
    +
    + )} + + {draft.type === 4 && ( +
    +

    Identity Details

    +
    + + + + + + + + + + + + + + + + + + +
    +
    + )} + {draft.type === 5 && ( +
    +

    SSH Key

    +