chore: update dependencies and enhance OLM password handling

- Updated various dependencies in package.json and bun.lock to their latest versions for improved stability and security.
- Introduced a new ecosystem.config.cjs file for better environment management.
- Enhanced OLM password handling with encryption and decryption functionalities. (Testing)
- Improved UI components for password dialogs to provide better user feedback and error handling.
- Added new database schema for managing nests and roles in the application.
This commit is contained in:
Nixyan 2026-01-14 15:20:38 -03:00
parent bf095582c8
commit 55e78db2cb
12 changed files with 487 additions and 138 deletions

184
bun.lock
View file

@ -5,60 +5,60 @@
"": {
"name": "sipher",
"dependencies": {
"@convex-dev/better-auth": "^0.10.9",
"@marsidev/react-turnstile": "^1.4.1",
"@matrix-org/olm": "^3.2.15",
"@nanostores/react": "^1.0.0",
"@phosphor-icons/react": "^2.1.10",
"@radix-ui/react-avatar": "^1.1.11",
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-context-menu": "^2.2.16",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-hover-card": "^1.1.15",
"@radix-ui/react-label": "^2.1.8",
"@radix-ui/react-menubar": "^1.1.16",
"@radix-ui/react-popover": "^1.1.15",
"@radix-ui/react-progress": "^1.1.8",
"@radix-ui/react-scroll-area": "^1.2.10",
"@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-tooltip": "^1.2.8",
"@types/bun": "^1.3.5",
"@types/libsodium-wrappers": "^0.7.14",
"better-auth": "1.4.10",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"convex": "^1.31.2",
"cross-env": "^10.1.0",
"date-fns": "^4.1.0",
"dexie": "^4.2.1",
"dexie-react-hooks": "^4.2.0",
"framer-motion": "^12.23.27",
"lucide-react": "^0.562.0",
"moment": "^2.30.1",
"nanostores": "^1.1.0",
"next": "16.1.1",
"next-themes": "^0.4.6",
"react": "19.2.3",
"react-day-picker": "^9.13.0",
"react-dom": "19.2.3",
"socket.io": "^4.8.3",
"socket.io-client": "^4.8.3",
"sonner": "^2.0.7",
"tailwind-merge": "^3.4.0",
"zod": "^4.3.5",
"@convex-dev/better-auth": "latest",
"@marsidev/react-turnstile": "latest",
"@matrix-org/olm": "latest",
"@nanostores/react": "latest",
"@phosphor-icons/react": "latest",
"@radix-ui/react-avatar": "latest",
"@radix-ui/react-checkbox": "latest",
"@radix-ui/react-context-menu": "latest",
"@radix-ui/react-dialog": "latest",
"@radix-ui/react-hover-card": "latest",
"@radix-ui/react-label": "latest",
"@radix-ui/react-menubar": "latest",
"@radix-ui/react-popover": "latest",
"@radix-ui/react-progress": "latest",
"@radix-ui/react-scroll-area": "latest",
"@radix-ui/react-separator": "latest",
"@radix-ui/react-slot": "latest",
"@radix-ui/react-tooltip": "latest",
"@types/bun": "latest",
"@types/libsodium-wrappers": "latest",
"better-auth": "latest",
"class-variance-authority": "latest",
"clsx": "latest",
"cmdk": "latest",
"convex": "latest",
"cross-env": "latest",
"date-fns": "latest",
"dexie": "latest",
"dexie-react-hooks": "latest",
"framer-motion": "latest",
"lucide-react": "latest",
"moment": "latest",
"nanostores": "latest",
"next": "latest",
"next-themes": "latest",
"react": "latest",
"react-day-picker": "latest",
"react-dom": "latest",
"socket.io": "latest",
"socket.io-client": "latest",
"sonner": "latest",
"tailwind-merge": "latest",
"zod": "latest",
},
"devDependencies": {
"@tailwindcss/postcss": "^4.1.18",
"@types/node": "^25.0.3",
"@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
"babel-plugin-react-compiler": "1.0.0",
"tailwindcss": "^4.1.18",
"tsx": "^4.21.0",
"tw-animate-css": "^1.4.0",
"typescript": "^5.9.3",
"@tailwindcss/postcss": "latest",
"@types/node": "latest",
"@types/react": "latest",
"@types/react-dom": "latest",
"babel-plugin-react-compiler": "latest",
"tailwindcss": "latest",
"tsx": "latest",
"tw-animate-css": "latest",
"typescript": "latest",
},
},
},
@ -71,17 +71,17 @@
"@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="],
"@better-auth/core": ["@better-auth/core@1.4.10", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "zod": "^4.1.12" }, "peerDependencies": { "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21", "better-call": "1.1.7", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" } }, "sha512-AThrfb6CpG80wqkanfrbN2/fGOYzhGladHFf3JhaWt/3/Vtf4h084T6PJLrDE7M/vCCGYvDI1DkvP3P1OB2HAg=="],
"@better-auth/core": ["@better-auth/core@1.4.12", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "zod": "^4.1.12" }, "peerDependencies": { "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21", "better-call": "1.1.7", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" } }, "sha512-VfqZwMAEl9rnGx092BIZ2Q5z8rt7jjN2OAbvPqehufSKZGmh8JsdtZRBMl/CHQir9bwi2Ev0UF4+7TQp+DXEMg=="],
"@better-auth/passkey": ["@better-auth/passkey@1.4.9", "", { "dependencies": { "@simplewebauthn/browser": "^13.1.2", "@simplewebauthn/server": "^13.1.2", "zod": "^4.1.12" }, "peerDependencies": { "@better-auth/core": "1.4.9", "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21", "better-auth": "1.4.9", "better-call": "1.1.7", "nanostores": "^1.0.1" } }, "sha512-fPsV0LYbmPytxrTaltM2RXbJnmSttX9UWr4wkZtJYgCBGeFqN8+8ZzBTZXOymWDJTVQ0kVZrD7c7/HyxXEG1zA=="],
"@better-auth/telemetry": ["@better-auth/telemetry@1.4.10", "", { "dependencies": { "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21" }, "peerDependencies": { "@better-auth/core": "1.4.10" } }, "sha512-Dq4XJX6EKsUu0h3jpRagX739p/VMOTcnJYWRrLtDYkqtZFg+sFiFsSWVcfapZoWpRSUGYX9iKwl6nDHn6Ju2oQ=="],
"@better-auth/telemetry": ["@better-auth/telemetry@1.4.12", "", { "dependencies": { "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21" }, "peerDependencies": { "@better-auth/core": "1.4.12" } }, "sha512-4q504Og42PzkUbZjXDt+FyeYaS0WZmAlEOC3nbBCZDObTVCRUnGgJW52B2maJ7BCVvAQgBGLEeQmQzU5+63J0A=="],
"@better-auth/utils": ["@better-auth/utils@0.3.0", "", {}, "sha512-W+Adw6ZA6mgvnSnhOki270rwJ42t4XzSK6YWGF//BbVXL6SwCLWfyzBc1lN2m/4RM28KubdBKQ4X5VMoLRNPQw=="],
"@better-fetch/fetch": ["@better-fetch/fetch@1.1.21", "", {}, "sha512-/ImESw0sskqlVR94jB+5+Pxjf+xBwDZF/N5+y2/q4EqD7IARUTSpPfIo8uf39SYpCxyOCtbyYpUrZ3F/k0zT4A=="],
"@convex-dev/better-auth": ["@convex-dev/better-auth@0.10.9", "", { "dependencies": { "@better-auth/passkey": "1.4.9", "@better-fetch/fetch": "^1.1.18", "common-tags": "^1.8.2", "convex-helpers": "^0.1.95", "jose": "^6.1.0", "remeda": "^2.32.0", "semver": "^7.7.3", "type-fest": "^4.39.1", "zod": "^4.0.0" }, "peerDependencies": { "better-auth": "1.4.9", "convex": "^1.25.0", "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0" } }, "sha512-GxLQv4oKh9vLAn/LadN8NqY/naAcyP3qmhSli3P5nObtDH1aq/EI/PZ/WpbdJCDrelL0TXg1JvGaWJueuGLXIA=="],
"@convex-dev/better-auth": ["@convex-dev/better-auth@0.10.10", "", { "dependencies": { "@better-auth/passkey": "1.4.9", "@better-fetch/fetch": "^1.1.18", "common-tags": "^1.8.2", "convex-helpers": "^0.1.95", "jose": "^6.1.0", "remeda": "^2.32.0", "semver": "^7.7.3", "type-fest": "^4.39.1", "zod": "^4.0.0" }, "peerDependencies": { "better-auth": "1.4.9", "convex": "^1.25.0", "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0" } }, "sha512-BpwQ2kph43O7hmtGQAJ+ie3KrjONp83659QDjKDdH+X8yIdGevgehaqS5GHB0iJo7zQTtvs687GnAeLZ4Xx3/w=="],
"@date-fns/tz": ["@date-fns/tz@1.4.1", "", {}, "sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA=="],
@ -389,15 +389,15 @@
"@tailwindcss/postcss": ["@tailwindcss/postcss@4.1.18", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.1.18", "@tailwindcss/oxide": "4.1.18", "postcss": "^8.4.41", "tailwindcss": "4.1.18" } }, "sha512-Ce0GFnzAOuPyfV5SxjXGn0CubwGcuDB0zcdaPuCSzAa/2vII24JTkH+I6jcbXLb1ctjZMZZI6OjDaLPJQL1S0g=="],
"@types/bun": ["@types/bun@1.3.5", "", { "dependencies": { "bun-types": "1.3.5" } }, "sha512-RnygCqNrd3srIPEWBd5LFeUYG7plCoH2Yw9WaZGyNmdTEei+gWaHqydbaIRkIkcbXwhBT94q78QljxN0Sk838w=="],
"@types/bun": ["@types/bun@1.3.6", "", { "dependencies": { "bun-types": "1.3.6" } }, "sha512-uWCv6FO/8LcpREhenN1d1b6fcspAB+cefwD7uti8C8VffIv0Um08TKMn98FynpTiU38+y2dUO55T11NgDt8VAA=="],
"@types/cors": ["@types/cors@2.8.19", "", { "dependencies": { "@types/node": "*" } }, "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg=="],
"@types/libsodium-wrappers": ["@types/libsodium-wrappers@0.7.14", "", {}, "sha512-5Kv68fXuXK0iDuUir1WPGw2R9fOZUlYlSAa0ztMcL0s0BfIDTqg9GXz8K30VJpPP3sxWhbolnQma2x+/TfkzDQ=="],
"@types/node": ["@types/node@25.0.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA=="],
"@types/node": ["@types/node@25.0.8", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-powIePYMmC3ibL0UJ2i2s0WIbq6cg6UyVFQxSCpaPxxzAaziRfimGivjdF943sSGV6RADVbk0Nvlm5P/FB44Zg=="],
"@types/react": ["@types/react@19.2.7", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg=="],
"@types/react": ["@types/react@19.2.8", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-3MbSL37jEchWZz2p2mjntRZtPt837ij10ApxKfgmXCTuHWagYg7iA5bqPw6C8BMPfwidlvfPI/fxOc42HLhcyg=="],
"@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
@ -413,11 +413,11 @@
"baseline-browser-mapping": ["baseline-browser-mapping@2.9.11", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ=="],
"better-auth": ["better-auth@1.4.10", "", { "dependencies": { "@better-auth/core": "1.4.10", "@better-auth/telemetry": "1.4.10", "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21", "@noble/ciphers": "^2.0.0", "@noble/hashes": "^2.0.0", "better-call": "1.1.7", "defu": "^6.1.4", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1", "zod": "^4.1.12" }, "peerDependencies": { "@lynx-js/react": "*", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "@sveltejs/kit": "^2.0.0", "@tanstack/react-start": "^1.0.0", "better-sqlite3": "^12.0.0", "drizzle-kit": ">=0.31.4", "drizzle-orm": ">=0.41.0", "mongodb": "^6.0.0 || ^7.0.0", "mysql2": "^3.0.0", "next": "^14.0.0 || ^15.0.0 || ^16.0.0", "pg": "^8.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0", "solid-js": "^1.0.0", "svelte": "^4.0.0 || ^5.0.0", "vitest": "^2.0.0 || ^3.0.0 || ^4.0.0", "vue": "^3.0.0" }, "optionalPeers": ["@lynx-js/react", "@prisma/client", "@sveltejs/kit", "@tanstack/react-start", "better-sqlite3", "drizzle-kit", "drizzle-orm", "mongodb", "mysql2", "next", "pg", "prisma", "react", "react-dom", "solid-js", "svelte", "vitest", "vue"] }, "sha512-0kqwEBJLe8eyFzbUspRG/htOriCf9uMLlnpe34dlIJGdmDfPuQISd4shShvUrvIVhPxsY1dSTXdXPLpqISYOYg=="],
"better-auth": ["better-auth@1.4.12", "", { "dependencies": { "@better-auth/core": "1.4.12", "@better-auth/telemetry": "1.4.12", "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21", "@noble/ciphers": "^2.0.0", "@noble/hashes": "^2.0.0", "better-call": "1.1.7", "defu": "^6.1.4", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1", "zod": "^4.1.12" }, "peerDependencies": { "@lynx-js/react": "*", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "@sveltejs/kit": "^2.0.0", "@tanstack/react-start": "^1.0.0", "better-sqlite3": "^12.0.0", "drizzle-kit": ">=0.31.4", "drizzle-orm": ">=0.41.0", "mongodb": "^6.0.0 || ^7.0.0", "mysql2": "^3.0.0", "next": "^14.0.0 || ^15.0.0 || ^16.0.0", "pg": "^8.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0", "solid-js": "^1.0.0", "svelte": "^4.0.0 || ^5.0.0", "vitest": "^2.0.0 || ^3.0.0 || ^4.0.0", "vue": "^3.0.0" }, "optionalPeers": ["@lynx-js/react", "@prisma/client", "@sveltejs/kit", "@tanstack/react-start", "better-sqlite3", "drizzle-kit", "drizzle-orm", "mongodb", "mysql2", "next", "pg", "prisma", "react", "react-dom", "solid-js", "svelte", "vitest", "vue"] }, "sha512-FsFMnWgk+AGrxsIGbpWLCibgYcbm6uNhPHln3ohXFDXSRa0gk39Beuh54Q+x6ml2qYodF0snxf/tPtDpBI/JiA=="],
"better-call": ["better-call@1.1.7", "", { "dependencies": { "@better-auth/utils": "^0.3.0", "@better-fetch/fetch": "^1.1.4", "rou3": "^0.7.10", "set-cookie-parser": "^2.7.1" }, "peerDependencies": { "zod": "^4.0.0" }, "optionalPeers": ["zod"] }, "sha512-6gaJe1bBIEgVebQu/7q9saahVzvBsGaByEnE8aDVncZEDiJO7sdNB28ot9I6iXSbR25egGmmZ6aIURXyQHRraQ=="],
"bun-types": ["bun-types@1.3.5", "", { "dependencies": { "@types/node": "*" } }, "sha512-inmAYe2PFLs0SUbFOWSVD24sg1jFlMPxOjOSSCYqUgn4Hsc3rDc7dFvfVYjFPNHtov6kgUeulV4SxbuIV/stPw=="],
"bun-types": ["bun-types@1.3.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-OlFwHcnNV99r//9v5IIOgQ9Uk37gZqrNMCcqEaExdkVq3Avwqok1bJFmvGMCkCE0FqzdY8VMOZpfpR3lwI+CsQ=="],
"caniuse-lite": ["caniuse-lite@1.0.30001757", "", {}, "sha512-r0nnL/I28Zi/yjk1el6ilj27tKcdjLsNqAOZr0yVjWPrSQyHgKI2INaEWw21bAQSv2LXRt1XuCS/GomNpWOxsQ=="],
@ -431,7 +431,7 @@
"common-tags": ["common-tags@1.8.2", "", {}, "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA=="],
"convex": ["convex@1.31.2", "", { "dependencies": { "esbuild": "0.25.4", "prettier": "^3.0.0" }, "peerDependencies": { "@auth0/auth0-react": "^2.0.1", "@clerk/clerk-react": "^4.12.8 || ^5.0.0", "react": "^18.0.0 || ^19.0.0-0 || ^19.0.0" }, "optionalPeers": ["@auth0/auth0-react", "@clerk/clerk-react", "react"], "bin": { "convex": "bin/main.js" } }, "sha512-RFuJOwlL2bM5X63egvBI5ZZZH6wESREpAbHsLjODxzDeJuewTLKrEnbvHV/NWp1uJYpgEFJziuGHmZ0tnAmmJg=="],
"convex": ["convex@1.31.4", "", { "dependencies": { "esbuild": "0.27.0", "prettier": "^3.0.0" }, "peerDependencies": { "@auth0/auth0-react": "^2.0.1", "@clerk/clerk-react": "^4.12.8 || ^5.0.0", "react": "^18.0.0 || ^19.0.0-0 || ^19.0.0" }, "optionalPeers": ["@auth0/auth0-react", "@clerk/clerk-react", "react"], "bin": { "convex": "bin/main.js" } }, "sha512-iDm283Gb/CFRb30cvhH6Z9qlYof6dhtin415FarKUKB3K7gumO0rn8snY0CTvUrThV3UnCtttbuL/1oY7LscyA=="],
"convex-helpers": ["convex-helpers@0.1.106", "", { "peerDependencies": { "@standard-schema/spec": "^1.0.0", "convex": "^1.25.4", "hono": "^4.0.5", "react": "^17.0.2 || ^18.0.0 || ^19.0.0", "typescript": "^5.5", "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["@standard-schema/spec", "hono", "react", "typescript", "zod"], "bin": { "convex-helpers": "bin.cjs" } }, "sha512-hWRe3yDaAVHMe4CUYw1YoQLiPZ1KIx6Kbf0w6UcRDx1BXpJgMCl3GVIMiSeYiA0PkbwjnIwGWIvoUVKloG5Tyw=="],
@ -471,7 +471,7 @@
"esbuild": ["esbuild@0.27.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.2", "@esbuild/android-arm": "0.27.2", "@esbuild/android-arm64": "0.27.2", "@esbuild/android-x64": "0.27.2", "@esbuild/darwin-arm64": "0.27.2", "@esbuild/darwin-x64": "0.27.2", "@esbuild/freebsd-arm64": "0.27.2", "@esbuild/freebsd-x64": "0.27.2", "@esbuild/linux-arm": "0.27.2", "@esbuild/linux-arm64": "0.27.2", "@esbuild/linux-ia32": "0.27.2", "@esbuild/linux-loong64": "0.27.2", "@esbuild/linux-mips64el": "0.27.2", "@esbuild/linux-ppc64": "0.27.2", "@esbuild/linux-riscv64": "0.27.2", "@esbuild/linux-s390x": "0.27.2", "@esbuild/linux-x64": "0.27.2", "@esbuild/netbsd-arm64": "0.27.2", "@esbuild/netbsd-x64": "0.27.2", "@esbuild/openbsd-arm64": "0.27.2", "@esbuild/openbsd-x64": "0.27.2", "@esbuild/openharmony-arm64": "0.27.2", "@esbuild/sunos-x64": "0.27.2", "@esbuild/win32-arm64": "0.27.2", "@esbuild/win32-ia32": "0.27.2", "@esbuild/win32-x64": "0.27.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw=="],
"framer-motion": ["framer-motion@12.23.27", "", { "dependencies": { "motion-dom": "^12.23.23", "motion-utils": "^12.23.6", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-EAcX8FS8jzZ4tSKpj+1GhwbVY+r1gfamPFwXZAsioPqu/ffRwU2otkKg6GEDCR41FVJv3RoBN7Aqep6drL9Itg=="],
"framer-motion": ["framer-motion@12.26.2", "", { "dependencies": { "motion-dom": "^12.26.2", "motion-utils": "^12.24.10", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-lflOQEdjquUi9sCg5Y1LrsZDlsjrHw7m0T9Yedvnk7Bnhqfkc89/Uha10J3CFhkL+TCZVCRw9eUGyM/lyYhXQA=="],
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
@ -523,9 +523,9 @@
"moment": ["moment@2.30.1", "", {}, "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how=="],
"motion-dom": ["motion-dom@12.23.23", "", { "dependencies": { "motion-utils": "^12.23.6" } }, "sha512-n5yolOs0TQQBRUFImrRfs/+6X4p3Q4n1dUEqt/H58Vx7OW6RF+foWEgmTVDhIWJIMXOuNNL0apKH2S16en9eiA=="],
"motion-dom": ["motion-dom@12.26.2", "", { "dependencies": { "motion-utils": "^12.24.10" } }, "sha512-KLMT1BroY8oKNeliA3JMNJ+nbCIsTKg6hJpDb4jtRAJ7nCKnnpg/LTq/NGqG90Limitz3kdAnAVXecdFVGlWTw=="],
"motion-utils": ["motion-utils@12.23.6", "", {}, "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ=="],
"motion-utils": ["motion-utils@12.24.10", "", {}, "sha512-x5TFgkCIP4pPsRLpKoI86jv/q8t8FQOiM/0E8QKBzfMozWHfkKap2gA1hOki+B5g3IsBNpxbUnfOum1+dgvYww=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
@ -639,8 +639,6 @@
"@better-auth/passkey/zod": ["zod@4.2.1", "", {}, "sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw=="],
"@convex-dev/better-auth/zod": ["zod@4.2.1", "", {}, "sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw=="],
"@radix-ui/react-arrow/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
"@radix-ui/react-checkbox/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
@ -723,7 +721,7 @@
"@types/cors/@types/node": ["@types/node@24.10.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ=="],
"convex/esbuild": ["esbuild@0.25.4", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.4", "@esbuild/android-arm": "0.25.4", "@esbuild/android-arm64": "0.25.4", "@esbuild/android-x64": "0.25.4", "@esbuild/darwin-arm64": "0.25.4", "@esbuild/darwin-x64": "0.25.4", "@esbuild/freebsd-arm64": "0.25.4", "@esbuild/freebsd-x64": "0.25.4", "@esbuild/linux-arm": "0.25.4", "@esbuild/linux-arm64": "0.25.4", "@esbuild/linux-ia32": "0.25.4", "@esbuild/linux-loong64": "0.25.4", "@esbuild/linux-mips64el": "0.25.4", "@esbuild/linux-ppc64": "0.25.4", "@esbuild/linux-riscv64": "0.25.4", "@esbuild/linux-s390x": "0.25.4", "@esbuild/linux-x64": "0.25.4", "@esbuild/netbsd-arm64": "0.25.4", "@esbuild/netbsd-x64": "0.25.4", "@esbuild/openbsd-arm64": "0.25.4", "@esbuild/openbsd-x64": "0.25.4", "@esbuild/sunos-x64": "0.25.4", "@esbuild/win32-arm64": "0.25.4", "@esbuild/win32-ia32": "0.25.4", "@esbuild/win32-x64": "0.25.4" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q=="],
"convex/esbuild": ["esbuild@0.27.0", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.0", "@esbuild/android-arm": "0.27.0", "@esbuild/android-arm64": "0.27.0", "@esbuild/android-x64": "0.27.0", "@esbuild/darwin-arm64": "0.27.0", "@esbuild/darwin-x64": "0.27.0", "@esbuild/freebsd-arm64": "0.27.0", "@esbuild/freebsd-x64": "0.27.0", "@esbuild/linux-arm": "0.27.0", "@esbuild/linux-arm64": "0.27.0", "@esbuild/linux-ia32": "0.27.0", "@esbuild/linux-loong64": "0.27.0", "@esbuild/linux-mips64el": "0.27.0", "@esbuild/linux-ppc64": "0.27.0", "@esbuild/linux-riscv64": "0.27.0", "@esbuild/linux-s390x": "0.27.0", "@esbuild/linux-x64": "0.27.0", "@esbuild/netbsd-arm64": "0.27.0", "@esbuild/netbsd-x64": "0.27.0", "@esbuild/openbsd-arm64": "0.27.0", "@esbuild/openbsd-x64": "0.27.0", "@esbuild/openharmony-arm64": "0.27.0", "@esbuild/sunos-x64": "0.27.0", "@esbuild/win32-arm64": "0.27.0", "@esbuild/win32-ia32": "0.27.0", "@esbuild/win32-x64": "0.27.0" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-jd0f4NHbD6cALCyGElNpGAOtWxSq46l9X/sWB0Nzd5er4Kz2YTm+Vl0qKFT9KUJvD8+fiO8AvoHhFvEatfVixA=="],
"engine.io/@types/node": ["@types/node@24.10.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ=="],
@ -763,54 +761,56 @@
"@radix-ui/react-visually-hidden/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"convex/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.4", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q=="],
"convex/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.0", "", { "os": "aix", "cpu": "ppc64" }, "sha512-KuZrd2hRjz01y5JK9mEBSD3Vj3mbCvemhT466rSuJYeE/hjuBrHfjjcjMdTm/sz7au+++sdbJZJmuBwQLuw68A=="],
"convex/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.4", "", { "os": "android", "cpu": "arm" }, "sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ=="],
"convex/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.27.0", "", { "os": "android", "cpu": "arm" }, "sha512-j67aezrPNYWJEOHUNLPj9maeJte7uSMM6gMoxfPC9hOg8N02JuQi/T7ewumf4tNvJadFkvLZMlAq73b9uwdMyQ=="],
"convex/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.4", "", { "os": "android", "cpu": "arm64" }, "sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A=="],
"convex/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.0", "", { "os": "android", "cpu": "arm64" }, "sha512-CC3vt4+1xZrs97/PKDkl0yN7w8edvU2vZvAFGD16n9F0Cvniy5qvzRXjfO1l94efczkkQE6g1x0i73Qf5uthOQ=="],
"convex/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.25.4", "", { "os": "android", "cpu": "x64" }, "sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ=="],
"convex/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.27.0", "", { "os": "android", "cpu": "x64" }, "sha512-wurMkF1nmQajBO1+0CJmcN17U4BP6GqNSROP8t0X/Jiw2ltYGLHpEksp9MpoBqkrFR3kv2/te6Sha26k3+yZ9Q=="],
"convex/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g=="],
"convex/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-uJOQKYCcHhg07DL7i8MzjvS2LaP7W7Pn/7uA0B5S1EnqAirJtbyw4yC5jQ5qcFjHK9l6o/MX9QisBg12kNkdHg=="],
"convex/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A=="],
"convex/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-8mG6arH3yB/4ZXiEnXof5MK72dE6zM9cDvUcPtxhUZsDjESl9JipZYW60C3JGreKCEP+p8P/72r69m4AZGJd5g=="],
"convex/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.4", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ=="],
"convex/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-9FHtyO988CwNMMOE3YIeci+UV+x5Zy8fI2qHNpsEtSF83YPBmE8UWmfYAQg6Ux7Gsmd4FejZqnEUZCMGaNQHQw=="],
"convex/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.4", "", { "os": "freebsd", "cpu": "x64" }, "sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ=="],
"convex/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-zCMeMXI4HS/tXvJz8vWGexpZj2YVtRAihHLk1imZj4efx1BQzN76YFeKqlDr3bUWI26wHwLWPd3rwh6pe4EV7g=="],
"convex/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.4", "", { "os": "linux", "cpu": "arm" }, "sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ=="],
"convex/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.0", "", { "os": "linux", "cpu": "arm" }, "sha512-t76XLQDpxgmq2cNXKTVEB7O7YMb42atj2Re2Haf45HkaUpjM2J0UuJZDuaGbPbamzZ7bawyGFUkodL+zcE+jvQ=="],
"convex/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ=="],
"convex/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-AS18v0V+vZiLJyi/4LphvBE+OIX682Pu7ZYNsdUHyUKSoRwdnOsMf6FDekwoAFKej14WAkOef3zAORJgAtXnlQ=="],
"convex/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.4", "", { "os": "linux", "cpu": "ia32" }, "sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ=="],
"convex/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.0", "", { "os": "linux", "cpu": "ia32" }, "sha512-Mz1jxqm/kfgKkc/KLHC5qIujMvnnarD9ra1cEcrs7qshTUSksPihGrWHVG5+osAIQ68577Zpww7SGapmzSt4Nw=="],
"convex/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.4", "", { "os": "linux", "cpu": "none" }, "sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA=="],
"convex/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.0", "", { "os": "linux", "cpu": "none" }, "sha512-QbEREjdJeIreIAbdG2hLU1yXm1uu+LTdzoq1KCo4G4pFOLlvIspBm36QrQOar9LFduavoWX2msNFAAAY9j4BDg=="],
"convex/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.4", "", { "os": "linux", "cpu": "none" }, "sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg=="],
"convex/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.0", "", { "os": "linux", "cpu": "none" }, "sha512-sJz3zRNe4tO2wxvDpH/HYJilb6+2YJxo/ZNbVdtFiKDufzWq4JmKAiHy9iGoLjAV7r/W32VgaHGkk35cUXlNOg=="],
"convex/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag=="],
"convex/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-z9N10FBD0DCS2dmSABDBb5TLAyF1/ydVb+N4pi88T45efQ/w4ohr/F/QYCkxDPnkhkp6AIpIcQKQ8F0ANoA2JA=="],
"convex/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.4", "", { "os": "linux", "cpu": "none" }, "sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA=="],
"convex/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.0", "", { "os": "linux", "cpu": "none" }, "sha512-pQdyAIZ0BWIC5GyvVFn5awDiO14TkT/19FTmFcPdDec94KJ1uZcmFs21Fo8auMXzD4Tt+diXu1LW1gHus9fhFQ=="],
"convex/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g=="],
"convex/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-hPlRWR4eIDDEci953RI1BLZitgi5uqcsjKMxwYfmi4LcwyWo2IcRP+lThVnKjNtk90pLS8nKdroXYOqW+QQH+w=="],
"convex/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.4", "", { "os": "linux", "cpu": "x64" }, "sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA=="],
"convex/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.0", "", { "os": "linux", "cpu": "x64" }, "sha512-1hBWx4OUJE2cab++aVZ7pObD6s+DK4mPGpemtnAORBvb5l/g5xFGk0vc0PjSkrDs0XaXj9yyob3d14XqvnQ4gw=="],
"convex/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.4", "", { "os": "none", "cpu": "arm64" }, "sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ=="],
"convex/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.0", "", { "os": "none", "cpu": "arm64" }, "sha512-6m0sfQfxfQfy1qRuecMkJlf1cIzTOgyaeXaiVaaki8/v+WB+U4hc6ik15ZW6TAllRlg/WuQXxWj1jx6C+dfy3w=="],
"convex/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.4", "", { "os": "none", "cpu": "x64" }, "sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw=="],
"convex/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.0", "", { "os": "none", "cpu": "x64" }, "sha512-xbbOdfn06FtcJ9d0ShxxvSn2iUsGd/lgPIO2V3VZIPDbEaIj1/3nBBe1AwuEZKXVXkMmpr6LUAgMkLD/4D2PPA=="],
"convex/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.4", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A=="],
"convex/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.0", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fWgqR8uNbCQ/GGv0yhzttj6sU/9Z5/Sv/VGU3F5OuXK6J6SlriONKrQ7tNlwBrJZXRYk5jUhuWvF7GYzGguBZQ=="],
"convex/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.4", "", { "os": "openbsd", "cpu": "x64" }, "sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw=="],
"convex/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-aCwlRdSNMNxkGGqQajMUza6uXzR/U0dIl1QmLjPtRbLOx3Gy3otfFu/VjATy4yQzo9yFDGTxYDo1FfAD9oRD2A=="],
"convex/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.4", "", { "os": "sunos", "cpu": "x64" }, "sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q=="],
"convex/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.0", "", { "os": "none", "cpu": "arm64" }, "sha512-nyvsBccxNAsNYz2jVFYwEGuRRomqZ149A39SHWk4hV0jWxKM0hjBPm3AmdxcbHiFLbBSwG6SbpIcUbXjgyECfA=="],
"convex/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ=="],
"convex/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.0", "", { "os": "sunos", "cpu": "x64" }, "sha512-Q1KY1iJafM+UX6CFEL+F4HRTgygmEW568YMqDA5UV97AuZSm21b7SXIrRJDwXWPzr8MGr75fUZPV67FdtMHlHA=="],
"convex/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.4", "", { "os": "win32", "cpu": "ia32" }, "sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg=="],
"convex/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-W1eyGNi6d+8kOmZIwi/EDjrL9nxQIQ0MiGqe/AWc6+IaHloxHSGoeRgDRKHFISThLmsewZ5nHFvGFWdBYlgKPg=="],
"convex/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.4", "", { "os": "win32", "cpu": "x64" }, "sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ=="],
"convex/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-30z1aKL9h22kQhilnYkORFYt+3wp7yZsHWus+wSKAJR8JtdfI76LJ4SBdMsCopTR3z/ORqVu5L1vtnHZWVj4cQ=="],
"convex/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.0", "", { "os": "win32", "cpu": "x64" }, "sha512-aIitBcjQeyOhMTImhLZmtxfdOcuNRpwlPNmlFKPcHQYPhEssw75Cl1TSXJXpMkzaua9FUetx/4OQKq7eJul5Cg=="],
}
}

View file

@ -0,0 +1,68 @@
import { defineTable } from "convex/server";
import { v } from "convex/values";
export const nests = {
nests: defineTable({
type: v.union(v.literal("global"), v.literal("regional"), v.literal("private")),
name: v.string(),
description: v.optional(v.string()),
images: v.object({
banner: v.id("storage"),
icon: v.id("storage"),
}),
colors: v.optional(
v.object({
primary: v.string(),
accent: v.string(),
})
),
createdAt: v.number(),
updatedAt: v.number(),
managerId: v.id("user"),
members: v.array(v.id("user")),
channels: v.array(v.id("channel")),
roles: v.array(v.id("role")),
region: v.optional(v.string()),
emojis: v.array(v.object({
id: v.id("storage"),
name: v.string(),
createdAt: v.number(),
})),
})
.index("managerId", ["managerId"])
.index("type", ["type"])
.index("type_region", ["type", "region"])
.index("createdAt", ["createdAt"]),
roles: defineTable({
nestId: v.id("nests"),
name: v.string(),
color: v.optional(v.string()),
hoist: v.optional(v.boolean()),
mentionable: v.optional(v.boolean()),
icon: v.optional(v.id("storage")),
position: v.optional(v.number()),
permissions: v.array(v.int64()), // Permissions as bitfield
flags: v.array(v.int64()), // Flags as bitfield
createdAt: v.number(),
updatedAt: v.number(),
})
.index("nestId", ["nestId"])
.index("nestId_position", ["nestId", "position"]),
channels: defineTable({
type: v.union(v.literal("text"), v.literal("category"), v.literal("announcement")),
name: v.string(),
nestId: v.id("nests"),
position: v.number(),
permissions: v.array(v.int64()), // Permissions as bitfield
overwrites: v.array(v.object({
id: v.union(v.id("user"), v.id("role")),
allow: v.union(v.array(v.int64()), v.null()), // Permissions as bitfield
deny: v.union(v.array(v.int64()), v.null()), // Permissions as bitfield
})),
createdAt: v.number(),
updatedAt: v.number(),
})
.index("nestId", ["nestId"])
.index("nestId_position", ["nestId", "position"])
.index("nestId_type", ["nestId", "type"])
}

44
ecosystem.config.cjs Normal file
View file

@ -0,0 +1,44 @@
const fs = require('fs');
const path = require('path');
// Load .env.local if it exists
function loadEnvFile(filename) {
const envPath = path.resolve(__dirname, filename);
if (!fs.existsSync(envPath)) return {};
const content = fs.readFileSync(envPath, 'utf-8');
const env = {};
for (const line of content.split('\n')) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith('#')) continue;
const [key, ...rest] = trimmed.split('=');
if (key) env[key.trim()] = rest.join('=').trim().replace(/^["']|["']$/g, '');
}
return env;
}
const envLocal = loadEnvFile('.env.local');
module.exports = {
apps: [
{
name: 'sipher',
script: 'src/server.ts',
interpreter: 'node_modules/.bin/tsx',
instances: 1,
exec_mode: 'fork',
watch: false,
max_memory_restart: '4G',
env: {
...envLocal,
NODE_ENV: 'development',
PORT: 3000,
},
env_production: {
...envLocal,
NODE_ENV: 'production',
PORT: 8081,
},
},
],
};

View file

@ -5,11 +5,11 @@
"scripts": {
"postinstall": "bun src/lib/scripts/copy-olm.ts",
"dev": "cross-env NODE_ENV=development PORT=3000 tsx src/server.ts",
"build": "bun src/lib/scripts/copy-olm.ts && next build",
"build": "bun src/lib/scripts/copy-olm.ts && convex deploy --cmd \"bun run build\"",
"start": "cross-env NODE_ENV=production PORT=8081 tsx src/server.ts"
},
"dependencies": {
"@convex-dev/better-auth": "^0.10.9",
"@convex-dev/better-auth": "^0.10.10",
"@marsidev/react-turnstile": "^1.4.1",
"@matrix-org/olm": "^3.2.15",
"@nanostores/react": "^1.0.0",
@ -27,18 +27,18 @@
"@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-tooltip": "^1.2.8",
"@types/bun": "^1.3.5",
"@types/bun": "^1.3.6",
"@types/libsodium-wrappers": "^0.7.14",
"better-auth": "1.4.10",
"better-auth": "1.4.12",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"convex": "^1.31.2",
"convex": "^1.31.4",
"cross-env": "^10.1.0",
"date-fns": "^4.1.0",
"dexie": "^4.2.1",
"dexie-react-hooks": "^4.2.0",
"framer-motion": "^12.23.27",
"framer-motion": "^12.26.2",
"lucide-react": "^0.562.0",
"moment": "^2.30.1",
"nanostores": "^1.1.0",
@ -55,8 +55,8 @@
},
"devDependencies": {
"@tailwindcss/postcss": "^4.1.18",
"@types/node": "^25.0.3",
"@types/react": "^19.2.7",
"@types/node": "^25.0.8",
"@types/react": "^19.2.8",
"@types/react-dom": "^19.2.3",
"babel-plugin-react-compiler": "1.0.0",
"tailwindcss": "^4.1.18",

View file

@ -1,21 +1,58 @@
import { db } from "@/lib/db";
// Track OLM initialization state
let olmInitPromise: Promise<any> | null = null;
// Load OLM via script tag to bypass bundler entirely
export async function loadOlm() {
if (typeof window === "undefined") throw new Error("OLM requires browser");
if ((window as any).Olm) return (window as any).Olm;
return new Promise((resolve, reject) => {
// If already initialized, return cached Olm
if ((window as any).__olmInitialized && (window as any).Olm) {
console.debug("[makeKeysOnSignUp]: OLM already initialized");
return (window as any).Olm;
}
// If initialization is in progress, wait for it
if (olmInitPromise) {
console.debug("[makeKeysOnSignUp]: OLM initialization in progress, waiting for it");
return olmInitPromise;
}
// Start initialization
olmInitPromise = new Promise((resolve, reject) => {
// Check if script already loaded but not initialized
if ((window as any).Olm) {
const Olm = (window as any).Olm;
Olm.init({ locateFile: () => "/olm.wasm" })
.then(() => {
(window as any).__olmInitialized = true;
resolve(Olm);
})
.catch(reject);
return;
}
const script = document.createElement("script");
script.src = "/olm.js";
script.onload = async () => {
try {
const Olm = (window as any).Olm;
await Olm.init({ locateFile: () => "/olm.wasm" });
(window as any).__olmInitialized = true;
resolve(Olm);
} catch (err) {
reject(err);
}
};
script.onerror = (err) => {
console.error("[makeKeysOnSignUp]: Failed to load OLM: ", err);
reject(new Error(`Failed to load OLM: ${err}`));
};
script.onerror = () => reject(new Error("Failed to load OLM"));
document.head.appendChild(script);
});
return olmInitPromise;
}
type SendKeysToServerFn = (args: {
@ -64,8 +101,8 @@ export default async function makeKeysOnSignUp(
const pickledAccount = account.pickle(localPassword);
// Store password in sessionStorage for unpickling later
sessionStorage.setItem(`olm_password_${odId}`, localPassword);
// Note: Password storage is handled by the OlmContext with encryption
// Do NOT store plain text password here
// Cache the account in window
if (!(window as any).olmAccountCache) {

View file

@ -112,6 +112,7 @@ export default function AppContainer() {
api.auth.retrieveServerOlmAccount,
data?.user?.id ? { userId: data.user.id } : "skip"
);
const sendKeysToServer = useMutation(api.auth.sendKeysToServer);
const consumeOTK = useMutation(api.auth.consumeOTK);

View file

@ -1,5 +1,5 @@
import { useOlmContext } from "@/contexts/olm-context";
import { Info, KeyRound, ShieldCheck } from "lucide-react";
import { AlertCircle, Info, KeyRound, ShieldCheck } from "lucide-react";
import { useEffect, useState } from "react";
import { Button } from "../ui/button";
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "../ui/dialog";
@ -8,21 +8,30 @@ import { Input } from "../ui/input";
export default function OlmPasswordDialog({ userId }: { userId: string }) {
const [needsPassword, setNeedsPassword] = useState(false);
const [password, setPasswordInput] = useState("");
const { setPassword } = useOlmContext();
const { setPassword, passwordError, clearPasswordError } = useOlmContext();
useEffect(() => {
// Get the password from the session storage
const password = sessionStorage.getItem(`olm_password_${userId}`);
console.log("🔒 Password from session storage:", password);
if (!password) {
// The context handles loading & decrypting the password from sessionStorage.
// We only need to show the dialog if the context doesn't have a password.
// This will be handled by the passwordError effect below.
// For initial load, we check if there's encrypted data - if not, show dialog.
const hasStoredPassword = sessionStorage.getItem(`olm_password_${userId}`);
if (!hasStoredPassword) {
setNeedsPassword(true);
return;
}
setPassword(password);
setNeedsPassword(false);
// If there IS stored data, the context will decrypt it and load it.
// If decryption fails or password is wrong, passwordError will be set.
}, [userId]);
// Show dialog when there's a password error (wrong password was entered)
useEffect(() => {
if (passwordError) {
setNeedsPassword(true);
setPasswordInput(""); // Clear the input for retry
}
}, [passwordError]);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (password.trim()) {
@ -55,25 +64,38 @@ export default function OlmPasswordDialog({ userId }: { userId: string }) {
<Input
type="password"
placeholder="Enter your encryption password"
className="h-11 text-center"
className={`h-11 text-center ${passwordError ? "border-destructive focus-visible:ring-destructive" : ""}`}
autoFocus
value={password}
onChange={(e) => setPasswordInput(e.target.value)}
onChange={(e) => {
setPasswordInput(e.target.value);
if (passwordError) clearPasswordError();
}}
/>
<div className="space-y-2">
<div className="flex items-center gap-2 rounded-md bg-emerald-500/10 dark:bg-emerald-400/10 px-3 py-2.5 text-emerald-700 dark:text-emerald-300 border border-emerald-200/20 dark:border-emerald-500/20">
<ShieldCheck className="h-4 w-4 shrink-0" />
{passwordError && (
<div className="flex items-center gap-2 rounded-md bg-destructive/10 px-3 py-2.5 text-destructive border border-destructive/20">
<AlertCircle className="h-4 w-4 shrink-0" />
<p className="text-xs leading-relaxed">
Your password is stored locally and never sent to our servers.
{passwordError}
</p>
</div>
<div className="flex items-center gap-2 rounded-md bg-blue-500/10 dark:bg-blue-400/10 px-3 py-2.5 text-blue-700 dark:text-blue-300 border border-blue-200/20 dark:border-blue-500/20">
)}
<div className="space-y-2">
<div className="flex items-center gap-2 rounded-md bg-chart-3/10 px-3 py-2.5 text-chart-3 border border-chart-3/20">
<Info className="h-4 w-4 shrink-0" />
<p className="text-xs leading-relaxed">
You'll be asked to re-enter this password each time you start a new browser session.
<br />
When continuing, the window will be reloaded, please do not close the window or refresh the page by yourself.
</p>
</div>
</div>
<div className="flex items-center gap-2 rounded-md bg-chart-2/10 px-3 py-2.5 text-chart-2 border border-chart-2/20">
<ShieldCheck className="h-4 w-4 shrink-0" />
<p className="text-xs leading-relaxed">
Your password is encrypted before being stored in your browser's session storage using a secure key that cannot be exported.
</p>
</div>
</div>
<div className="flex gap-3">

View file

@ -31,7 +31,7 @@ export default function OlmSetupDialog({
const handleSubmit = async () => {
if (!localPassword.trim()) return;
await onCreateAccount(localPassword);
setLocalPassword(""); // Clear password after attempt
setLocalPassword("");
};
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {

View file

@ -1,6 +1,7 @@
"use client"
import { loadOlm } from "@/app/auth/scripts/makeKeys";
import { decryptPassword, encryptPassword, getOrCreatePasswordEncryptionKey } from "@/lib/crypto";
import { db } from "@/lib/db";
import { checkOlmStatus, getOlmAccount, handleOlmAccountCreation, SendKeysToServerFn } from "@/lib/olm";
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
@ -25,10 +26,12 @@ interface OlmContextValue {
// Password & setup
password: string | null;
passwordError: string | null;
showOlmModal: boolean;
setShowOlmModal: (show: boolean) => void;
handleCreateAccount: (password: string) => Promise<void>;
setPassword: (password: string) => void;
clearPasswordError: () => void;
}
const OlmContext = createContext<OlmContextValue | null>(null);
@ -55,14 +58,34 @@ export function OlmProvider({
const [olmAccount, setOlmAccount] = useState<Olm.Account | null>(null);
const [olmStatus, setOlmStatus] = useState<SiPher.OlmStatus>("checking");
const [password, setPasswordState] = useState<string | null>(null);
const [passwordError, setPasswordError] = useState<string | null>(null);
const [showOlmModal, setShowOlmModal] = useState(false);
// Cache sessions in memory: recipientId -> Session
const sessionsRef = useRef<Map<string, Olm.Session>>(new Map());
// Track pending session creation to prevent race conditions
const pendingSessionsRef = useRef<Map<string, Promise<Olm.Session | null>>>(new Map());
// Encryption key for secure password storage (persisted in IndexedDB)
const encryptionKeyRef = useRef<CryptoKey | null>(null);
const [encryptionKeyReady, setEncryptionKeyReady] = useState(false);
// Track if password was set manually (to prevent load-from-storage race condition)
const passwordSetManuallyRef = useRef(false);
// Track if we're currently loading the OLM account (prevent duplicate loads)
const isLoadingAccountRef = useRef(false);
const [, forceUpdate] = useState({});
// Initialize encryption key on mount
useEffect(() => {
getOrCreatePasswordEncryptionKey()
.then((key) => {
encryptionKeyRef.current = key;
setEncryptionKeyReady(true);
})
.catch((err) => {
console.error("[OlmContext]: Failed to initialize encryption key:", err);
});
}, []);
// Helper: Cache session in memory
const cacheSession = useCallback((recipientId: string, session: Olm.Session) => {
sessionsRef.current.set(recipientId, session);
@ -137,18 +160,34 @@ export function OlmProvider({
// Helper: Clear password from state and storage
const clearPassword = useCallback(() => {
if (!userId) return;
passwordSetManuallyRef.current = false;
setPasswordState(null);
sessionStorage.removeItem(getPasswordStorageKey(userId));
}, [userId, getPasswordStorageKey]);
// Load password from sessionStorage on mount
// Load and decrypt password from sessionStorage on mount
useEffect(() => {
if (!userId) return;
if (!userId || !encryptionKeyReady || !encryptionKeyRef.current) return;
// Skip if password was set manually (prevents race condition loop)
if (passwordSetManuallyRef.current) return;
const loadStoredPassword = async () => {
const stored = sessionStorage.getItem(getPasswordStorageKey(userId));
if (stored) {
setPasswordState(stored);
if (!stored) return;
const decrypted = await decryptPassword(stored, encryptionKeyRef.current!);
if (decrypted) {
setPasswordState(decrypted);
console.debug("[OlmContext]: Password loaded and decrypted from storage");
} else {
// Decryption failed - clear stale data
sessionStorage.removeItem(getPasswordStorageKey(userId));
console.debug("[OlmContext]: Cleared stale encrypted password");
}
}, [userId, getPasswordStorageKey]);
};
loadStoredPassword();
}, [userId, getPasswordStorageKey, encryptionKeyReady]);
// Check OLM status when user data and server status are available
useEffect(() => {
@ -169,34 +208,70 @@ export function OlmProvider({
// Load and unpickle the OLM account when password is available
useEffect(() => {
if (!userId || !password) return;
// Prevent duplicate loads
if (isLoadingAccountRef.current) {
console.debug("[OlmContext]: Already loading account, skipping...");
return;
}
const loadAccount = async () => {
isLoadingAccountRef.current = true;
try {
console.debug("[OlmContext]: Loading OLM account...");
const account = await getOlmAccount(userId, password);
if (!account) {
console.warn("[OlmContext]: No OLM account found");
isLoadingAccountRef.current = false;
return;
}
setOlmAccount(account);
setPasswordError(null);
console.debug("[OlmContext]: OLM account loaded successfully");
} catch (err) {
console.error("[OlmContext]: Failed to load OLM account:", err);
// Password might be wrong - clear it
// Password is wrong - clear it and set error
setPasswordError("Incorrect encryption password. Please try again.");
clearPassword();
} finally {
isLoadingAccountRef.current = false;
}
};
loadAccount();
}, [userId, password, clearPassword]);
// Set password and store in sessionStorage
// Clear password error
const clearPasswordError = useCallback(() => {
setPasswordError(null);
}, []);
// Set password and store encrypted in sessionStorage
const setPassword = useCallback((newPassword: string) => {
if (!userId) return;
sessionStorage.setItem(getPasswordStorageKey(userId), newPassword);
// Mark as manually set to prevent load-from-storage race condition
passwordSetManuallyRef.current = true;
setPasswordError(null);
setPasswordState(newPassword);
// Encrypt and store asynchronously
if (encryptionKeyRef.current) {
encryptPassword(newPassword, encryptionKeyRef.current)
.then((encrypted) => {
// Only store if the password hasn't been cleared since we started
// This prevents the race condition where clearPassword runs before this .then()
if (passwordSetManuallyRef.current) {
sessionStorage.setItem(getPasswordStorageKey(userId), encrypted);
console.debug("[OlmContext]: Password encrypted and stored");
} else {
console.debug("[OlmContext]: Skipped storing password (was cleared)");
}
})
.catch((err) => {
console.error("[OlmContext]: Failed to encrypt password:", err);
});
}
}, [userId, getPasswordStorageKey]);
// Handle OLM account creation
@ -325,6 +400,8 @@ export function OlmProvider({
senderId: string,
preKeyMessage: string
): Promise<Olm.Session | null> => {
console.debug("[OlmContext]: Args passed to createInboundSession", { senderId, preKeyMessage });
if (!validateSessionRequirements()) {
return null;
}
@ -338,8 +415,8 @@ export function OlmProvider({
try {
console.debug(`[OlmContext]: Creating inbound session from sender ${senderId}`);
const Olm = await loadOlm();
const newSession = new Olm.Session();
const Olm: typeof import("@matrix-org/olm") = await loadOlm();
const newSession: Olm.Session = new Olm.Session();
// Create inbound session from the pre-key message
newSession.create_inbound(olmAccount!, preKeyMessage);
@ -374,10 +451,12 @@ export function OlmProvider({
createInboundSession,
sessions: sessionsRef.current,
password,
passwordError,
showOlmModal,
setShowOlmModal,
handleCreateAccount,
setPassword,
clearPasswordError,
};
return (

View file

@ -155,6 +155,7 @@ export function SocketProvider({ children, user, refetchUser }: SocketProviderPr
const processIncomingDM = useCallback(
async (data: { content: { type: 0 | 1; body: unknown }, participants: string[] }) => {
// Get the current user id
console.debug("[Socket]: Processing incoming DM", data);
const currentUserId = user.id;
if (!currentUserId) {
console.error("[Socket]: No user ID available");

88
src/lib/crypto/index.ts Normal file
View file

@ -0,0 +1,88 @@
import { db } from "@/lib/db";
const PASSWORD_ENCRYPTION_KEY_ID = "password_encryption_key";
/**
* Get or create the password encryption key.
* The key is non-extractable, meaning its raw bytes cannot be read,
* even if an attacker accesses IndexedDB directly.
*/
export async function getOrCreatePasswordEncryptionKey(): Promise<CryptoKey> {
// Try to load existing key from DB
const existing = await db.encryptionKeys.get(PASSWORD_ENCRYPTION_KEY_ID);
if (existing) {
console.debug("[PEC - getOrCreatePasswordEncryptionKey]: Loaded existing encryption key from DB");
return existing.key;
}
// Generate new AES-GCM key (non-extractable)
const newKey = await crypto.subtle.generateKey(
{ name: "AES-GCM", length: 256 },
false, // NOT extractable - raw key bytes cannot be exported
["encrypt", "decrypt"]
);
// Store in DB
await db.encryptionKeys.add({
id: PASSWORD_ENCRYPTION_KEY_ID,
key: newKey,
createdAt: Date.now(),
});
console.debug("[PEC - getOrCreatePasswordEncryptionKey]: Generated and stored new encryption key");
return newKey;
}
/**
* Encrypt a password string using AES-GCM.
* Returns a base64-encoded string containing IV + ciphertext.
*/
export async function encryptPassword(password: string, key: CryptoKey): Promise<string> {
const encoder = new TextEncoder();
const data = encoder.encode(password);
// Generate random IV for each encryption
const iv = crypto.getRandomValues(new Uint8Array(12));
const encrypted = await crypto.subtle.encrypt(
{ name: "AES-GCM", iv },
key,
data
);
// Combine IV + ciphertext
const combined = new Uint8Array(iv.length + encrypted.byteLength);
combined.set(iv);
combined.set(new Uint8Array(encrypted), iv.length);
// Encode as base64
return btoa(String.fromCharCode(...combined));
}
/**
* Decrypt a password string using AES-GCM.
* Returns null if decryption fails (e.g., wrong key or corrupted data).
*/
export async function decryptPassword(encryptedData: string, key: CryptoKey): Promise<string | null> {
try {
// Decode from base64
const combined = new Uint8Array(
atob(encryptedData).split("").map(c => c.charCodeAt(0))
);
// Extract IV (first 12 bytes) and ciphertext
const iv = combined.slice(0, 12);
const data = combined.slice(12);
const decrypted = await crypto.subtle.decrypt(
{ name: "AES-GCM", iv },
key,
data
);
return new TextDecoder().decode(decrypted);
} catch (err) {
console.warn("[PEC - decryptPassword]: Password decryption failed:", err);
return null;
}
}

View file

@ -28,6 +28,13 @@ export interface UnreadCount {
count: number;
}
/** Encryption key storage (for password protection in session storage) */
export interface EncryptionKey {
id: string;
key: CryptoKey;
createdAt: number;
}
// ============================================
// Database
// ============================================
@ -38,6 +45,7 @@ class SipherDB extends Dexie {
channels!: EntityTable<SiPher.Channel, "id">;
messages!: EntityTable<SiPher.Messages.ClientEncrypted.EncryptedMessage, "id">;
unreadCounts!: EntityTable<UnreadCount, "channelId">;
encryptionKeys!: EntityTable<EncryptionKey, "id">;
constructor() {
super("SipherDB");
@ -48,6 +56,7 @@ class SipherDB extends Dexie {
channels: "id, *participants, type, lastMessageAt, createdAt",
messages: "id, channelId, fromUserId, timestamp, status",
unreadCounts: "channelId",
encryptionKeys: "id, createdAt",
});
}
}