feat: enhance deployment process and update dependencies

- Updated the deployment script to build the web application before deploying.
- Upgraded Wrangler dependency from 4.61.1 to 4.69.0.

feat: add import item limit and request body size limit

- Introduced a new limit for the maximum total items allowed in a single import (5000).
- Set a hard body size limit for JSON API endpoints (25 MB).

feat: validate KDF parameters during registration and password change

- Added validation for KDF parameters to ensure compliance with Bitwarden's minimum requirements.
- Enhanced error handling for invalid KDF parameters during user registration and password change.

feat: clean up R2 files on user deletion

- Implemented cleanup of R2 files associated with user attachments and sends before deleting user metadata.

feat: verify folder ownership when creating or updating ciphers

- Added checks to ensure that users cannot reference folders owned by other users when creating or updating ciphers.

fix: handle corrupted cipher data gracefully

- Improved error handling when retrieving ciphers from the database to avoid crashes due to corrupted data.

feat: increment send access count atomically

- Added a method to atomically increment the access count for sends and return whether the update was successful.

fix: enforce request body size limits

- Implemented checks to reject oversized request bodies for non-file upload paths.

fix: update error handling for database initialization

- Enhanced error logging for database initialization failures while providing a generic message to clients.

feat: enhance security with Content Security Policy

- Added a Content Security Policy to the web application to improve security against XSS attacks.

fix: remove plaintext TOTP secret from localStorage

- Updated the TOTP enabling process to remove the plaintext secret from localStorage after it is stored on the server.

fix: ensure only PBKDF2 hash is sent for public send access

- Modified the public send access payload to ensure only the PBKDF2 hash is sent, never the plaintext password.
This commit is contained in:
shuaiplus
2026-03-01 21:01:52 +08:00
committed by Shuai
parent 1a94f8dd44
commit 7d5681665f
18 changed files with 349 additions and 186 deletions
+2 -2
View File
@@ -85,12 +85,12 @@ npx wrangler d1 create nodewarden-db
npx wrangler r2 bucket create nodewarden-attachments npx wrangler r2 bucket create nodewarden-attachments
# 部署 # 部署
npx wrangler deploy npm run deploy
# 需更新时重新拉取仓库,重新部署即可,无需创建云资源 # 需更新时重新拉取仓库,重新部署即可,无需创建云资源
git clone https://github.com/shuaiplus/NodeWarden.git git clone https://github.com/shuaiplus/NodeWarden.git
cd NodeWarden cd NodeWarden
npx wrangler deploy npm run deploy
``` ```
--- ---
+2 -2
View File
@@ -87,12 +87,12 @@ npx wrangler d1 create nodewarden-db
npx wrangler r2 bucket create nodewarden-attachments npx wrangler r2 bucket create nodewarden-attachments
# Deploy # Deploy
npx wrangler deploy npm run deploy
# To update later: re-clone and re-deploy — no need to recreate cloud resources # To update later: re-clone and re-deploy — no need to recreate cloud resources
git clone https://github.com/shuaiplus/NodeWarden.git git clone https://github.com/shuaiplus/NodeWarden.git
cd NodeWarden cd NodeWarden
npx wrangler deploy npm run deploy
``` ```
--- ---
+156 -157
View File
@@ -22,7 +22,7 @@
"tsx": "^4.21.0", "tsx": "^4.21.0",
"typescript": "^5.9.3", "typescript": "^5.9.3",
"vite": "^7.3.1", "vite": "^7.3.1",
"wrangler": "^4.61.1" "wrangler": "^4.69.0"
} }
}, },
"node_modules/@babel/code-frame": { "node_modules/@babel/code-frame": {
@@ -383,14 +383,14 @@
} }
}, },
"node_modules/@cloudflare/unenv-preset": { "node_modules/@cloudflare/unenv-preset": {
"version": "2.12.0", "version": "2.14.0",
"resolved": "https://registry.npmmirror.com/@cloudflare/unenv-preset/-/unenv-preset-2.12.0.tgz", "resolved": "https://registry.npmmirror.com/@cloudflare/unenv-preset/-/unenv-preset-2.14.0.tgz",
"integrity": "sha512-NK4vN+2Z/GbfGS4BamtbbVk1rcu5RmqaYGiyHJQrA09AoxdZPHDF3W/EhgI0YSK8p3vRo/VNCtbSJFPON7FWMQ==", "integrity": "sha512-XKAkWhi1nBdNsSEoNG9nkcbyvfUrSjSf+VYVPfOto3gLTZVc3F4g6RASCMh6IixBKCG2yDgZKQIHGKtjcnLnKg==",
"dev": true, "dev": true,
"license": "MIT OR Apache-2.0", "license": "MIT OR Apache-2.0",
"peerDependencies": { "peerDependencies": {
"unenv": "2.0.0-rc.24", "unenv": "2.0.0-rc.24",
"workerd": "^1.20260115.0" "workerd": "^1.20260218.0"
}, },
"peerDependenciesMeta": { "peerDependenciesMeta": {
"workerd": { "workerd": {
@@ -399,9 +399,9 @@
} }
}, },
"node_modules/@cloudflare/workerd-darwin-64": { "node_modules/@cloudflare/workerd-darwin-64": {
"version": "1.20260128.0", "version": "1.20260305.0",
"resolved": "https://registry.npmmirror.com/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20260128.0.tgz", "resolved": "https://registry.npmmirror.com/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20260305.0.tgz",
"integrity": "sha512-XJN8zWWNG3JwAUqqwMLNKJ9fZfdlQkx/zTTHW/BB8wHat9LjKD6AzxqCu432YmfjR+NxEKCzUOxMu1YOxlVxmg==", "integrity": "sha512-chhKOpymo0Eh9J3nymrauMqKGboCc4uz/j0gA1G4gioMnKsN2ZDKJ+qjRZDnCoVGy8u2C4pxlmyIfsXCAfIzhQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -416,9 +416,9 @@
} }
}, },
"node_modules/@cloudflare/workerd-darwin-arm64": { "node_modules/@cloudflare/workerd-darwin-arm64": {
"version": "1.20260128.0", "version": "1.20260305.0",
"resolved": "https://registry.npmmirror.com/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20260128.0.tgz", "resolved": "https://registry.npmmirror.com/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20260305.0.tgz",
"integrity": "sha512-vKnRcmnm402GQ5DOdfT5H34qeR2m07nhnTtky8mTkNWP+7xmkz32AMdclwMmfO/iX9ncyKwSqmml2wPG32eq/w==", "integrity": "sha512-K9aG2OQk5bBfOP+fyGPqLcqZ9OR3ra6uwnxJ8f2mveq2A2LsCI7ZeGxQiAj75Ti80ytH/gJffZIx4Np2JtU3aQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -433,9 +433,9 @@
} }
}, },
"node_modules/@cloudflare/workerd-linux-64": { "node_modules/@cloudflare/workerd-linux-64": {
"version": "1.20260128.0", "version": "1.20260305.0",
"resolved": "https://registry.npmmirror.com/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20260128.0.tgz", "resolved": "https://registry.npmmirror.com/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20260305.0.tgz",
"integrity": "sha512-RiaR+Qugof/c6oI5SagD2J5wJmIfI8wQWaV2Y9905Raj6sAYOFaEKfzkKnoLLLNYb4NlXicBrffJi1j7R/ypUA==", "integrity": "sha512-tt7XUoIw/cYFeGbkPkcZ6XX1aZm26Aju/4ih+DXxOosbBeGshFSrNJDBfAKKOvkjsAZymJ+WWVDBU+hmNaGfwA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -450,9 +450,9 @@
} }
}, },
"node_modules/@cloudflare/workerd-linux-arm64": { "node_modules/@cloudflare/workerd-linux-arm64": {
"version": "1.20260128.0", "version": "1.20260305.0",
"resolved": "https://registry.npmmirror.com/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20260128.0.tgz", "resolved": "https://registry.npmmirror.com/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20260305.0.tgz",
"integrity": "sha512-U39U9vcXLXYDbrJ112Q7D0LDUUnM54oXfAxPgrL2goBwio7Z6RnsM25TRvm+Q06F4+FeDOC4D51JXlFHb9t1OA==", "integrity": "sha512-72QTkY5EzylmvCZ8ZTrnJ9DctmQsfSof1OKyOWqu/pv/B2yACfuPMikq8RpPxvVu7hhS0ztGP6ZvXz72Htq4Zg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -467,9 +467,9 @@
} }
}, },
"node_modules/@cloudflare/workerd-windows-64": { "node_modules/@cloudflare/workerd-windows-64": {
"version": "1.20260128.0", "version": "1.20260305.0",
"resolved": "https://registry.npmmirror.com/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20260128.0.tgz", "resolved": "https://registry.npmmirror.com/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20260305.0.tgz",
"integrity": "sha512-fdJwSqRkJsAJFJ7+jy0th2uMO6fwaDA8Ny6+iFCssfzlNkc4dP/twXo+3F66FMLMe/6NIqjzVts0cpiv7ERYbQ==", "integrity": "sha512-BA0uaQPOaI2F6mJtBDqplGnQQhpXCzwEMI33p/TnDxtSk9u8CGIfBFuI6uqo8mJ6ijIaPjeBLGOn2CiRMET4qg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -484,9 +484,9 @@
} }
}, },
"node_modules/@cloudflare/workers-types": { "node_modules/@cloudflare/workers-types": {
"version": "4.20260131.0", "version": "4.20260305.0",
"resolved": "https://registry.npmmirror.com/@cloudflare/workers-types/-/workers-types-4.20260131.0.tgz", "resolved": "https://registry.npmmirror.com/@cloudflare/workers-types/-/workers-types-4.20260305.0.tgz",
"integrity": "sha512-ELgvb2mp68Al50p+FmpgCO2hgU5o4tmz8pi7kShN+cRXc0UZoEdxpDIikR0CeT7b3tV7wlnEnsUzd0UoJLS0oQ==", "integrity": "sha512-sCgPFnQ03SVpC2OVW8wysONLZW/A8hlp9Mq2ckG/h1oId4kr9NawA6vUiOmOjCWRn2hIohejBYVQ+Vu20rCdKA==",
"dev": true, "dev": true,
"license": "MIT OR Apache-2.0", "license": "MIT OR Apache-2.0",
"peer": true "peer": true
@@ -516,9 +516,9 @@
} }
}, },
"node_modules/@esbuild/aix-ppc64": { "node_modules/@esbuild/aix-ppc64": {
"version": "0.27.0", "version": "0.27.3",
"resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.27.0.tgz", "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz",
"integrity": "sha512-KuZrd2hRjz01y5JK9mEBSD3Vj3mbCvemhT466rSuJYeE/hjuBrHfjjcjMdTm/sz7au+++sdbJZJmuBwQLuw68A==", "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==",
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
@@ -533,9 +533,9 @@
} }
}, },
"node_modules/@esbuild/android-arm": { "node_modules/@esbuild/android-arm": {
"version": "0.27.0", "version": "0.27.3",
"resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.27.0.tgz", "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.27.3.tgz",
"integrity": "sha512-j67aezrPNYWJEOHUNLPj9maeJte7uSMM6gMoxfPC9hOg8N02JuQi/T7ewumf4tNvJadFkvLZMlAq73b9uwdMyQ==", "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@@ -550,9 +550,9 @@
} }
}, },
"node_modules/@esbuild/android-arm64": { "node_modules/@esbuild/android-arm64": {
"version": "0.27.0", "version": "0.27.3",
"resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.27.0.tgz", "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz",
"integrity": "sha512-CC3vt4+1xZrs97/PKDkl0yN7w8edvU2vZvAFGD16n9F0Cvniy5qvzRXjfO1l94efczkkQE6g1x0i73Qf5uthOQ==", "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -567,9 +567,9 @@
} }
}, },
"node_modules/@esbuild/android-x64": { "node_modules/@esbuild/android-x64": {
"version": "0.27.0", "version": "0.27.3",
"resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.27.0.tgz", "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.27.3.tgz",
"integrity": "sha512-wurMkF1nmQajBO1+0CJmcN17U4BP6GqNSROP8t0X/Jiw2ltYGLHpEksp9MpoBqkrFR3kv2/te6Sha26k3+yZ9Q==", "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -584,9 +584,9 @@
} }
}, },
"node_modules/@esbuild/darwin-arm64": { "node_modules/@esbuild/darwin-arm64": {
"version": "0.27.0", "version": "0.27.3",
"resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.27.0.tgz", "resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz",
"integrity": "sha512-uJOQKYCcHhg07DL7i8MzjvS2LaP7W7Pn/7uA0B5S1EnqAirJtbyw4yC5jQ5qcFjHK9l6o/MX9QisBg12kNkdHg==", "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -601,9 +601,9 @@
} }
}, },
"node_modules/@esbuild/darwin-x64": { "node_modules/@esbuild/darwin-x64": {
"version": "0.27.0", "version": "0.27.3",
"resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.27.0.tgz", "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz",
"integrity": "sha512-8mG6arH3yB/4ZXiEnXof5MK72dE6zM9cDvUcPtxhUZsDjESl9JipZYW60C3JGreKCEP+p8P/72r69m4AZGJd5g==", "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -618,9 +618,9 @@
} }
}, },
"node_modules/@esbuild/freebsd-arm64": { "node_modules/@esbuild/freebsd-arm64": {
"version": "0.27.0", "version": "0.27.3",
"resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.0.tgz", "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz",
"integrity": "sha512-9FHtyO988CwNMMOE3YIeci+UV+x5Zy8fI2qHNpsEtSF83YPBmE8UWmfYAQg6Ux7Gsmd4FejZqnEUZCMGaNQHQw==", "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -635,9 +635,9 @@
} }
}, },
"node_modules/@esbuild/freebsd-x64": { "node_modules/@esbuild/freebsd-x64": {
"version": "0.27.0", "version": "0.27.3",
"resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.27.0.tgz", "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz",
"integrity": "sha512-zCMeMXI4HS/tXvJz8vWGexpZj2YVtRAihHLk1imZj4efx1BQzN76YFeKqlDr3bUWI26wHwLWPd3rwh6pe4EV7g==", "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -652,9 +652,9 @@
} }
}, },
"node_modules/@esbuild/linux-arm": { "node_modules/@esbuild/linux-arm": {
"version": "0.27.0", "version": "0.27.3",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.27.0.tgz", "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz",
"integrity": "sha512-t76XLQDpxgmq2cNXKTVEB7O7YMb42atj2Re2Haf45HkaUpjM2J0UuJZDuaGbPbamzZ7bawyGFUkodL+zcE+jvQ==", "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@@ -669,9 +669,9 @@
} }
}, },
"node_modules/@esbuild/linux-arm64": { "node_modules/@esbuild/linux-arm64": {
"version": "0.27.0", "version": "0.27.3",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.27.0.tgz", "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz",
"integrity": "sha512-AS18v0V+vZiLJyi/4LphvBE+OIX682Pu7ZYNsdUHyUKSoRwdnOsMf6FDekwoAFKej14WAkOef3zAORJgAtXnlQ==", "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -686,9 +686,9 @@
} }
}, },
"node_modules/@esbuild/linux-ia32": { "node_modules/@esbuild/linux-ia32": {
"version": "0.27.0", "version": "0.27.3",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.27.0.tgz", "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz",
"integrity": "sha512-Mz1jxqm/kfgKkc/KLHC5qIujMvnnarD9ra1cEcrs7qshTUSksPihGrWHVG5+osAIQ68577Zpww7SGapmzSt4Nw==", "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==",
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
@@ -703,9 +703,9 @@
} }
}, },
"node_modules/@esbuild/linux-loong64": { "node_modules/@esbuild/linux-loong64": {
"version": "0.27.0", "version": "0.27.3",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.27.0.tgz", "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz",
"integrity": "sha512-QbEREjdJeIreIAbdG2hLU1yXm1uu+LTdzoq1KCo4G4pFOLlvIspBm36QrQOar9LFduavoWX2msNFAAAY9j4BDg==", "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==",
"cpu": [ "cpu": [
"loong64" "loong64"
], ],
@@ -720,9 +720,9 @@
} }
}, },
"node_modules/@esbuild/linux-mips64el": { "node_modules/@esbuild/linux-mips64el": {
"version": "0.27.0", "version": "0.27.3",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.27.0.tgz", "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz",
"integrity": "sha512-sJz3zRNe4tO2wxvDpH/HYJilb6+2YJxo/ZNbVdtFiKDufzWq4JmKAiHy9iGoLjAV7r/W32VgaHGkk35cUXlNOg==", "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==",
"cpu": [ "cpu": [
"mips64el" "mips64el"
], ],
@@ -737,9 +737,9 @@
} }
}, },
"node_modules/@esbuild/linux-ppc64": { "node_modules/@esbuild/linux-ppc64": {
"version": "0.27.0", "version": "0.27.3",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.27.0.tgz", "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz",
"integrity": "sha512-z9N10FBD0DCS2dmSABDBb5TLAyF1/ydVb+N4pi88T45efQ/w4ohr/F/QYCkxDPnkhkp6AIpIcQKQ8F0ANoA2JA==", "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==",
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
@@ -754,9 +754,9 @@
} }
}, },
"node_modules/@esbuild/linux-riscv64": { "node_modules/@esbuild/linux-riscv64": {
"version": "0.27.0", "version": "0.27.3",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.27.0.tgz", "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz",
"integrity": "sha512-pQdyAIZ0BWIC5GyvVFn5awDiO14TkT/19FTmFcPdDec94KJ1uZcmFs21Fo8auMXzD4Tt+diXu1LW1gHus9fhFQ==", "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==",
"cpu": [ "cpu": [
"riscv64" "riscv64"
], ],
@@ -771,9 +771,9 @@
} }
}, },
"node_modules/@esbuild/linux-s390x": { "node_modules/@esbuild/linux-s390x": {
"version": "0.27.0", "version": "0.27.3",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.27.0.tgz", "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz",
"integrity": "sha512-hPlRWR4eIDDEci953RI1BLZitgi5uqcsjKMxwYfmi4LcwyWo2IcRP+lThVnKjNtk90pLS8nKdroXYOqW+QQH+w==", "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==",
"cpu": [ "cpu": [
"s390x" "s390x"
], ],
@@ -788,9 +788,9 @@
} }
}, },
"node_modules/@esbuild/linux-x64": { "node_modules/@esbuild/linux-x64": {
"version": "0.27.0", "version": "0.27.3",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.27.0.tgz", "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz",
"integrity": "sha512-1hBWx4OUJE2cab++aVZ7pObD6s+DK4mPGpemtnAORBvb5l/g5xFGk0vc0PjSkrDs0XaXj9yyob3d14XqvnQ4gw==", "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -805,9 +805,9 @@
} }
}, },
"node_modules/@esbuild/netbsd-arm64": { "node_modules/@esbuild/netbsd-arm64": {
"version": "0.27.0", "version": "0.27.3",
"resolved": "https://registry.npmmirror.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.0.tgz", "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz",
"integrity": "sha512-6m0sfQfxfQfy1qRuecMkJlf1cIzTOgyaeXaiVaaki8/v+WB+U4hc6ik15ZW6TAllRlg/WuQXxWj1jx6C+dfy3w==", "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -822,9 +822,9 @@
} }
}, },
"node_modules/@esbuild/netbsd-x64": { "node_modules/@esbuild/netbsd-x64": {
"version": "0.27.0", "version": "0.27.3",
"resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.27.0.tgz", "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz",
"integrity": "sha512-xbbOdfn06FtcJ9d0ShxxvSn2iUsGd/lgPIO2V3VZIPDbEaIj1/3nBBe1AwuEZKXVXkMmpr6LUAgMkLD/4D2PPA==", "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -839,9 +839,9 @@
} }
}, },
"node_modules/@esbuild/openbsd-arm64": { "node_modules/@esbuild/openbsd-arm64": {
"version": "0.27.0", "version": "0.27.3",
"resolved": "https://registry.npmmirror.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.0.tgz", "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz",
"integrity": "sha512-fWgqR8uNbCQ/GGv0yhzttj6sU/9Z5/Sv/VGU3F5OuXK6J6SlriONKrQ7tNlwBrJZXRYk5jUhuWvF7GYzGguBZQ==", "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -856,9 +856,9 @@
} }
}, },
"node_modules/@esbuild/openbsd-x64": { "node_modules/@esbuild/openbsd-x64": {
"version": "0.27.0", "version": "0.27.3",
"resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.27.0.tgz", "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz",
"integrity": "sha512-aCwlRdSNMNxkGGqQajMUza6uXzR/U0dIl1QmLjPtRbLOx3Gy3otfFu/VjATy4yQzo9yFDGTxYDo1FfAD9oRD2A==", "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -873,9 +873,9 @@
} }
}, },
"node_modules/@esbuild/openharmony-arm64": { "node_modules/@esbuild/openharmony-arm64": {
"version": "0.27.0", "version": "0.27.3",
"resolved": "https://registry.npmmirror.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.0.tgz", "resolved": "https://registry.npmmirror.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz",
"integrity": "sha512-nyvsBccxNAsNYz2jVFYwEGuRRomqZ149A39SHWk4hV0jWxKM0hjBPm3AmdxcbHiFLbBSwG6SbpIcUbXjgyECfA==", "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -890,9 +890,9 @@
} }
}, },
"node_modules/@esbuild/sunos-x64": { "node_modules/@esbuild/sunos-x64": {
"version": "0.27.0", "version": "0.27.3",
"resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.27.0.tgz", "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz",
"integrity": "sha512-Q1KY1iJafM+UX6CFEL+F4HRTgygmEW568YMqDA5UV97AuZSm21b7SXIrRJDwXWPzr8MGr75fUZPV67FdtMHlHA==", "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -907,9 +907,9 @@
} }
}, },
"node_modules/@esbuild/win32-arm64": { "node_modules/@esbuild/win32-arm64": {
"version": "0.27.0", "version": "0.27.3",
"resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.27.0.tgz", "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz",
"integrity": "sha512-W1eyGNi6d+8kOmZIwi/EDjrL9nxQIQ0MiGqe/AWc6+IaHloxHSGoeRgDRKHFISThLmsewZ5nHFvGFWdBYlgKPg==", "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -924,9 +924,9 @@
} }
}, },
"node_modules/@esbuild/win32-ia32": { "node_modules/@esbuild/win32-ia32": {
"version": "0.27.0", "version": "0.27.3",
"resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.27.0.tgz", "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz",
"integrity": "sha512-30z1aKL9h22kQhilnYkORFYt+3wp7yZsHWus+wSKAJR8JtdfI76LJ4SBdMsCopTR3z/ORqVu5L1vtnHZWVj4cQ==", "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==",
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
@@ -941,9 +941,9 @@
} }
}, },
"node_modules/@esbuild/win32-x64": { "node_modules/@esbuild/win32-x64": {
"version": "0.27.0", "version": "0.27.3",
"resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.27.0.tgz", "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz",
"integrity": "sha512-aIitBcjQeyOhMTImhLZmtxfdOcuNRpwlPNmlFKPcHQYPhEssw75Cl1TSXJXpMkzaua9FUetx/4OQKq7eJul5Cg==", "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -958,9 +958,9 @@
} }
}, },
"node_modules/@img/colour": { "node_modules/@img/colour": {
"version": "1.0.0", "version": "1.1.0",
"resolved": "https://registry.npmmirror.com/@img/colour/-/colour-1.0.0.tgz", "resolved": "https://registry.npmmirror.com/@img/colour/-/colour-1.1.0.tgz",
"integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==", "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@@ -2337,9 +2337,9 @@
} }
}, },
"node_modules/esbuild": { "node_modules/esbuild": {
"version": "0.27.0", "version": "0.27.3",
"resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.27.0.tgz", "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.27.3.tgz",
"integrity": "sha512-jd0f4NHbD6cALCyGElNpGAOtWxSq46l9X/sWB0Nzd5er4Kz2YTm+Vl0qKFT9KUJvD8+fiO8AvoHhFvEatfVixA==", "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==",
"dev": true, "dev": true,
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT", "license": "MIT",
@@ -2350,32 +2350,32 @@
"node": ">=18" "node": ">=18"
}, },
"optionalDependencies": { "optionalDependencies": {
"@esbuild/aix-ppc64": "0.27.0", "@esbuild/aix-ppc64": "0.27.3",
"@esbuild/android-arm": "0.27.0", "@esbuild/android-arm": "0.27.3",
"@esbuild/android-arm64": "0.27.0", "@esbuild/android-arm64": "0.27.3",
"@esbuild/android-x64": "0.27.0", "@esbuild/android-x64": "0.27.3",
"@esbuild/darwin-arm64": "0.27.0", "@esbuild/darwin-arm64": "0.27.3",
"@esbuild/darwin-x64": "0.27.0", "@esbuild/darwin-x64": "0.27.3",
"@esbuild/freebsd-arm64": "0.27.0", "@esbuild/freebsd-arm64": "0.27.3",
"@esbuild/freebsd-x64": "0.27.0", "@esbuild/freebsd-x64": "0.27.3",
"@esbuild/linux-arm": "0.27.0", "@esbuild/linux-arm": "0.27.3",
"@esbuild/linux-arm64": "0.27.0", "@esbuild/linux-arm64": "0.27.3",
"@esbuild/linux-ia32": "0.27.0", "@esbuild/linux-ia32": "0.27.3",
"@esbuild/linux-loong64": "0.27.0", "@esbuild/linux-loong64": "0.27.3",
"@esbuild/linux-mips64el": "0.27.0", "@esbuild/linux-mips64el": "0.27.3",
"@esbuild/linux-ppc64": "0.27.0", "@esbuild/linux-ppc64": "0.27.3",
"@esbuild/linux-riscv64": "0.27.0", "@esbuild/linux-riscv64": "0.27.3",
"@esbuild/linux-s390x": "0.27.0", "@esbuild/linux-s390x": "0.27.3",
"@esbuild/linux-x64": "0.27.0", "@esbuild/linux-x64": "0.27.3",
"@esbuild/netbsd-arm64": "0.27.0", "@esbuild/netbsd-arm64": "0.27.3",
"@esbuild/netbsd-x64": "0.27.0", "@esbuild/netbsd-x64": "0.27.3",
"@esbuild/openbsd-arm64": "0.27.0", "@esbuild/openbsd-arm64": "0.27.3",
"@esbuild/openbsd-x64": "0.27.0", "@esbuild/openbsd-x64": "0.27.3",
"@esbuild/openharmony-arm64": "0.27.0", "@esbuild/openharmony-arm64": "0.27.3",
"@esbuild/sunos-x64": "0.27.0", "@esbuild/sunos-x64": "0.27.3",
"@esbuild/win32-arm64": "0.27.0", "@esbuild/win32-arm64": "0.27.3",
"@esbuild/win32-ia32": "0.27.0", "@esbuild/win32-ia32": "0.27.3",
"@esbuild/win32-x64": "0.27.0" "@esbuild/win32-x64": "0.27.3"
} }
}, },
"node_modules/escalade": { "node_modules/escalade": {
@@ -2541,16 +2541,16 @@
} }
}, },
"node_modules/miniflare": { "node_modules/miniflare": {
"version": "4.20260128.0", "version": "4.20260305.0",
"resolved": "https://registry.npmmirror.com/miniflare/-/miniflare-4.20260128.0.tgz", "resolved": "https://registry.npmmirror.com/miniflare/-/miniflare-4.20260305.0.tgz",
"integrity": "sha512-AVCn3vDRY+YXu1sP4mRn81ssno6VUqxo29uY2QVfgxXU2TMLvhRIoGwm7RglJ3Gzfuidit5R86CMQ6AvdFTGAw==", "integrity": "sha512-jVhtKJtiwaZa3rI+WgoLvSJmEazDsoUmAPYRUmEe2VO6VSbvkhbnDRm+dsPbYRatgNIExwrpqG1rv96jHiSb0w==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@cspotcode/source-map-support": "0.8.1", "@cspotcode/source-map-support": "0.8.1",
"sharp": "^0.34.5", "sharp": "^0.34.5",
"undici": "7.18.2", "undici": "7.18.2",
"workerd": "1.20260128.0", "workerd": "1.20260305.0",
"ws": "8.18.0", "ws": "8.18.0",
"youch": "4.1.0-beta.10" "youch": "4.1.0-beta.10"
}, },
@@ -2780,9 +2780,9 @@
} }
}, },
"node_modules/semver": { "node_modules/semver": {
"version": "7.7.3", "version": "7.7.4",
"resolved": "https://registry.npmmirror.com/semver/-/semver-7.7.3.tgz", "resolved": "https://registry.npmmirror.com/semver/-/semver-7.7.4.tgz",
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
"dev": true, "dev": true,
"license": "ISC", "license": "ISC",
"bin": { "bin": {
@@ -3113,13 +3113,12 @@
} }
}, },
"node_modules/workerd": { "node_modules/workerd": {
"version": "1.20260128.0", "version": "1.20260305.0",
"resolved": "https://registry.npmmirror.com/workerd/-/workerd-1.20260128.0.tgz", "resolved": "https://registry.npmmirror.com/workerd/-/workerd-1.20260305.0.tgz",
"integrity": "sha512-EhLJGptSGFi8AEErLiamO3PoGpbRqL+v4Ve36H2B38VxmDgFOSmDhfepBnA14sCQzGf1AEaoZX2DCwZsmO74yQ==", "integrity": "sha512-JkhfCLU+w+KbQmZ9k49IcDYc78GBo7eG8Mir8E2+KVjR7otQAmpcLlsous09YLh8WQ3Bt3Mi6/WMStvMAPukeA==",
"dev": true, "dev": true,
"hasInstallScript": true, "hasInstallScript": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"bin": { "bin": {
"workerd": "bin/workerd" "workerd": "bin/workerd"
}, },
@@ -3127,11 +3126,11 @@
"node": ">=16" "node": ">=16"
}, },
"optionalDependencies": { "optionalDependencies": {
"@cloudflare/workerd-darwin-64": "1.20260128.0", "@cloudflare/workerd-darwin-64": "1.20260305.0",
"@cloudflare/workerd-darwin-arm64": "1.20260128.0", "@cloudflare/workerd-darwin-arm64": "1.20260305.0",
"@cloudflare/workerd-linux-64": "1.20260128.0", "@cloudflare/workerd-linux-64": "1.20260305.0",
"@cloudflare/workerd-linux-arm64": "1.20260128.0", "@cloudflare/workerd-linux-arm64": "1.20260305.0",
"@cloudflare/workerd-windows-64": "1.20260128.0" "@cloudflare/workerd-windows-64": "1.20260305.0"
} }
}, },
"node_modules/wouter": { "node_modules/wouter": {
@@ -3149,20 +3148,20 @@
} }
}, },
"node_modules/wrangler": { "node_modules/wrangler": {
"version": "4.61.1", "version": "4.69.0",
"resolved": "https://registry.npmmirror.com/wrangler/-/wrangler-4.61.1.tgz", "resolved": "https://registry.npmmirror.com/wrangler/-/wrangler-4.69.0.tgz",
"integrity": "sha512-hfYQ16VLPkNi8xE1/V3052S2stM5e+vq3Idpt83sXoDC3R7R1CLgMkK6M6+Qp3G+9GVDNyHCkvohMPdfFTaD4Q==", "integrity": "sha512-EmVfIM65I5b4ITHe3Y9R7zQyf4NUBQ1leStakMlWiVR9n6VlDwuEltyQI2l3i0JciDnWyR3uqe+T6C08ivniTQ==",
"dev": true, "dev": true,
"license": "MIT OR Apache-2.0", "license": "MIT OR Apache-2.0",
"dependencies": { "dependencies": {
"@cloudflare/kv-asset-handler": "0.4.2", "@cloudflare/kv-asset-handler": "0.4.2",
"@cloudflare/unenv-preset": "2.12.0", "@cloudflare/unenv-preset": "2.14.0",
"blake3-wasm": "2.1.5", "blake3-wasm": "2.1.5",
"esbuild": "0.27.0", "esbuild": "0.27.3",
"miniflare": "4.20260128.0", "miniflare": "4.20260305.0",
"path-to-regexp": "6.3.0", "path-to-regexp": "6.3.0",
"unenv": "2.0.0-rc.24", "unenv": "2.0.0-rc.24",
"workerd": "1.20260128.0" "workerd": "1.20260305.0"
}, },
"bin": { "bin": {
"wrangler": "bin/wrangler.js", "wrangler": "bin/wrangler.js",
@@ -3175,7 +3174,7 @@
"fsevents": "~2.3.2" "fsevents": "~2.3.2"
}, },
"peerDependencies": { "peerDependencies": {
"@cloudflare/workers-types": "^4.20260128.0" "@cloudflare/workers-types": "^4.20260305.0"
}, },
"peerDependenciesMeta": { "peerDependenciesMeta": {
"@cloudflare/workers-types": { "@cloudflare/workers-types": {
+2 -3
View File
@@ -12,8 +12,7 @@
"web:dev": "vite --config webapp/vite.config.ts", "web:dev": "vite --config webapp/vite.config.ts",
"web:build": "vite build --config webapp/vite.config.ts", "web:build": "vite build --config webapp/vite.config.ts",
"web:typecheck": "tsc -p webapp/tsconfig.json --noEmit", "web:typecheck": "tsc -p webapp/tsconfig.json --noEmit",
"deploymy": "wrangler deploy -c wrangler.my.toml", "deploy": "npm run web:build && wrangler deploy"
"deploy": "wrangler deploy"
}, },
"keywords": [ "keywords": [
"bitwarden", "bitwarden",
@@ -42,7 +41,7 @@
"tsx": "^4.21.0", "tsx": "^4.21.0",
"typescript": "^5.9.3", "typescript": "^5.9.3",
"vite": "^7.3.1", "vite": "^7.3.1",
"wrangler": "^4.61.1" "wrangler": "^4.69.0"
}, },
"dependencies": { "dependencies": {
"@tanstack/react-query": "^5.90.21", "@tanstack/react-query": "^5.90.21",
+8
View File
@@ -103,6 +103,14 @@
// Max IDs per SQL batch when moving ciphers in bulk. // Max IDs per SQL batch when moving ciphers in bulk.
// 批量移动密码项时每批 SQL 的最大 ID 数量。 // 批量移动密码项时每批 SQL 的最大 ID 数量。
bulkMoveChunkSize: 200, bulkMoveChunkSize: 200,
// Max total items (folders + ciphers) allowed in a single import.
// 单次导入允许的最大条目数(文件夹 + 密码项合计)。
importItemLimit: 5000,
},
request: {
// Hard body size limit for JSON API endpoints (bytes). File upload paths are exempt.
// JSON 接口请求 body 大小上限(字节),文件上传接口除外。
maxBodyBytes: 25 * 1024 * 1024,
}, },
compatibility: { compatibility: {
// Single source of truth for /config.version and /api/version. // Single source of truth for /config.version and /api/version.
+44
View File
@@ -18,6 +18,32 @@ function looksLikeEncString(value: string): boolean {
return parts.length >= 2; return parts.length >= 2;
} }
/**
* Validate KDF parameters according to Bitwarden minimum requirements.
* Returns an error message if invalid, or null if OK.
*/
function validateKdfParams(kdfType: number | undefined, kdfIterations: number | undefined, kdfMemory?: number | undefined, kdfParallelism?: number | undefined): string | null {
const type = kdfType ?? 0;
if (type === 0) {
// PBKDF2-SHA256: minimum 100 000 iterations
if (typeof kdfIterations === 'number' && kdfIterations < 100_000) {
return 'PBKDF2 iterations must be at least 100000';
}
} else if (type === 1) {
// Argon2id: iterations >= 2, memory >= 16 MiB, parallelism >= 1
if (typeof kdfIterations === 'number' && kdfIterations < 2) {
return 'Argon2id iterations must be at least 2';
}
if (typeof kdfMemory === 'number' && kdfMemory < 16) {
return 'Argon2id memory must be at least 16 MiB';
}
if (typeof kdfParallelism === 'number' && kdfParallelism < 1) {
return 'Argon2id parallelism must be at least 1';
}
}
return null;
}
function normalizeTotpSecret(input: string): string { function normalizeTotpSecret(input: string): string {
return input.toUpperCase().replace(/[\s-]/g, '').replace(/=+$/g, ''); return input.toUpperCase().replace(/[\s-]/g, '').replace(/=+$/g, '');
} }
@@ -111,6 +137,9 @@ export async function handleRegister(request: Request, env: Env): Promise<Respon
if (!email || !masterPasswordHash || !key) { if (!email || !masterPasswordHash || !key) {
return errorResponse('Email, masterPasswordHash, and key are required', 400); return errorResponse('Email, masterPasswordHash, and key are required', 400);
} }
if (!email.includes('@') || email.length < 3) {
return errorResponse('Invalid email address', 400);
}
if (!privateKey || !publicKey) { if (!privateKey || !publicKey) {
return errorResponse('Private key and public key are required', 400); return errorResponse('Private key and public key are required', 400);
} }
@@ -121,6 +150,9 @@ export async function handleRegister(request: Request, env: Env): Promise<Respon
return errorResponse('encryptedPrivateKey is not a valid encrypted string', 400); return errorResponse('encryptedPrivateKey is not a valid encrypted string', 400);
} }
const kdfErr = validateKdfParams(body.kdf, body.kdfIterations, body.kdfMemory, body.kdfParallelism);
if (kdfErr) return errorResponse(kdfErr, 400);
const now = new Date().toISOString(); const now = new Date().toISOString();
const auth = new AuthService(env); const auth = new AuthService(env);
const serverHash = await auth.hashPasswordServer(masterPasswordHash, email); const serverHash = await auth.hashPasswordServer(masterPasswordHash, email);
@@ -338,6 +370,9 @@ export async function handleChangePassword(request: Request, env: Env, userId: s
return errorResponse('new encryptedPrivateKey is not a valid encrypted string', 400); return errorResponse('new encryptedPrivateKey is not a valid encrypted string', 400);
} }
const kdfErr = validateKdfParams(body.kdf ?? user.kdfType, body.kdfIterations, body.kdfMemory, body.kdfParallelism);
if (kdfErr) return errorResponse(kdfErr, 400);
user.masterPasswordHash = await auth.hashPasswordServer(body.newMasterPasswordHash, user.email); user.masterPasswordHash = await auth.hashPasswordServer(body.newMasterPasswordHash, user.email);
if (nextKey) user.key = nextKey; if (nextKey) user.key = nextKey;
if (nextPrivateKey) user.privateKey = nextPrivateKey; if (nextPrivateKey) user.privateKey = nextPrivateKey;
@@ -350,6 +385,15 @@ export async function handleChangePassword(request: Request, env: Env, userId: s
user.updatedAt = new Date().toISOString(); user.updatedAt = new Date().toISOString();
await storage.saveUser(user); await storage.saveUser(user);
await storage.deleteRefreshTokensByUserId(user.id); await storage.deleteRefreshTokensByUserId(user.id);
await storage.createAuditLog({
id: generateUUID(),
actorUserId: user.id,
action: 'user.password.change',
targetType: 'user',
targetId: user.id,
metadata: JSON.stringify({ email: user.email }),
createdAt: user.updatedAt,
});
return new Response(null, { status: 200 }); return new Response(null, { status: 200 });
} }
+22
View File
@@ -255,6 +255,28 @@ export async function handleAdminDeleteUser(
return errorResponse('User not found', 404); return errorResponse('User not found', 404);
} }
// Clean up R2 files before DB cascade deletes the metadata rows.
// 1. Attachment files (keyed by cipherId/attachmentId)
const attachmentMap = await storage.getAttachmentsByUserId(target.id);
for (const [cipherId, attachments] of attachmentMap) {
for (const att of attachments) {
await env.ATTACHMENTS.delete(`${cipherId}/${att.id}`);
}
}
// 2. Send files (keyed by sends/sendId/fileId)
const sends = await storage.getAllSends(target.id);
for (const send of sends) {
if (send.type === 1) { // SendType.File
try {
const parsed = JSON.parse(send.data) as Record<string, unknown>;
const fileId = typeof parsed.id === 'string' ? parsed.id : null;
if (fileId) {
await env.ATTACHMENTS.delete(`sends/${send.id}/${fileId}`);
}
} catch { /* non-file send or bad data, skip */ }
}
}
await storage.deleteRefreshTokensByUserId(target.id); await storage.deleteRefreshTokensByUserId(target.id);
await storage.deleteUserById(target.id); await storage.deleteUserById(target.id);
await writeAuditLog(storage, actorUser.id, 'admin.user.delete', 'user', target.id, { await writeAuditLog(storage, actorUser.id, 'admin.user.delete', 'user', target.id, {
+27
View File
@@ -144,6 +144,12 @@ export async function handleGetCipher(request: Request, env: Env, userId: string
return jsonResponse(cipherToResponse(cipher, attachments)); return jsonResponse(cipherToResponse(cipher, attachments));
} }
async function verifyFolderOwnership(storage: StorageService, folderId: string | null | undefined, userId: string): Promise<boolean> {
if (!folderId) return true;
const folder = await storage.getFolder(folderId);
return !!(folder && folder.userId === userId);
}
// POST /api/ciphers // POST /api/ciphers
export async function handleCreateCipher(request: Request, env: Env, userId: string): Promise<Response> { export async function handleCreateCipher(request: Request, env: Env, userId: string): Promise<Response> {
const storage = new StorageService(env.DB); const storage = new StorageService(env.DB);
@@ -178,6 +184,12 @@ export async function handleCreateCipher(request: Request, env: Env, userId: str
const createFields = getAliasedProp(cipherData, ['fields', 'Fields']); const createFields = getAliasedProp(cipherData, ['fields', 'Fields']);
cipher.fields = createFields.present ? (createFields.value ?? null) : (cipher.fields ?? null); cipher.fields = createFields.present ? (createFields.value ?? null) : (cipher.fields ?? null);
// Prevent referencing a folder owned by another user.
if (cipher.folderId) {
const folderOk = await verifyFolderOwnership(storage, cipher.folderId, userId);
if (!folderOk) return errorResponse('Folder not found', 404);
}
await storage.saveCipher(cipher); await storage.saveCipher(cipher);
await storage.updateRevisionDate(userId); await storage.updateRevisionDate(userId);
@@ -232,6 +244,12 @@ export async function handleUpdateCipher(request: Request, env: Env, userId: str
cipher.fields = null; cipher.fields = null;
} }
// Prevent referencing a folder owned by another user.
if (cipher.folderId) {
const folderOk = await verifyFolderOwnership(storage, cipher.folderId, userId);
if (!folderOk) return errorResponse('Folder not found', 404);
}
await storage.saveCipher(cipher); await storage.saveCipher(cipher);
await storage.updateRevisionDate(userId); await storage.updateRevisionDate(userId);
@@ -331,6 +349,10 @@ export async function handlePartialUpdateCipher(request: Request, env: Env, user
} }
if (body.folderId !== undefined) { if (body.folderId !== undefined) {
if (body.folderId) {
const folderOk = await verifyFolderOwnership(storage, body.folderId, userId);
if (!folderOk) return errorResponse('Folder not found', 404);
}
cipher.folderId = body.folderId; cipher.folderId = body.folderId;
} }
if (body.favorite !== undefined) { if (body.favorite !== undefined) {
@@ -359,6 +381,11 @@ export async function handleBulkMoveCiphers(request: Request, env: Env, userId:
return errorResponse('ids array is required', 400); return errorResponse('ids array is required', 400);
} }
if (body.folderId) {
const folderOk = await verifyFolderOwnership(storage, body.folderId, userId);
if (!folderOk) return errorResponse('Folder not found', 404);
}
await storage.bulkMoveCiphers(body.ids, body.folderId || null, userId); await storage.bulkMoveCiphers(body.ids, body.folderId || null, userId);
return new Response(null, { status: 204 }); return new Response(null, { status: 204 });
+4 -2
View File
@@ -391,8 +391,10 @@ export async function handlePrelogin(request: Request, env: Env): Promise<Respon
// Return default KDF settings even if user doesn't exist (to prevent user enumeration) // Return default KDF settings even if user doesn't exist (to prevent user enumeration)
const kdfType = user?.kdfType ?? 0; const kdfType = user?.kdfType ?? 0;
const kdfIterations = user?.kdfIterations ?? LIMITS.auth.defaultKdfIterations; const kdfIterations = user?.kdfIterations ?? LIMITS.auth.defaultKdfIterations;
const kdfMemory = user?.kdfMemory; // Use ?? null so non-existent users return null (not undefined/omitted) for these fields,
const kdfParallelism = user?.kdfParallelism; // matching the response shape of real PBKDF2 users and reducing enumeration signal.
const kdfMemory = user?.kdfMemory ?? null;
const kdfParallelism = user?.kdfParallelism ?? null;
return jsonResponse({ return jsonResponse({
kdf: kdfType, kdf: kdfType,
+4
View File
@@ -102,6 +102,10 @@ export async function handleCiphersImport(request: Request, env: Env, userId: st
const ciphers = importData.ciphers || []; const ciphers = importData.ciphers || [];
const folderRelationships = importData.folderRelationships || []; const folderRelationships = importData.folderRelationships || [];
if (folders.length + ciphers.length > LIMITS.performance.importItemLimit) {
return errorResponse(`Import exceeds maximum of ${LIMITS.performance.importItemLimit} items`, 400);
}
const now = new Date().toISOString(); const now = new Date().toISOString();
const batchChunkSize = LIMITS.performance.bulkMoveChunkSize; const batchChunkSize = LIMITS.performance.bulkMoveChunkSize;
+16 -8
View File
@@ -1022,9 +1022,11 @@ export async function handleAccessSend(request: Request, env: Env, accessId: str
} }
if (send.type === SendType.Text) { if (send.type === SendType.Text) {
const updated = await storage.incrementSendAccessCount(send.id);
if (!updated) {
return errorResponse(SEND_INACCESSIBLE_MSG, 404);
}
send.accessCount += 1; send.accessCount += 1;
send.updatedAt = new Date().toISOString();
await storage.saveSend(send);
await storage.updateRevisionDate(send.userId); await storage.updateRevisionDate(send.userId);
} }
@@ -1068,9 +1070,11 @@ export async function handleAccessSendFile(
return validationErr; return validationErr;
} }
const updated = await storage.incrementSendAccessCount(send.id);
if (!updated) {
return errorResponse(SEND_INACCESSIBLE_MSG, 404);
}
send.accessCount += 1; send.accessCount += 1;
send.updatedAt = new Date().toISOString();
await storage.saveSend(send);
await storage.updateRevisionDate(send.userId); await storage.updateRevisionDate(send.userId);
const token = await createSendFileDownloadToken(send.id, fileId, secret); const token = await createSendFileDownloadToken(send.id, fileId, secret);
@@ -1106,9 +1110,11 @@ export async function handleAccessSendV2(request: Request, env: Env): Promise<Re
} }
if (send.type === SendType.Text) { if (send.type === SendType.Text) {
const updated = await storage.incrementSendAccessCount(send.id);
if (!updated) {
return errorResponse(SEND_INACCESSIBLE_MSG, 404);
}
send.accessCount += 1; send.accessCount += 1;
send.updatedAt = new Date().toISOString();
await storage.saveSend(send);
await storage.updateRevisionDate(send.userId); await storage.updateRevisionDate(send.userId);
} }
@@ -1145,9 +1151,11 @@ export async function handleAccessSendFileV2(request: Request, env: Env, fileId:
return errorResponse(SEND_INACCESSIBLE_MSG, 404); return errorResponse(SEND_INACCESSIBLE_MSG, 404);
} }
const updated = await storage.incrementSendAccessCount(send.id);
if (!updated) {
return errorResponse(SEND_INACCESSIBLE_MSG, 404);
}
send.accessCount += 1; send.accessCount += 1;
send.updatedAt = new Date().toISOString();
await storage.saveSend(send);
await storage.updateRevisionDate(send.userId); await storage.updateRevisionDate(send.userId);
const downloadToken = await createSendFileDownloadToken(send.id, fileId, secret); const downloadToken = await createSendFileDownloadToken(send.id, fileId, secret);
+4 -2
View File
@@ -34,12 +34,14 @@ export default {
void ctx; void ctx;
await ensureDatabaseInitialized(env); await ensureDatabaseInitialized(env);
if (dbInitError) { if (dbInitError) {
// Log full error server-side, return generic message to client.
console.error('DB init error (not forwarded to client):', dbInitError);
const resp = jsonResponse( const resp = jsonResponse(
{ {
error: 'Database not initialized', error: 'Database not initialized',
error_description: dbInitError, error_description: 'Database initialization failed. Check server logs for details.',
ErrorModel: { ErrorModel: {
Message: dbInitError, Message: 'Service temporarily unavailable',
Object: 'error', Object: 'error',
}, },
}, },
+14
View File
@@ -240,6 +240,18 @@ export async function handleRequest(request: Request, env: Env): Promise<Respons
// Route matching // Route matching
try { try {
// Reject oversized bodies before any path-specific parsing.
// File upload paths enforce their own limits and are exempt here.
const isFileUploadPath =
/^\/api\/ciphers\/[a-f0-9-]+\/attachment\/[a-f0-9-]+$/i.test(path) ||
/^\/api\/sends\/[a-f0-9-]+\/file\/[a-f0-9-]+$/i.test(path);
if (!isFileUploadPath) {
const contentLength = parseInt(request.headers.get('Content-Length') || '0', 10);
if (contentLength > LIMITS.request.maxBodyBytes) {
return errorResponse('Request body too large', 413);
}
}
// Setup status // Setup status
if (path === '/setup/status' && method === 'GET') { if (path === '/setup/status' && method === 'GET') {
return handleSetupStatus(request, env); return handleSetupStatus(request, env);
@@ -328,6 +340,8 @@ export async function handleRequest(request: Request, env: Env): Promise<Respons
// Notifications hub (stub - no auth required, return 200 for connection) // Notifications hub (stub - no auth required, return 200 for connection)
if (path.startsWith('/notifications/')) { if (path.startsWith('/notifications/')) {
const blocked = await enforcePublicRateLimit();
if (blocked) return blocked;
return new Response(null, { status: 200 }); return new Response(null, { status: 200 });
} }
+39 -5
View File
@@ -425,7 +425,13 @@ export class StorageService {
async getCipher(id: string): Promise<Cipher | null> { async getCipher(id: string): Promise<Cipher | null> {
const row = await this.db.prepare('SELECT data FROM ciphers WHERE id = ?').bind(id).first<{ data: string }>(); const row = await this.db.prepare('SELECT data FROM ciphers WHERE id = ?').bind(id).first<{ data: string }>();
return row?.data ? (JSON.parse(row.data) as Cipher) : null; if (!row?.data) return null;
try {
return JSON.parse(row.data) as Cipher;
} catch {
console.error('Corrupted cipher data, id:', id);
return null;
}
} }
async saveCipher(cipher: Cipher): Promise<void> { async saveCipher(cipher: Cipher): Promise<void> {
@@ -460,7 +466,9 @@ export class StorageService {
async getAllCiphers(userId: string): Promise<Cipher[]> { async getAllCiphers(userId: string): Promise<Cipher[]> {
const res = await this.db.prepare('SELECT data FROM ciphers WHERE user_id = ? ORDER BY updated_at DESC').bind(userId).all<{ data: string }>(); const res = await this.db.prepare('SELECT data FROM ciphers WHERE user_id = ? ORDER BY updated_at DESC').bind(userId).all<{ data: string }>();
return (res.results || []).map(r => JSON.parse(r.data) as Cipher); return (res.results || []).flatMap(r => {
try { return [JSON.parse(r.data) as Cipher]; } catch { return []; }
});
} }
async getCiphersPage(userId: string, includeDeleted: boolean, limit: number, offset: number): Promise<Cipher[]> { async getCiphersPage(userId: string, includeDeleted: boolean, limit: number, offset: number): Promise<Cipher[]> {
@@ -475,7 +483,9 @@ export class StorageService {
) )
.bind(userId, limit, offset) .bind(userId, limit, offset)
.all<{ data: string }>(); .all<{ data: string }>();
return (res.results || []).map(r => JSON.parse(r.data) as Cipher); return (res.results || []).flatMap(r => {
try { return [JSON.parse(r.data) as Cipher]; } catch { return []; }
});
} }
async getCiphersByIds(ids: string[], userId: string): Promise<Cipher[]> { async getCiphersByIds(ids: string[], userId: string): Promise<Cipher[]> {
@@ -484,7 +494,9 @@ export class StorageService {
const placeholders = ids.map(() => '?').join(','); const placeholders = ids.map(() => '?').join(',');
const stmt = this.db.prepare(`SELECT data FROM ciphers WHERE user_id = ? AND id IN (${placeholders})`); const stmt = this.db.prepare(`SELECT data FROM ciphers WHERE user_id = ? AND id IN (${placeholders})`);
const res = await stmt.bind(userId, ...ids).all<{ data: string }>(); const res = await stmt.bind(userId, ...ids).all<{ data: string }>();
return (res.results || []).map(r => JSON.parse(r.data) as Cipher); return (res.results || []).flatMap(r => {
try { return [JSON.parse(r.data) as Cipher]; } catch { return []; }
});
} }
async bulkMoveCiphers(ids: string[], folderId: string | null, userId: string): Promise<void> { async bulkMoveCiphers(ids: string[], folderId: string | null, userId: string): Promise<void> {
@@ -555,7 +567,12 @@ export class StorageService {
.all<{ data: string }>(); .all<{ data: string }>();
for (const row of (res.results || [])) { for (const row of (res.results || [])) {
const cipher = JSON.parse(row.data) as Cipher; let cipher: Cipher;
try {
cipher = JSON.parse(row.data) as Cipher;
} catch {
continue;
}
cipher.folderId = null; cipher.folderId = null;
cipher.updatedAt = now; cipher.updatedAt = now;
await this.saveCipher(cipher); await this.saveCipher(cipher);
@@ -857,6 +874,23 @@ export class StorageService {
).run(); ).run();
} }
/**
* Atomically increment access_count and update updated_at.
* Returns true if the row was updated (send still available),
* false if max_access_count has already been reached.
*/
async incrementSendAccessCount(sendId: string): Promise<boolean> {
const now = new Date().toISOString();
const result = await this.db
.prepare(
'UPDATE sends SET access_count = access_count + 1, updated_at = ? ' +
'WHERE id = ? AND (max_access_count IS NULL OR access_count < max_access_count)'
)
.bind(now, sendId)
.run();
return (result.meta.changes ?? 0) > 0;
}
async deleteSend(id: string, userId: string): Promise<void> { async deleteSend(id: string, userId: string): Promise<void> {
await this.db.prepare('DELETE FROM sends WHERE id = ? AND user_id = ?').bind(id, userId).run(); await this.db.prepare('DELETE FROM sends WHERE id = ? AND user_id = ?').bind(id, userId).run();
} }
-1
View File
@@ -5,7 +5,6 @@ const CORS_HEADERS = 'Content-Type, Authorization, Accept, Device-Type, Bitwarde
function isTrustedClientOrigin(origin: string): boolean { function isTrustedClientOrigin(origin: string): boolean {
// Official browser extension / desktop-webview common origins. // Official browser extension / desktop-webview common origins.
if (origin === 'null') return true;
if (origin.startsWith('chrome-extension://')) return true; if (origin.startsWith('chrome-extension://')) return true;
if (origin.startsWith('moz-extension://')) return true; if (origin.startsWith('moz-extension://')) return true;
if (origin.startsWith('safari-web-extension://')) return true; if (origin.startsWith('safari-web-extension://')) return true;
+1
View File
@@ -3,6 +3,7 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https://icons.bitwarden.net; connect-src 'self'; font-src 'self'; frame-ancestors 'none'; form-action 'self'; base-uri 'self';" />
<link rel="icon" type="image/png" href="/favicon.ico" /> <link rel="icon" type="image/png" href="/favicon.ico" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" /> <link rel="apple-touch-icon" href="/apple-touch-icon.png" />
<title>NodeWarden</title> <title>NodeWarden</title>
+2 -1
View File
@@ -56,7 +56,8 @@ export default function SettingsPage(props: SettingsPageProps) {
async function enableTotp(): Promise<void> { async function enableTotp(): Promise<void> {
await props.onEnableTotp(secret, token); await props.onEnableTotp(secret, token);
localStorage.setItem(totpSecretStorageKey, secret); // Secret is now stored on the server; remove plaintext copy from localStorage.
localStorage.removeItem(totpSecretStorageKey);
setTotpLocked(true); setTotpLocked(true);
} }
+2 -3
View File
@@ -993,9 +993,8 @@ async function buildPublicSendAccessPayload(password?: string, keyPart?: string
const payload: Record<string, unknown> = {}; const payload: Record<string, unknown> = {};
const plainPassword = String(password || '').trim(); const plainPassword = String(password || '').trim();
if (!plainPassword) return payload; if (!plainPassword) return payload;
payload.password = plainPassword;
// Official clients send a PBKDF2 hash bound to send key material. // Only send the PBKDF2 hash bound to the send key material — never send plaintext password.
if (keyPart) { if (keyPart) {
try { try {
const sendKeyMaterial = base64UrlToBytes(keyPart); const sendKeyMaterial = base64UrlToBytes(keyPart);
@@ -1004,7 +1003,7 @@ async function buildPublicSendAccessPayload(password?: string, keyPart?: string
payload.password_hash_b64 = passwordHashB64; payload.password_hash_b64 = passwordHashB64;
payload.passwordHashB64 = passwordHashB64; payload.passwordHashB64 = passwordHashB64;
} catch { } catch {
// Fallback to plain password for legacy compatibility. // Key material invalid; cannot compute hash — server will reject as unauthorized.
} }
} }
return payload; return payload;