From 55e78db2cbcc3896b5300ade795b8294e6e4629f Mon Sep 17 00:00:00 2001 From: Nixyan Date: Wed, 14 Jan 2026 15:20:38 -0300 Subject: [PATCH] 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. --- bun.lock | 184 ++++++++++----------- convex/betterAuth/schemas/nests.ts | 68 ++++++++ ecosystem.config.cjs | 44 +++++ package.json | 16 +- src/app/auth/scripts/makeKeys.ts | 53 +++++- src/components/app-container.tsx | 1 + src/components/olm/olm-password-dialog.tsx | 56 +++++-- src/components/olm/olm-setup-dialog.tsx | 2 +- src/contexts/olm-context.tsx | 103 ++++++++++-- src/contexts/socket-context.tsx | 1 + src/lib/crypto/index.ts | 88 ++++++++++ src/lib/db/index.ts | 9 + 12 files changed, 487 insertions(+), 138 deletions(-) create mode 100644 convex/betterAuth/schemas/nests.ts create mode 100644 ecosystem.config.cjs create mode 100644 src/lib/crypto/index.ts diff --git a/bun.lock b/bun.lock index fd532e8..9bf3c54 100644 --- a/bun.lock +++ b/bun.lock @@ -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=="], } } diff --git a/convex/betterAuth/schemas/nests.ts b/convex/betterAuth/schemas/nests.ts new file mode 100644 index 0000000..c2221f9 --- /dev/null +++ b/convex/betterAuth/schemas/nests.ts @@ -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"]) +} \ No newline at end of file diff --git a/ecosystem.config.cjs b/ecosystem.config.cjs new file mode 100644 index 0000000..7506cb0 --- /dev/null +++ b/ecosystem.config.cjs @@ -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, + }, + }, + ], +}; diff --git a/package.json b/package.json index 484376b..f3805c1 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/app/auth/scripts/makeKeys.ts b/src/app/auth/scripts/makeKeys.ts index a6fade4..40d57cb 100644 --- a/src/app/auth/scripts/makeKeys.ts +++ b/src/app/auth/scripts/makeKeys.ts @@ -1,21 +1,58 @@ import { db } from "@/lib/db"; +// Track OLM initialization state +let olmInitPromise: Promise | 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 () => { - const Olm = (window as any).Olm; - await Olm.init({ locateFile: () => "/olm.wasm" }); - resolve(Olm); + 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) { diff --git a/src/components/app-container.tsx b/src/components/app-container.tsx index cda0aa0..23046b8 100644 --- a/src/components/app-container.tsx +++ b/src/components/app-container.tsx @@ -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); diff --git a/src/components/olm/olm-password-dialog.tsx b/src/components/olm/olm-password-dialog.tsx index f3cd12a..d424eb8 100644 --- a/src/components/olm/olm-password-dialog.tsx +++ b/src/components/olm/olm-password-dialog.tsx @@ -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 }) { setPasswordInput(e.target.value)} + onChange={(e) => { + setPasswordInput(e.target.value); + if (passwordError) clearPasswordError(); + }} /> -
-
- + {passwordError && ( +
+

- Your password is stored locally and never sent to our servers. + {passwordError}

-
+ )} +
+

You'll be asked to re-enter this password each time you start a new browser session. +
+ When continuing, the window will be reloaded, please do not close the window or refresh the page by yourself.

+
+ +

+ Your password is encrypted before being stored in your browser's session storage using a secure key that cannot be exported. +

+
diff --git a/src/components/olm/olm-setup-dialog.tsx b/src/components/olm/olm-setup-dialog.tsx index d05cdbf..c98140c 100644 --- a/src/components/olm/olm-setup-dialog.tsx +++ b/src/components/olm/olm-setup-dialog.tsx @@ -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) => { diff --git a/src/contexts/olm-context.tsx b/src/contexts/olm-context.tsx index 2ec4d39..de96c89 100644 --- a/src/contexts/olm-context.tsx +++ b/src/contexts/olm-context.tsx @@ -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; setPassword: (password: string) => void; + clearPasswordError: () => void; } const OlmContext = createContext(null); @@ -55,14 +58,34 @@ export function OlmProvider({ const [olmAccount, setOlmAccount] = useState(null); const [olmStatus, setOlmStatus] = useState("checking"); const [password, setPasswordState] = useState(null); + const [passwordError, setPasswordError] = useState(null); const [showOlmModal, setShowOlmModal] = useState(false); // Cache sessions in memory: recipientId -> Session const sessionsRef = useRef>(new Map()); // Track pending session creation to prevent race conditions const pendingSessionsRef = useRef>>(new Map()); + // Encryption key for secure password storage (persisted in IndexedDB) + const encryptionKeyRef = useRef(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; - const stored = sessionStorage.getItem(getPasswordStorageKey(userId)); - if (stored) { - setPasswordState(stored); - } - }, [userId, getPasswordStorageKey]); + 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) 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"); + } + }; + + 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 => { + 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 ( diff --git a/src/contexts/socket-context.tsx b/src/contexts/socket-context.tsx index 6ac66bb..45b1068 100644 --- a/src/contexts/socket-context.tsx +++ b/src/contexts/socket-context.tsx @@ -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"); diff --git a/src/lib/crypto/index.ts b/src/lib/crypto/index.ts new file mode 100644 index 0000000..143aa1f --- /dev/null +++ b/src/lib/crypto/index.ts @@ -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 { + // 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 { + 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 { + 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; + } +} diff --git a/src/lib/db/index.ts b/src/lib/db/index.ts index 888babc..82e32e1 100644 --- a/src/lib/db/index.ts +++ b/src/lib/db/index.ts @@ -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; messages!: EntityTable; unreadCounts!: EntityTable; + encryptionKeys!: EntityTable; 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", }); } }