diff --git a/.env.local.example b/.env.local.example index af95c0d..29b1c3f 100644 --- a/.env.local.example +++ b/.env.local.example @@ -1,7 +1,15 @@ BETTER_AUTH_SECRET= BETTER_AUTH_URL= -DATABASE_URL= +# Should follow the format: redis://: +# Could use password and username if needed +REDIS_URL= + +# Comma separated list of allowed hostnames for CORS +# Example: DEV_ALLOWED_HOSTNAMES=localhost,127.0.0.1,::1 +DEV_ALLOWED_HOSTNAMES= + +DATABASE_URL=postgresql://:@:/ EMAIL_HOST= EMAIL_PORT= @@ -9,7 +17,7 @@ EMAIL_SECURE= EMAIL_USER= EMAIL_PASSWORD= -DEBUG= +DEBUG=app:*,test:* MINIO_BUCKET= MINIO_ENDPOINT= diff --git a/bun.lock b/bun.lock index 80fd691..54c5c42 100644 --- a/bun.lock +++ b/bun.lock @@ -5,58 +5,59 @@ "": { "name": "sipher", "dependencies": { - "@better-auth/drizzle-adapter": "^1.5.6", - "@hookform/resolvers": "^5.2.2", - "@nanostores/react": "^1.1.0", - "@react-email/components": "1.0.10", - "better-auth": "^1.5.6", - "bullmq": "^5.71.1", - "class-variance-authority": "^0.7.1", - "clsx": "^2.1.1", - "debug": "^4.4.3", - "dexie": "^4.4.1", - "dexie-react-hooks": "^4.4.0", - "dotenv": "^17.3.1", - "drizzle-orm": "^0.45.2", - "framer-motion": "^12.38.0", - "ioredis": "^5.10.1", - "lucide-react": "^1.7.0", - "minio": "^8.0.7", - "nanostores": "^1.2.0", - "next": "16.2.1", - "next-themes": "^0.4.6", - "nodemailer": "^8.0.4", - "pg": "^8.20.0", - "radix-ui": "^1.4.3", - "react": "19.2.4", - "react-dom": "19.2.4", - "react-hook-form": "^7.72.0", - "socket.io": "^4.8.3", - "socket.io-client": "^4.8.3", - "sonner": "^2.0.7", - "tailwind-merge": "^3.5.0", - "tweetnacl": "^1.0.3", - "tweetnacl-util": "^0.15.1", - "zod": "^4.3.6", + "@better-auth/drizzle-adapter": "latest", + "@hookform/resolvers": "latest", + "@nanostores/react": "latest", + "@react-email/components": "latest", + "better-auth": "latest", + "bullmq": "latest", + "class-variance-authority": "latest", + "clsx": "latest", + "debug": "latest", + "dexie": "latest", + "dexie-react-hooks": "latest", + "dotenv": "latest", + "drizzle-orm": "latest", + "framer-motion": "latest", + "ioredis": "latest", + "lucide-react": "latest", + "minio": "latest", + "nanostores": "latest", + "next": "latest", + "next-themes": "latest", + "nodemailer": "latest", + "pg": "latest", + "radix-ui": "latest", + "react": "latest", + "react-dom": "latest", + "react-hook-form": "latest", + "socket.io": "latest", + "socket.io-client": "latest", + "sonner": "latest", + "tailwind-merge": "latest", + "tweetnacl": "latest", + "tweetnacl-util": "latest", + "zod": "latest", }, "devDependencies": { - "@tailwindcss/postcss": "^4.2.2", - "@types/bun": "^1.3.11", - "@types/debug": "^4.1.13", - "@types/node": "^25.5.0", - "@types/nodemailer": "^7.0.11", - "@types/pg": "^8.20.0", - "@types/react": "^19.2.14", - "@types/react-dom": "^19.2.3", - "babel-plugin-react-compiler": "1.0.0", - "cross-env": "^10.1.0", - "drizzle-kit": "^0.31.10", - "react-email": "5.2.10", - "shadcn": "^4.1.1", - "tailwindcss": "^4.2.2", - "tsx": "^4.21.0", - "tw-animate-css": "^1.4.0", - "typescript": "^6.0.2", + "@tailwindcss/postcss": "latest", + "@types/bun": "latest", + "@types/debug": "latest", + "@types/node": "latest", + "@types/nodemailer": "latest", + "@types/pg": "latest", + "@types/react": "latest", + "@types/react-dom": "latest", + "auth": "latest", + "babel-plugin-react-compiler": "latest", + "cross-env": "latest", + "drizzle-kit": "latest", + "react-email": "latest", + "shadcn": "latest", + "tailwindcss": "latest", + "tsx": "latest", + "tw-animate-css": "latest", + "typescript": "latest", }, }, }, @@ -112,8 +113,18 @@ "@babel/plugin-transform-modules-commonjs": ["@babel/plugin-transform-modules-commonjs@7.28.6", "", { "dependencies": { "@babel/helper-module-transforms": "^7.28.6", "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-jppVbf8IV9iWWwWTQIxJMAJCWBuuKx71475wHwYytrRGQ2CWiDvYlADQno3tcYpS/T2UUWFQp3nVtYfK/YBQrA=="], + "@babel/plugin-transform-react-display-name": ["@babel/plugin-transform-react-display-name@7.28.0", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-D6Eujc2zMxKjfa4Zxl4GHMsmhKKZ9VpcqIchJLvwTxad9zWIYulwYItBovpDOoNLISpcZSXoDJ5gaGbQUDqViA=="], + + "@babel/plugin-transform-react-jsx": ["@babel/plugin-transform-react-jsx@7.28.6", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-module-imports": "^7.28.6", "@babel/helper-plugin-utils": "^7.28.6", "@babel/plugin-syntax-jsx": "^7.28.6", "@babel/types": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-61bxqhiRfAACulXSLd/GxqmAedUSrRZIu/cbaT18T1CetkTmtDN15it7i80ru4DVqRK1WMxQhXs+Lf9kajm5Ow=="], + + "@babel/plugin-transform-react-jsx-development": ["@babel/plugin-transform-react-jsx-development@7.27.1", "", { "dependencies": { "@babel/plugin-transform-react-jsx": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-ykDdF5yI4f1WrAolLqeF3hmYU12j9ntLQl/AOG1HAS21jxyg1Q0/J/tpREuYLfatGdGmXp/3yS0ZA76kOlVq9Q=="], + + "@babel/plugin-transform-react-pure-annotations": ["@babel/plugin-transform-react-pure-annotations@7.27.1", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-JfuinvDOsD9FVMTHpzA/pBLisxpv1aSf+OIV8lgH3MuWrks19R27e6a6DipIg4aX1Zm9Wpb04p8wljfKrVSnPA=="], + "@babel/plugin-transform-typescript": ["@babel/plugin-transform-typescript@7.28.6", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-create-class-features-plugin": "^7.28.6", "@babel/helper-plugin-utils": "^7.28.6", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-0YWL2RFxOqEm9Efk5PvreamxPME8OyY0wM5wh5lHjF+VtVhdneCWGzZeSqzOfiobVqQaNCd2z0tQvnI9DaPWPw=="], + "@babel/preset-react": ["@babel/preset-react@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-option": "^7.27.1", "@babel/plugin-transform-react-display-name": "^7.28.0", "@babel/plugin-transform-react-jsx": "^7.27.1", "@babel/plugin-transform-react-jsx-development": "^7.27.1", "@babel/plugin-transform-react-pure-annotations": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Z3J8vhRq7CeLjdC58jLv4lnZ5RKFUJWqH5emvxmv9Hv3BD1T9R/Im713R4MTKwvFaV74ejZ3sM01LyEKk4ugNQ=="], + "@babel/preset-typescript": ["@babel/preset-typescript@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-option": "^7.27.1", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-transform-modules-commonjs": "^7.27.1", "@babel/plugin-transform-typescript": "^7.28.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-+bQy5WOI2V6LJZpPVxY+yp66XdZ2yifu0Mc1aP5CQKgjn4QM5IN2i5fAZ4xKop47pr8rpVhiAeu+nDQa12C8+g=="], "@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="], @@ -122,21 +133,21 @@ "@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], - "@better-auth/core": ["@better-auth/core@1.5.6", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.39.0", "@standard-schema/spec": "^1.1.0", "zod": "^4.3.6" }, "peerDependencies": { "@better-auth/utils": "0.3.1", "@better-fetch/fetch": "1.1.21", "@cloudflare/workers-types": ">=4", "@opentelemetry/api": "^1.9.0", "better-call": "1.3.2", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" }, "optionalPeers": ["@cloudflare/workers-types"] }, "sha512-Ez9DZdIMFyxHremmoLz1emFPGNQomDC1jqqBPnZ6Ci+6TiGN3R9w/Y03cJn6I8r1ycKgOzeVMZtJ/erOZ27Gsw=="], + "@better-auth/core": ["@better-auth/core@1.6.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.39.0", "@standard-schema/spec": "^1.1.0", "zod": "^4.3.6" }, "peerDependencies": { "@better-auth/utils": "0.4.0", "@better-fetch/fetch": "1.1.21", "@cloudflare/workers-types": ">=4", "@opentelemetry/api": "^1.9.0", "better-call": "1.3.5", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" }, "optionalPeers": ["@cloudflare/workers-types"] }, "sha512-LmdPTyKRDn6iCcXBGlOHOyzpJl1W/3w64zrEbhhHaWmtdpzQWlY8awlWBoDTL9eL4TAusr9dDvwIbMYTvEqaeA=="], - "@better-auth/drizzle-adapter": ["@better-auth/drizzle-adapter@1.5.6", "", { "peerDependencies": { "@better-auth/core": "1.5.6", "@better-auth/utils": "^0.3.0", "drizzle-orm": ">=0.41.0" }, "optionalPeers": ["drizzle-orm"] }, "sha512-VfFFmaoFw3ug12SiSuIwzrMoHyIVmkMGWm9gZ4sXdYYVX4HboCL4m3fjzOhppcmK5OGatRuU+N1UX6wxCITcXw=="], + "@better-auth/drizzle-adapter": ["@better-auth/drizzle-adapter@1.6.0", "", { "peerDependencies": { "@better-auth/core": "^1.6.0", "@better-auth/utils": "0.4.0", "drizzle-orm": ">=0.41.0" }, "optionalPeers": ["drizzle-orm"] }, "sha512-iMgvZlrL4FI63CGaxLqE5rgA2Q9VVmc2fQIP7N5E79nGAEpHtztstHFPlen9RDLRJA4xa3wuyVaPSILylwE+LA=="], - "@better-auth/kysely-adapter": ["@better-auth/kysely-adapter@1.5.6", "", { "peerDependencies": { "@better-auth/core": "1.5.6", "@better-auth/utils": "^0.3.0", "kysely": "^0.27.0 || ^0.28.0" }, "optionalPeers": ["kysely"] }, "sha512-Fnf+h8WVKtw6lEOmVmiVVzDf3shJtM60AYf9XTnbdCeUd6MxN/KnaJZpkgtYnRs7a+nwtkVB+fg4lGETebGFXQ=="], + "@better-auth/kysely-adapter": ["@better-auth/kysely-adapter@1.6.0", "", { "peerDependencies": { "@better-auth/core": "^1.6.0", "@better-auth/utils": "0.4.0", "kysely": "^0.27.0 || ^0.28.0" }, "optionalPeers": ["kysely"] }, "sha512-ZLEp2j3jquX7wrPQ7tPOSRAjmMoHhdrsgkuH9Bp/fgNZV7M1eiwAY6fHRGKad6KIldoI+iazMUIm60v11fIHCg=="], - "@better-auth/memory-adapter": ["@better-auth/memory-adapter@1.5.6", "", { "peerDependencies": { "@better-auth/core": "1.5.6", "@better-auth/utils": "^0.3.0" } }, "sha512-rS7ZsrIl5uvloUgNN0u9LOZJMMXnsZXVdUZ3MrTBKWM2KpoJjzPr9yN3Szyma5+0V7SltnzSGHPkYj2bEzzmlA=="], + "@better-auth/memory-adapter": ["@better-auth/memory-adapter@1.6.0", "", { "peerDependencies": { "@better-auth/core": "^1.6.0", "@better-auth/utils": "0.4.0" } }, "sha512-FbLmz6ujltw8RDUkBzutwIfoV+q9Mu0gLVrfhDAb9INe+jLcaQikiIjFdVwPzpx+bOs6bWTDfylrlI6+Ytxs3Q=="], - "@better-auth/mongo-adapter": ["@better-auth/mongo-adapter@1.5.6", "", { "peerDependencies": { "@better-auth/core": "1.5.6", "@better-auth/utils": "^0.3.0", "mongodb": "^6.0.0 || ^7.0.0" }, "optionalPeers": ["mongodb"] }, "sha512-6+M3MS2mor8fTUV3EI1FBLP0cs6QfbN+Ovx9+XxR/GdfKIBoNFzmPEPRbdGt+ft6PvrITsUm+T70+kkHgVSP6w=="], + "@better-auth/mongo-adapter": ["@better-auth/mongo-adapter@1.6.0", "", { "peerDependencies": { "@better-auth/core": "^1.6.0", "@better-auth/utils": "0.4.0", "mongodb": "^6.0.0 || ^7.0.0" }, "optionalPeers": ["mongodb"] }, "sha512-EYZwMpcpoaLRnfhEr+k+MTKS8SKi51TWh1b7bLSy+yHLL0PdbadFsGYZPgzLbZEaq4kUP0asMzXxA+blutjOQQ=="], - "@better-auth/prisma-adapter": ["@better-auth/prisma-adapter@1.5.6", "", { "peerDependencies": { "@better-auth/core": "1.5.6", "@better-auth/utils": "^0.3.0", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0" }, "optionalPeers": ["@prisma/client", "prisma"] }, "sha512-UxY9vQJs1Tt+O+T2YQnseDMlWmUSQvFZSBb5YiFRg7zcm+TEzujh4iX2/csA0YiZptLheovIuVWTP9nriewEBA=="], + "@better-auth/prisma-adapter": ["@better-auth/prisma-adapter@1.6.0", "", { "peerDependencies": { "@better-auth/core": "^1.6.0", "@better-auth/utils": "0.4.0", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0" }, "optionalPeers": ["@prisma/client", "prisma"] }, "sha512-8x/aqR1NckGiC49P02cxuH0wLzbJXvE/v2NnMEFo6h3uWq4ESYL0jTY9vNlFeVIKDyGSzrbteofzzG+yQv0wAQ=="], - "@better-auth/telemetry": ["@better-auth/telemetry@1.5.6", "", { "dependencies": { "@better-auth/utils": "0.3.1", "@better-fetch/fetch": "1.1.21" }, "peerDependencies": { "@better-auth/core": "1.5.6" } }, "sha512-yXC7NSxnIFlxDkGdpD7KA+J9nqIQAPCJKe77GoaC5bWoe/DALo1MYorZfTgOafS7wrslNtsPT4feV/LJi1ubqQ=="], + "@better-auth/telemetry": ["@better-auth/telemetry@1.6.0", "", { "peerDependencies": { "@better-auth/core": "^1.6.0", "@better-auth/utils": "0.4.0", "@better-fetch/fetch": "1.1.21" } }, "sha512-JrJyx1ioswEAh8rB7mVxEFIDLl6AK3W3rtqc2MK6BgvcmKveWJ730Eoi/PNvi0b4tFk4kczmuQITm69uMbnTvQ=="], - "@better-auth/utils": ["@better-auth/utils@0.3.1", "", {}, "sha512-+CGp4UmZSUrHHnpHhLPYu6cV+wSUSvVbZbNykxhUDocpVNTo9uFFxw/NqJlh1iC4wQ9HKKWGCKuZ5wUgS0v6Kg=="], + "@better-auth/utils": ["@better-auth/utils@0.4.0", "", { "dependencies": { "@noble/hashes": "^2.0.1" } }, "sha512-RpMtLUIQAEWMgdPLNVbIF5ON2mm+CH0U3rCdUCU1VyeAUui4m38DyK7/aXMLZov2YDjG684pS1D0MBllrmgjQA=="], "@better-fetch/fetch": ["@better-fetch/fetch@1.1.21", "", {}, "sha512-/ImESw0sskqlVR94jB+5+Pxjf+xBwDZF/N5+y2/q4EqD7IARUTSpPfIo8uf39SYpCxyOCtbyYpUrZ3F/k0zT4A=="], @@ -148,6 +159,10 @@ "@chevrotain/utils": ["@chevrotain/utils@10.5.0", "", {}, "sha512-hBzuU5+JjB2cqNZyszkDHZgOSrUUT8V3dhgRl8Q9Gp6dAj/H5+KILGjbhDpc3Iy9qmqlm/akuOI2ut9VUtzJxQ=="], + "@clack/core": ["@clack/core@0.5.0", "", { "dependencies": { "picocolors": "^1.0.0", "sisteransi": "^1.0.5" } }, "sha512-p3y0FIOwaYRUPRcMO7+dlmLh8PSRcrjuTndsiA0WAFbWES0mLZlrjVoBRZ9DzkPFJZG6KGkJmoEAY0ZcVWTkow=="], + + "@clack/prompts": ["@clack/prompts@0.11.0", "", { "dependencies": { "@clack/core": "0.5.0", "picocolors": "^1.0.0", "sisteransi": "^1.0.5" } }, "sha512-pMN5FcrEw9hUkZA4f+zLlzivQSeQf5dRGJjSUbvVYDLvpKCdQx5OaknvKzgbtXOizhP+SJJJjqEbOe55uKKfAw=="], + "@dotenvx/dotenvx": ["@dotenvx/dotenvx@1.53.0", "", { "dependencies": { "commander": "^11.1.0", "dotenv": "^17.2.1", "eciesjs": "^0.4.10", "execa": "^5.1.1", "fdir": "^6.2.0", "ignore": "^5.3.0", "object-treeify": "1.1.33", "picomatch": "^4.0.2", "which": "^4.0.0" }, "bin": { "dotenvx": "src/cli/dotenvx.js" } }, "sha512-OnsVuJ5O2WCUMXBnyuYah08/I6Tnt1FEZ2PGH9skSRuRh3LK5UoGa6Bzi5Toj/F/0mbeFfv+eNKTsYRoGgRh3Q=="], "@drizzle-team/brocli": ["@drizzle-team/brocli@0.10.2", "", {}, "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w=="], @@ -324,23 +339,23 @@ "@nanostores/react": ["@nanostores/react@1.1.0", "", { "peerDependencies": { "nanostores": "^1.2.0", "react": ">=18.0.0" } }, "sha512-MbH35fjhcf7LAubYX5vhOChYUfTLzNLqH/mBGLVsHkcvjy0F8crO1WQwdmQ2xKbAmtpalDa2zBt3Hlg5kqr8iw=="], - "@next/env": ["@next/env@16.2.1", "", {}, "sha512-n8P/HCkIWW+gVal2Z8XqXJ6aB3J0tuM29OcHpCsobWlChH/SITBs1DFBk/HajgrwDkqqBXPbuUuzgDvUekREPg=="], + "@next/env": ["@next/env@16.2.2", "", {}, "sha512-LqSGz5+xGk9EL/iBDr2yo/CgNQV6cFsNhRR2xhSXYh7B/hb4nePCxlmDvGEKG30NMHDFf0raqSyOZiQrO7BkHQ=="], - "@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.2.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-BwZ8w8YTaSEr2HIuXLMLxIdElNMPvY9fLqb20LX9A9OMGtJilhHLbCL3ggyd0TwjmMcTxi0XXt+ur1vWUoxj2Q=="], + "@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.2.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-B92G3ulrwmkDSEJEp9+XzGLex5wC1knrmCSIylyVeiAtCIfvEJYiN3v5kXPlYt5R4RFlsfO/v++aKV63Acrugg=="], - "@next/swc-darwin-x64": ["@next/swc-darwin-x64@16.2.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-/vrcE6iQSJq3uL3VGVHiXeaKbn8Es10DGTGRJnRZlkNQQk3kaNtAJg8Y6xuAlrx/6INKVjkfi5rY0iEXorZ6uA=="], + "@next/swc-darwin-x64": ["@next/swc-darwin-x64@16.2.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-7ZwSgNKJNQiwW0CKhNm9B1WS2L1Olc4B2XY0hPYCAL3epFnugMhuw5TMWzMilQ3QCZcCHoYm9NGWTHbr5REFxw=="], - "@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@16.2.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-uLn+0BK+C31LTVbQ/QU+UaVrV0rRSJQ8RfniQAHPghDdgE+SlroYqcmFnO5iNjNfVWCyKZHYrs3Nl0mUzWxbBw=="], + "@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@16.2.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-c3m8kBHMziMgo2fICOP/cd/5YlrxDU5YYjAJeQLyFsCqVF8xjOTH/QYG4a2u48CvvZZSj1eHQfBCbyh7kBr30Q=="], - "@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@16.2.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-ssKq6iMRnHdnycGp9hCuGnXJZ0YPr4/wNwrfE5DbmvEcgl9+yv97/Kq3TPVDfYome1SW5geciLB9aiEqKXQjlQ=="], + "@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@16.2.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-VKLuscm0P/mIfzt+SDdn2+8TNNJ7f0qfEkA+az7OqQbjzKdBxAHs0UvuiVoCtbwX+dqMEL9U54b5wQ/aN3dHeg=="], - "@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@16.2.1", "", { "os": "linux", "cpu": "x64" }, "sha512-HQm7SrHRELJ30T1TSmT706IWovFFSRGxfgUkyWJZF/RKBMdbdRWJuFrcpDdE5vy9UXjFOx6L3mRdqH04Mmx0hg=="], + "@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@16.2.2", "", { "os": "linux", "cpu": "x64" }, "sha512-kU3OPHJq6sBUjOk7wc5zJ7/lipn8yGldMoAv4z67j6ov6Xo/JvzA7L7LCsyzzsXmgLEhk3Qkpwqaq/1+XpNR3g=="], - "@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@16.2.1", "", { "os": "linux", "cpu": "x64" }, "sha512-aV2iUaC/5HGEpbBkE+4B8aHIudoOy5DYekAKOMSHoIYQ66y/wIVeaRx8MS2ZMdxe/HIXlMho4ubdZs/J8441Tg=="], + "@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@16.2.2", "", { "os": "linux", "cpu": "x64" }, "sha512-CKXRILyErMtUftp+coGcZ38ZwE/Aqq45VMCcRLr2I4OXKrgxIBDXHnBgeX/UMil0S09i2JXaDL3Q+TN8D/cKmg=="], - "@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@16.2.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-IXdNgiDHaSk0ZUJ+xp0OQTdTgnpx1RCfRTalhn3cjOP+IddTMINwA7DXZrwTmGDO8SUr5q2hdP/du4DcrB1GxA=="], + "@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@16.2.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-sS/jSk5VUoShUqINJFvNjVT7JfR5ORYj/+/ZpOYbbIohv/lQfduWnGAycq2wlknbOql2xOR0DoV0s6Xfcy49+g=="], - "@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.2.1", "", { "os": "win32", "cpu": "x64" }, "sha512-qvU+3a39Hay+ieIztkGSbF7+mccbbg1Tk25hc4JDylf8IHjYmY/Zm64Qq1602yPyQqvie+vf5T/uPwNxDNIoeg=="], + "@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.2.2", "", { "os": "win32", "cpu": "x64" }, "sha512-aHaKceJgdySReT7qeck5oShucxWRiiEuwCGK8HHALe6yZga8uyFpLkPgaRw3kkF04U7ROogL/suYCNt/+CuXGA=="], "@noble/ciphers": ["@noble/ciphers@2.1.1", "", {}, "sha512-bysYuiVfhxNJuldNXlFEitTVdNnYUc+XNJZd7Qm2a5j1vZHgY+fazadNFWFaMK/2vye0JVlxV3gHmC0WDfAOQw=="], @@ -516,7 +531,7 @@ "@react-email/column": ["@react-email/column@0.0.14", "", { "peerDependencies": { "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-f+W+Bk2AjNO77zynE33rHuQhyqVICx4RYtGX9NKsGUg0wWjdGP0qAuIkhx9Rnmk4/hFMo1fUrtYNqca9fwJdHg=="], - "@react-email/components": ["@react-email/components@1.0.10", "", { "dependencies": { "@react-email/body": "0.3.0", "@react-email/button": "0.2.1", "@react-email/code-block": "0.2.1", "@react-email/code-inline": "0.0.6", "@react-email/column": "0.0.14", "@react-email/container": "0.0.16", "@react-email/font": "0.0.10", "@react-email/head": "0.0.13", "@react-email/heading": "0.0.16", "@react-email/hr": "0.0.12", "@react-email/html": "0.0.12", "@react-email/img": "0.0.12", "@react-email/link": "0.0.13", "@react-email/markdown": "0.0.18", "@react-email/preview": "0.0.14", "@react-email/render": "2.0.4", "@react-email/row": "0.0.13", "@react-email/section": "0.0.17", "@react-email/tailwind": "2.0.6", "@react-email/text": "0.1.6" }, "peerDependencies": { "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-r/BnqfAjr3apcvn/NDx2DqNRD5BP5wZLRdjn2IVHXjt4KmQ5RHWSCAvFiXAzRHys1BWQ2zgIc7cpWePUcAl+nw=="], + "@react-email/components": ["@react-email/components@1.0.11", "", { "dependencies": { "@react-email/body": "0.3.0", "@react-email/button": "0.2.1", "@react-email/code-block": "0.2.1", "@react-email/code-inline": "0.0.6", "@react-email/column": "0.0.14", "@react-email/container": "0.0.16", "@react-email/font": "0.0.10", "@react-email/head": "0.0.13", "@react-email/heading": "0.0.16", "@react-email/hr": "0.0.12", "@react-email/html": "0.0.12", "@react-email/img": "0.0.12", "@react-email/link": "0.0.13", "@react-email/markdown": "0.0.18", "@react-email/preview": "0.0.14", "@react-email/render": "2.0.5", "@react-email/row": "0.0.13", "@react-email/section": "0.0.17", "@react-email/tailwind": "2.0.7", "@react-email/text": "0.1.6" }, "peerDependencies": { "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-s0CX31+S/u1MhBWYFAuZru0NHNExTY+OeZC9OrGyzl8PGQ0Iz/4gq3O4rHUVuA1D7FjAcPbwG1Up0yey/Xh6dw=="], "@react-email/container": ["@react-email/container@0.0.16", "", { "peerDependencies": { "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-QWBB56RkkU0AJ9h+qy33gfT5iuZknPC7Un/IjZv9B0QmMIK+WWacc0cH6y2SV5Cv/b99hU94fjEMOOO4enpkbQ=="], @@ -538,13 +553,13 @@ "@react-email/preview": ["@react-email/preview@0.0.14", "", { "peerDependencies": { "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-aYK8q0IPkBXyMsbpMXgxazwHxYJxTrXrV95GFuu2HbEiIToMwSyUgb8HDFYwPqqfV03/jbwqlsXmFxsOd+VNaw=="], - "@react-email/render": ["@react-email/render@2.0.4", "", { "dependencies": { "html-to-text": "^9.0.5", "prettier": "^3.5.3" }, "peerDependencies": { "react": "^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-kht2oTFQ1SwrLpd882ahTvUtNa9s53CERHstiTbzhm6aR2Hbykp/mQ4tpPvsBGkKAEvKRlDEoooh60Uk6nHK1g=="], + "@react-email/render": ["@react-email/render@2.0.5", "", { "dependencies": { "html-to-text": "^9.0.5", "prettier": "^3.5.3" }, "peerDependencies": { "react": "^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-oAsSpY/vYt9ReDcRQDBLxENwCNAklkE6bvP5Kl9ZlmVr/RZpfhloJp8xc/OZki/YF2nisRRX50aEy8P9v3R5GA=="], "@react-email/row": ["@react-email/row@0.0.13", "", { "peerDependencies": { "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-bYnOac40vIKCId7IkwuLAAsa3fKfSfqCvv6epJKmPE0JBuu5qI4FHFCl9o9dVpIIS08s/ub+Y/txoMt0dYziGw=="], "@react-email/section": ["@react-email/section@0.0.17", "", { "peerDependencies": { "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-qNl65ye3W0Rd5udhdORzTV9ezjb+GFqQQSae03NDzXtmJq6sqVXNWNiVolAjvJNypim+zGXmv6J9TcV5aNtE/w=="], - "@react-email/tailwind": ["@react-email/tailwind@2.0.6", "", { "dependencies": { "tailwindcss": "4.1.18" }, "peerDependencies": { "@react-email/body": ">=0", "@react-email/button": ">=0", "@react-email/code-block": ">=0", "@react-email/code-inline": ">=0", "@react-email/container": ">=0", "@react-email/heading": ">=0", "@react-email/hr": ">=0", "@react-email/img": ">=0", "@react-email/link": ">=0", "@react-email/preview": ">=0", "@react-email/text": ">=0", "react": "^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@react-email/body", "@react-email/button", "@react-email/code-block", "@react-email/code-inline", "@react-email/container", "@react-email/heading", "@react-email/hr", "@react-email/img", "@react-email/link", "@react-email/preview"] }, "sha512-3PgL/GYWmgS+puLPQ2aLlsplHSOFztRl70fowBkbLIb8ZUIgvx5YId6zYCCHeM2+DQ/EG3iXXqLNTahVztuMqQ=="], + "@react-email/tailwind": ["@react-email/tailwind@2.0.7", "", { "dependencies": { "tailwindcss": "^4.1.18" }, "peerDependencies": { "@react-email/body": ">=0", "@react-email/button": ">=0", "@react-email/code-block": ">=0", "@react-email/code-inline": ">=0", "@react-email/container": ">=0", "@react-email/heading": ">=0", "@react-email/hr": ">=0", "@react-email/img": ">=0", "@react-email/link": ">=0", "@react-email/preview": ">=0", "@react-email/text": ">=0", "react": "^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@react-email/body", "@react-email/button", "@react-email/code-block", "@react-email/code-inline", "@react-email/container", "@react-email/heading", "@react-email/hr", "@react-email/img", "@react-email/link", "@react-email/preview"] }, "sha512-kGw80weVFXikcnCXbigTGXGWQ0MRCSYNCudcdkWxebkWYd0FG6/NPoN3V1p/u68/4+NxZwYPVi2fhnp0x23HdA=="], "@react-email/text": ["@react-email/text@0.1.6", "", { "peerDependencies": { "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-TYqkioRS45wTR5il3dYk/SbUjjEdhSwh9BtRNB99qNH1pXAwA45H7rAuxehiu8iJQJH0IyIr+6n62gBz9ezmsw=="], @@ -602,9 +617,9 @@ "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], - "@types/node": ["@types/node@25.5.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw=="], + "@types/node": ["@types/node@25.5.2", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-tO4ZIRKNC+MDWV4qKVZe3Ql/woTnmHDr5JD8UI5hn2pwBrHEwOEMZK7WlNb5RKB6EoJ02gwmQS9OrjuFnZYdpg=="], - "@types/nodemailer": ["@types/nodemailer@7.0.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-E+U4RzR2dKrx+u3N4DlsmLaDC6mMZOM/TPROxA0UAPiTgI0y4CEFBmZE+coGWTjakDriRsXG368lNk1u9Q0a2g=="], + "@types/nodemailer": ["@types/nodemailer@8.0.0", "", { "dependencies": { "@types/node": "*" } }, "sha512-fyf8jWULsCo0d0BuoQ75i6IeoHs47qcqxWc7yUdUcV0pOZGjUTTOvwdG1PRXUDqN/8A64yQdQdnA2pZgcdi+cA=="], "@types/pg": ["@types/pg@8.20.0", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow=="], @@ -638,6 +653,8 @@ "atomically": ["atomically@2.1.1", "", { "dependencies": { "stubborn-fs": "^2.0.0", "when-exit": "^2.1.4" } }, "sha512-P4w9o2dqARji6P7MHprklbfiArZAWvo07yW7qs3pdljb3BWr12FIB7W+p0zJiuiVsUpRO0iZn1kFFcpPegg0tQ=="], + "auth": ["auth@1.6.0", "", { "dependencies": { "@babel/core": "^7.29.0", "@babel/preset-react": "^7.28.5", "@babel/preset-typescript": "^7.28.5", "@better-auth/core": "1.6.0", "@better-auth/telemetry": "1.6.0", "@better-auth/utils": "0.4.0", "@clack/prompts": "^0.11.0", "@mrleebo/prisma-ast": "^0.13.1", "better-auth": "1.6.0", "c12": "^3.3.3", "chalk": "^5.6.2", "commander": "^12.1.0", "dotenv": "^17.3.1", "get-tsconfig": "^4.13.6", "open": "^10.2.0", "prettier": "^3.8.1", "prompts": "^2.4.2", "semver": "^7.7.4", "yocto-spinner": "^0.2.3", "zod": "^4.3.6" }, "bin": { "better-auth": "dist/index.mjs", "auth": "dist/index.mjs" } }, "sha512-SLsmXisEPCr3iCU6WufTb+8jyQTAl54sDSOBAsibz5jqj6vuko0wxbs+iPN4sB1ibdkUN25pWh344F87jv10TQ=="], + "aws-ssl-profiles": ["aws-ssl-profiles@1.1.2", "", {}, "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g=="], "babel-plugin-react-compiler": ["babel-plugin-react-compiler@1.0.0", "", { "dependencies": { "@babel/types": "^7.26.0" } }, "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw=="], @@ -648,9 +665,9 @@ "baseline-browser-mapping": ["baseline-browser-mapping@2.10.0", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA=="], - "better-auth": ["better-auth@1.5.6", "", { "dependencies": { "@better-auth/core": "1.5.6", "@better-auth/drizzle-adapter": "1.5.6", "@better-auth/kysely-adapter": "1.5.6", "@better-auth/memory-adapter": "1.5.6", "@better-auth/mongo-adapter": "1.5.6", "@better-auth/prisma-adapter": "1.5.6", "@better-auth/telemetry": "1.5.6", "@better-auth/utils": "0.3.1", "@better-fetch/fetch": "1.1.21", "@noble/ciphers": "^2.1.1", "@noble/hashes": "^2.0.1", "better-call": "1.3.2", "defu": "^6.1.4", "jose": "^6.1.3", "kysely": "^0.28.12", "nanostores": "^1.1.1", "zod": "^4.3.6" }, "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", "@tanstack/solid-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", "@tanstack/solid-start", "better-sqlite3", "drizzle-kit", "drizzle-orm", "mongodb", "mysql2", "next", "pg", "prisma", "react", "react-dom", "solid-js", "svelte", "vitest", "vue"] }, "sha512-QSpJTqaT1XVfWRQe/fm3PgeuwOIlz1nWX/Dx7nsHStJ382bLzmDbQk2u7IT0IJ6wS5SRxfqEE1Ev9TXontgyAQ=="], + "better-auth": ["better-auth@1.6.0", "", { "dependencies": { "@better-auth/core": "1.6.0", "@better-auth/drizzle-adapter": "1.6.0", "@better-auth/kysely-adapter": "1.6.0", "@better-auth/memory-adapter": "1.6.0", "@better-auth/mongo-adapter": "1.6.0", "@better-auth/prisma-adapter": "1.6.0", "@better-auth/telemetry": "1.6.0", "@better-auth/utils": "0.4.0", "@better-fetch/fetch": "1.1.21", "@noble/ciphers": "^2.1.1", "@noble/hashes": "^2.0.1", "better-call": "1.3.5", "defu": "^6.1.4", "jose": "^6.1.3", "kysely": "^0.28.14", "nanostores": "^1.1.1", "zod": "^4.3.6" }, "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", "@tanstack/solid-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", "@tanstack/solid-start", "better-sqlite3", "drizzle-kit", "drizzle-orm", "mongodb", "mysql2", "next", "pg", "prisma", "react", "react-dom", "solid-js", "svelte", "vitest", "vue"] }, "sha512-reEK4X37w/X0Wi0ZpNSo6w3j9F2tsA7ebWn2AmWTzkceiatkxcadRg9aK+Mirw2PY56GQqX9dBgqBG6XMNU/Zg=="], - "better-call": ["better-call@1.3.2", "", { "dependencies": { "@better-auth/utils": "^0.3.1", "@better-fetch/fetch": "^1.1.21", "rou3": "^0.7.12", "set-cookie-parser": "^3.0.1" }, "peerDependencies": { "zod": "^4.0.0" }, "optionalPeers": ["zod"] }, "sha512-4cZIfrerDsNTn3cm+MhLbUePN0gdwkhSXEuG7r/zuQ8c/H7iU0/jSK5TD3FW7U0MgKHce/8jGpPYNO4Ve+4NBw=="], + "better-call": ["better-call@1.3.5", "", { "dependencies": { "@better-auth/utils": "^0.4.0", "@better-fetch/fetch": "^1.1.21", "rou3": "^0.7.12", "set-cookie-parser": "^3.0.1" }, "peerDependencies": { "zod": "^4.0.0" }, "optionalPeers": ["zod"] }, "sha512-kOFJkBP7utAQLEYrobZm3vkTH8mXq5GNgvjc5/XEST1ilVHaxXUXfeDeFlqoETMtyqS4+3/h4ONX2i++ebZrvA=="], "block-stream2": ["block-stream2@2.1.0", "", { "dependencies": { "readable-stream": "^3.4.0" } }, "sha512-suhjmLI57Ewpmq00qaygS8UgEq2ly2PCItenIyhMqVjo4t4pGzqMvfgJuX8iWTeSDdfSSqS6j38fL4ToNL7Pfg=="], @@ -668,7 +685,7 @@ "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], - "bullmq": ["bullmq@5.71.1", "", { "dependencies": { "cron-parser": "4.9.0", "ioredis": "5.10.1", "msgpackr": "1.11.5", "node-abort-controller": "3.1.1", "semver": "7.7.4", "tslib": "2.8.1", "uuid": "11.1.0" } }, "sha512-kOBfdcsHmO6wwmIjpersoVdYQ7jkjTgky4Yop0loc7QwSdgxliSzD69U9ijZuRrkyCJwz5p5eqxeGeQkJ0YGZQ=="], + "bullmq": ["bullmq@5.73.1", "", { "dependencies": { "cron-parser": "4.9.0", "ioredis": "5.10.1", "msgpackr": "1.11.5", "node-abort-controller": "3.1.1", "semver": "7.7.4", "tslib": "2.8.1", "uuid": "11.1.0" } }, "sha512-BdcY5R8PR62VziZGBmjDqPDb1Hhok2j8CJRslAI03sqzJ8k0wW1m9doBjdzBk7rrwNc3wb18bL9m/dniJ9N14g=="], "bun-types": ["bun-types@1.3.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-1KGPpoxQWl9f6wcZh57LvrPIInQMn2TQ7jsgxqpRzg+l0QPOFvJVH7HmvHo/AiPgwXy+/Thf6Ov3EdVn1vOabg=="], @@ -676,7 +693,7 @@ "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], - "c12": ["c12@3.1.0", "", { "dependencies": { "chokidar": "^4.0.3", "confbox": "^0.2.2", "defu": "^6.1.4", "dotenv": "^16.6.1", "exsolve": "^1.0.7", "giget": "^2.0.0", "jiti": "^2.4.2", "ohash": "^2.0.11", "pathe": "^2.0.3", "perfect-debounce": "^1.0.0", "pkg-types": "^2.2.0", "rc9": "^2.1.2" }, "peerDependencies": { "magicast": "^0.3.5" }, "optionalPeers": ["magicast"] }, "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw=="], + "c12": ["c12@3.3.3", "", { "dependencies": { "chokidar": "^5.0.0", "confbox": "^0.2.2", "defu": "^6.1.4", "dotenv": "^17.2.3", "exsolve": "^1.0.8", "giget": "^2.0.0", "jiti": "^2.6.1", "ohash": "^2.0.11", "pathe": "^2.0.3", "perfect-debounce": "^2.0.0", "pkg-types": "^2.3.0", "rc9": "^2.1.2" }, "peerDependencies": { "magicast": "*" }, "optionalPeers": ["magicast"] }, "sha512-750hTRvgBy5kcMNPdh95Qo+XUBeGo8C7nsKSmedDmaQI+E0r82DwHeM6vBewDe4rGFbnxoa4V9pw+sPh5+Iz8Q=="], "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], @@ -716,7 +733,7 @@ "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], - "commander": ["commander@13.1.0", "", {}, "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw=="], + "commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="], "conf": ["conf@15.1.0", "", { "dependencies": { "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "atomically": "^2.0.3", "debounce-fn": "^6.0.0", "dot-prop": "^10.0.0", "env-paths": "^3.0.0", "json-schema-typed": "^8.0.1", "semver": "^7.7.2", "uint8array-extras": "^1.5.0" } }, "sha512-Uy5YN9KEu0WWDaZAVJ5FAmZoaJt9rdK6kH+utItPyGsCqCgaTKkrmZx3zoE0/3q6S3bcp3Ihkk+ZqPxWxFK5og=="], @@ -782,7 +799,7 @@ "detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="], - "dexie": ["dexie@4.4.1", "", {}, "sha512-4Xec5+yrS+TgyFAnMrneFOt/QG8sD3FxlkUVpfypui3SriRN80UN0SZBWmkNAY7ulfKgk0ilvv7M6pBURprdgA=="], + "dexie": ["dexie@4.4.2", "", {}, "sha512-zMtV8q79EFE5U8FKZvt0Y/77PCU/Hr/RDxv1EDeo228L+m/HTbeN2AjoQm674rhQCX8n3ljK87lajt7UQuZfvw=="], "dexie-react-hooks": ["dexie-react-hooks@4.4.0", "", { "peerDependencies": { "dexie": ">=4.2.0-alpha.1 <5.0.0", "react": ">=16" } }, "sha512-ObLXBS5+4BJU8vtSvBx6b9fY6zZYgniAtwxzjCHsUQadgbqYN6935X2/1TWw4Rf2N1aZV1io5/ziox4vKuxABA=="], @@ -798,7 +815,7 @@ "dot-prop": ["dot-prop@10.1.0", "", { "dependencies": { "type-fest": "^5.0.0" } }, "sha512-MVUtAugQMOff5RnBy2d9N31iG0lNwg1qAoAOn7pOK5wf94WIaE3My2p3uwTQuvS2AcqchkcR3bHByjaM0mmi7Q=="], - "dotenv": ["dotenv@17.3.1", "", {}, "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA=="], + "dotenv": ["dotenv@17.4.1", "", {}, "sha512-k8DaKGP6r1G30Lx8V4+pCsLzKr8vLmV2paqEj1Y55GdAgJuIqpRp5FfajGF8KtwMxCz9qJc6wUIJnm053d/WCw=="], "drizzle-kit": ["drizzle-kit@0.31.10", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.25.4", "tsx": "^4.21.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-7OZcmQUrdGI+DUNNsKBn1aW8qSoKuTH7d0mYgSP8bAzdFzKoovxEFnoGQp2dVs82EOJeYycqRtciopszwUf8bw=="], @@ -1084,7 +1101,7 @@ "long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="], - "lru-cache": ["lru-cache@11.2.6", "", {}, "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ=="], + "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], "lru.min": ["lru.min@1.1.4", "", {}, "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA=="], @@ -1148,7 +1165,7 @@ "negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], - "next": ["next@16.2.1", "", { "dependencies": { "@next/env": "16.2.1", "@swc/helpers": "0.5.15", "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.2.1", "@next/swc-darwin-x64": "16.2.1", "@next/swc-linux-arm64-gnu": "16.2.1", "@next/swc-linux-arm64-musl": "16.2.1", "@next/swc-linux-x64-gnu": "16.2.1", "@next/swc-linux-x64-musl": "16.2.1", "@next/swc-win32-arm64-msvc": "16.2.1", "@next/swc-win32-x64-msvc": "16.2.1", "sharp": "^0.34.5" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-VaChzNL7o9rbfdt60HUj8tev4m6d7iC1igAy157526+cJlXOQu5LzsBXNT+xaJnTP/k+utSX5vMv7m0G+zKH+Q=="], + "next": ["next@16.2.2", "", { "dependencies": { "@next/env": "16.2.2", "@swc/helpers": "0.5.15", "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.2.2", "@next/swc-darwin-x64": "16.2.2", "@next/swc-linux-arm64-gnu": "16.2.2", "@next/swc-linux-arm64-musl": "16.2.2", "@next/swc-linux-x64-gnu": "16.2.2", "@next/swc-linux-x64-musl": "16.2.2", "@next/swc-win32-arm64-msvc": "16.2.2", "@next/swc-win32-x64-msvc": "16.2.2", "sharp": "^0.34.5" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-i6AJdyVa4oQjyvX/6GeER8dpY/xlIV+4NMv/svykcLtURJSy/WzDnnUk/TM4d0uewFHK7xSQz4TbIwPgjky+3A=="], "next-themes": ["next-themes@0.4.6", "", { "peerDependencies": { "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA=="], @@ -1164,7 +1181,7 @@ "node-releases": ["node-releases@2.0.36", "", {}, "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA=="], - "nodemailer": ["nodemailer@8.0.4", "", {}, "sha512-k+jf6N8PfQJ0Fe8ZhJlgqU5qJU44Lpvp2yvidH3vp1lPnVQMgi4yEEMPXg5eJS1gFIJTVq1NHBk7Ia9ARdSBdQ=="], + "nodemailer": ["nodemailer@8.0.5", "", {}, "sha512-0PF8Yb1yZuQfQbq+5/pZJrtF6WQcjTd5/S4JOHs9PGFxuTqoB/icwuB44pOdURHJbRKX1PPoJZtY7R4VUoCC8w=="], "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], @@ -1186,7 +1203,7 @@ "onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="], - "open": ["open@11.0.0", "", { "dependencies": { "default-browser": "^5.4.0", "define-lazy-prop": "^3.0.0", "is-in-ssh": "^1.0.0", "is-inside-container": "^1.0.0", "powershell-utils": "^0.1.0", "wsl-utils": "^0.3.0" } }, "sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw=="], + "open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="], "ora": ["ora@8.2.0", "", { "dependencies": { "chalk": "^5.3.0", "cli-cursor": "^5.0.0", "cli-spinners": "^2.9.2", "is-interactive": "^2.0.0", "is-unicode-supported": "^2.0.0", "log-symbols": "^6.0.0", "stdin-discarder": "^0.2.2", "string-width": "^7.2.0", "strip-ansi": "^7.1.0" } }, "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw=="], @@ -1216,7 +1233,7 @@ "peberminta": ["peberminta@0.9.0", "", {}, "sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ=="], - "perfect-debounce": ["perfect-debounce@1.0.0", "", {}, "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA=="], + "perfect-debounce": ["perfect-debounce@2.1.0", "", {}, "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g=="], "pg": ["pg@8.20.0", "", { "dependencies": { "pg-connection-string": "^2.12.0", "pg-pool": "^3.13.0", "pg-protocol": "^1.13.0", "pg-types": "2.2.0", "pgpass": "1.0.5" }, "optionalDependencies": { "pg-cloudflare": "^1.3.0" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA=="], @@ -1294,7 +1311,7 @@ "react-email": ["react-email@5.2.10", "", { "dependencies": { "@babel/parser": "7.27.0", "@babel/traverse": "7.27.0", "chokidar": "^4.0.3", "commander": "^13.0.0", "conf": "^15.0.2", "debounce": "^2.0.0", "esbuild": "0.27.3", "glob": "^13.0.6", "jiti": "2.4.2", "log-symbols": "^7.0.0", "mime-types": "^3.0.0", "normalize-path": "^3.0.0", "nypm": "0.6.2", "ora": "^8.0.0", "prompts": "2.4.2", "socket.io": "^4.8.1", "tsconfig-paths": "4.2.0" }, "bin": { "email": "dist/index.mjs" } }, "sha512-Ys8yR5/a0nXf5u2GlT2UV93PJHC3ZnuMnNebEn7I5UE9XfMFPtlpgDs02mPJOJn49fhJjDTWIUlZD1vmQPDgJg=="], - "react-hook-form": ["react-hook-form@7.72.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-V4v6jubaf6JAurEaVnT9aUPKFbNtDgohj5CIgVGyPHvT9wRx5OZHVjz31GsxnPNI278XMu+ruFz+wGOscHaLKw=="], + "react-hook-form": ["react-hook-form@7.72.1", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-RhwBoy2ygeVZje+C+bwJ8g0NjTdBmDlJvAUHTxRjTmSUKPYsKfMphkS2sgEMotsY03bP358yEYlnUeZy//D9Ig=="], "react-remove-scroll": ["react-remove-scroll@2.7.2", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q=="], @@ -1362,7 +1379,7 @@ "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], - "shadcn": ["shadcn@4.1.1", "", { "dependencies": { "@babel/core": "^7.28.0", "@babel/parser": "^7.28.0", "@babel/plugin-transform-typescript": "^7.28.0", "@babel/preset-typescript": "^7.27.1", "@dotenvx/dotenvx": "^1.48.4", "@modelcontextprotocol/sdk": "^1.26.0", "@types/validate-npm-package-name": "^4.0.2", "browserslist": "^4.26.2", "commander": "^14.0.0", "cosmiconfig": "^9.0.0", "dedent": "^1.6.0", "deepmerge": "^4.3.1", "diff": "^8.0.2", "execa": "^9.6.0", "fast-glob": "^3.3.3", "fs-extra": "^11.3.1", "fuzzysort": "^3.1.0", "https-proxy-agent": "^7.0.6", "kleur": "^4.1.5", "msw": "^2.10.4", "node-fetch": "^3.3.2", "open": "^11.0.0", "ora": "^8.2.0", "postcss": "^8.5.6", "postcss-selector-parser": "^7.1.0", "prompts": "^2.4.2", "recast": "^0.23.11", "stringify-object": "^5.0.0", "tailwind-merge": "^3.0.1", "ts-morph": "^26.0.0", "tsconfig-paths": "^4.2.0", "validate-npm-package-name": "^7.0.1", "zod": "^3.24.1", "zod-to-json-schema": "^3.24.6" }, "bin": { "shadcn": "dist/index.js" } }, "sha512-nBj+7LYC9kzV9v9QmRPpoOhfW4KctJVQejywdAt/K+K+z4RYlJOcO2a4AaF7elrRWkfCbgXeGK02liV0KB9HvQ=="], + "shadcn": ["shadcn@4.2.0", "", { "dependencies": { "@babel/core": "^7.28.0", "@babel/parser": "^7.28.0", "@babel/plugin-transform-typescript": "^7.28.0", "@babel/preset-typescript": "^7.27.1", "@dotenvx/dotenvx": "^1.48.4", "@modelcontextprotocol/sdk": "^1.26.0", "@types/validate-npm-package-name": "^4.0.2", "browserslist": "^4.26.2", "commander": "^14.0.0", "cosmiconfig": "^9.0.0", "dedent": "^1.6.0", "deepmerge": "^4.3.1", "diff": "^8.0.2", "execa": "^9.6.0", "fast-glob": "^3.3.3", "fs-extra": "^11.3.1", "fuzzysort": "^3.1.0", "https-proxy-agent": "^7.0.6", "kleur": "^4.1.5", "msw": "^2.10.4", "node-fetch": "^3.3.2", "open": "^11.0.0", "ora": "^8.2.0", "postcss": "^8.5.6", "postcss-selector-parser": "^7.1.0", "prompts": "^2.4.2", "recast": "^0.23.11", "stringify-object": "^5.0.0", "tailwind-merge": "^3.0.1", "ts-morph": "^26.0.0", "tsconfig-paths": "^4.2.0", "validate-npm-package-name": "^7.0.1", "zod": "^3.24.1", "zod-to-json-schema": "^3.24.6" }, "bin": { "shadcn": "dist/index.js" } }, "sha512-ZDuV340itidaUd4Gi1BxQX+Y7Ush6BHp6URZBM2RyxUUBZ6yFtOWIr4nVY+Ro+YRSpo82v7JrsmtcU5xoBCMJQ=="], "sharp": ["sharp@0.34.5", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="], @@ -1526,7 +1543,7 @@ "ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="], - "wsl-utils": ["wsl-utils@0.3.1", "", { "dependencies": { "is-wsl": "^3.1.0", "powershell-utils": "^0.1.0" } }, "sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg=="], + "wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="], "xml2js": ["xml2js@0.6.2", "", { "dependencies": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" } }, "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA=="], @@ -1544,6 +1561,8 @@ "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], + "yocto-spinner": ["yocto-spinner@0.2.3", "", { "dependencies": { "yoctocolors": "^2.1.1" } }, "sha512-sqBChb33loEnkoXte1bLg45bEBsOP9N1kzQh5JZNKj/0rik4zAPTNSAVPj3uQAdc6slYJ0Ksc403G2XgxsJQFQ=="], + "yoctocolors": ["yoctocolors@2.1.2", "", {}, "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug=="], "yoctocolors-cjs": ["yoctocolors-cjs@2.1.3", "", {}, "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw=="], @@ -1562,8 +1581,6 @@ "@babel/generator/@babel/parser": ["@babel/parser@7.29.0", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww=="], - "@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], - "@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], "@babel/helper-create-class-features-plugin/@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="], @@ -1586,6 +1603,8 @@ "@dotenvx/dotenvx/commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="], + "@dotenvx/dotenvx/dotenv": ["dotenv@17.3.1", "", {}, "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA=="], + "@dotenvx/dotenvx/execa": ["execa@5.1.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", "human-signals": "^2.1.0", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^4.0.1", "onetime": "^5.1.2", "signal-exit": "^3.0.3", "strip-final-newline": "^2.0.0" } }, "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg=="], "@dotenvx/dotenvx/which": ["which@4.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg=="], @@ -1596,14 +1615,14 @@ "@noble/curves/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], + "@prisma/config/c12": ["c12@3.1.0", "", { "dependencies": { "chokidar": "^4.0.3", "confbox": "^0.2.2", "defu": "^6.1.4", "dotenv": "^16.6.1", "exsolve": "^1.0.7", "giget": "^2.0.0", "jiti": "^2.4.2", "ohash": "^2.0.11", "pathe": "^2.0.3", "perfect-debounce": "^1.0.0", "pkg-types": "^2.2.0", "rc9": "^2.1.2" }, "peerDependencies": { "magicast": "^0.3.5" }, "optionalPeers": ["magicast"] }, "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw=="], + "@prisma/engines/@prisma/get-platform": ["@prisma/get-platform@7.4.2", "", { "dependencies": { "@prisma/debug": "7.4.2" } }, "sha512-UTnChXRwiauzl/8wT4hhe7Xmixja9WE28oCnGpBtRejaHhvekx5kudr3R4Y9mLSA0kqGnAMeyTiKwDVMjaEVsw=="], "@prisma/fetch-engine/@prisma/get-platform": ["@prisma/get-platform@7.4.2", "", { "dependencies": { "@prisma/debug": "7.4.2" } }, "sha512-UTnChXRwiauzl/8wT4hhe7Xmixja9WE28oCnGpBtRejaHhvekx5kudr3R4Y9mLSA0kqGnAMeyTiKwDVMjaEVsw=="], "@prisma/get-platform/@prisma/debug": ["@prisma/debug@7.2.0", "", {}, "sha512-YSGTiSlBAVJPzX4ONZmMotL+ozJwQjRmZweQNIq/ER0tQJKJynNkRB3kyvt37eOfsbMCXk3gnLF6J9OJ4QWftw=="], - "@react-email/tailwind/tailwindcss": ["tailwindcss@4.1.18", "", {}, "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw=="], - "@tailwindcss/node/jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="], @@ -1620,11 +1639,15 @@ "@types/cors/@types/node": ["@types/node@20.19.35", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-Uarfe6J91b9HAUXxjvSOdiO2UPOKLm07Q1oh0JHxoZ1y8HoqxDAu3gVrsrOHeiio0kSsoVBt4wFrKOm0dKxVPQ=="], - "@types/nodemailer/@types/node": ["@types/node@20.19.36", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-+3TQ+XhRjbmeKGHMhmUZfKlkF2/mAc+PpO2B90PBI7hRpkgPCSo5PaJ8tfWBJ4LMIuqrnKLD5TveeGMy+curtg=="], + "@types/pg/@types/node": ["@types/node@25.5.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw=="], "accepts/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], - "c12/dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], + "bun-types/@types/node": ["@types/node@25.5.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw=="], + + "c12/chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="], + + "c12/dotenv": ["dotenv@17.3.1", "", {}, "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA=="], "c12/jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], @@ -1660,12 +1683,16 @@ "ora/log-symbols": ["log-symbols@6.0.0", "", { "dependencies": { "chalk": "^5.3.0", "is-unicode-supported": "^1.3.0" } }, "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw=="], + "path-scurry/lru-cache": ["lru-cache@11.2.6", "", {}, "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ=="], + "prompts/kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="], "proper-lockfile/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], "proxy-addr/ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], + "react-email/commander": ["commander@13.1.0", "", {}, "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw=="], + "react-email/esbuild": ["esbuild@0.27.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.3", "@esbuild/android-arm": "0.27.3", "@esbuild/android-arm64": "0.27.3", "@esbuild/android-x64": "0.27.3", "@esbuild/darwin-arm64": "0.27.3", "@esbuild/darwin-x64": "0.27.3", "@esbuild/freebsd-arm64": "0.27.3", "@esbuild/freebsd-x64": "0.27.3", "@esbuild/linux-arm": "0.27.3", "@esbuild/linux-arm64": "0.27.3", "@esbuild/linux-ia32": "0.27.3", "@esbuild/linux-loong64": "0.27.3", "@esbuild/linux-mips64el": "0.27.3", "@esbuild/linux-ppc64": "0.27.3", "@esbuild/linux-riscv64": "0.27.3", "@esbuild/linux-s390x": "0.27.3", "@esbuild/linux-x64": "0.27.3", "@esbuild/netbsd-arm64": "0.27.3", "@esbuild/netbsd-x64": "0.27.3", "@esbuild/openbsd-arm64": "0.27.3", "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", "@esbuild/win32-arm64": "0.27.3", "@esbuild/win32-ia32": "0.27.3", "@esbuild/win32-x64": "0.27.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg=="], "restore-cursor/onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="], @@ -1676,6 +1703,8 @@ "shadcn/commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="], + "shadcn/open": ["open@11.0.0", "", { "dependencies": { "default-browser": "^5.4.0", "define-lazy-prop": "^3.0.0", "is-in-ssh": "^1.0.0", "is-inside-container": "^1.0.0", "powershell-utils": "^0.1.0", "wsl-utils": "^0.3.0" } }, "sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw=="], + "shadcn/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], "tsx/esbuild": ["esbuild@0.27.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.3", "@esbuild/android-arm": "0.27.3", "@esbuild/android-arm64": "0.27.3", "@esbuild/android-x64": "0.27.3", "@esbuild/darwin-arm64": "0.27.3", "@esbuild/darwin-x64": "0.27.3", "@esbuild/freebsd-arm64": "0.27.3", "@esbuild/freebsd-x64": "0.27.3", "@esbuild/linux-arm": "0.27.3", "@esbuild/linux-arm64": "0.27.3", "@esbuild/linux-ia32": "0.27.3", "@esbuild/linux-loong64": "0.27.3", "@esbuild/linux-mips64el": "0.27.3", "@esbuild/linux-ppc64": "0.27.3", "@esbuild/linux-riscv64": "0.27.3", "@esbuild/linux-s390x": "0.27.3", "@esbuild/linux-x64": "0.27.3", "@esbuild/netbsd-arm64": "0.27.3", "@esbuild/netbsd-x64": "0.27.3", "@esbuild/openbsd-arm64": "0.27.3", "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", "@esbuild/win32-arm64": "0.27.3", "@esbuild/win32-ia32": "0.27.3", "@esbuild/win32-x64": "0.27.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg=="], @@ -1756,12 +1785,18 @@ "@esbuild-kit/core-utils/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.18.20", "", { "os": "win32", "cpu": "x64" }, "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="], + "@prisma/config/c12/dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], + + "@prisma/config/c12/jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], + + "@prisma/config/c12/perfect-debounce": ["perfect-debounce@1.0.0", "", {}, "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA=="], + "@types/cors/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - "@types/nodemailer/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - "accepts/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + "c12/chokidar/readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="], + "cliui/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], "cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], @@ -1828,6 +1863,8 @@ "react-email/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.3", "", { "os": "win32", "cpu": "x64" }, "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA=="], + "shadcn/open/wsl-utils": ["wsl-utils@0.3.1", "", { "dependencies": { "is-wsl": "^3.1.0", "powershell-utils": "^0.1.0" } }, "sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg=="], + "tsx/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.3", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg=="], "tsx/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.27.3", "", { "os": "android", "cpu": "arm" }, "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA=="], diff --git a/package.json b/package.json index 7730a65..f5e3490 100644 --- a/package.json +++ b/package.json @@ -30,32 +30,32 @@ "db:update": "bun run db:generate && bun run db:push" }, "dependencies": { - "@better-auth/drizzle-adapter": "^1.5.6", + "@better-auth/drizzle-adapter": "^1.6.0", "@hookform/resolvers": "^5.2.2", "@nanostores/react": "^1.1.0", - "@react-email/components": "1.0.10", - "better-auth": "^1.5.6", - "bullmq": "^5.71.1", + "@react-email/components": "1.0.11", + "better-auth": "^1.6.0", + "bullmq": "^5.73.1", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "debug": "^4.4.3", - "dexie": "^4.4.1", + "dexie": "^4.4.2", "dexie-react-hooks": "^4.4.0", - "dotenv": "^17.3.1", + "dotenv": "^17.4.1", "drizzle-orm": "^0.45.2", "framer-motion": "^12.38.0", "ioredis": "^5.10.1", "lucide-react": "^1.7.0", "minio": "^8.0.7", "nanostores": "^1.2.0", - "next": "16.2.1", + "next": "16.2.2", "next-themes": "^0.4.6", - "nodemailer": "^8.0.4", + "nodemailer": "^8.0.5", "pg": "^8.20.0", "radix-ui": "^1.4.3", "react": "19.2.4", "react-dom": "19.2.4", - "react-hook-form": "^7.72.0", + "react-hook-form": "^7.72.1", "socket.io": "^4.8.3", "socket.io-client": "^4.8.3", "sonner": "^2.0.7", @@ -68,16 +68,17 @@ "@tailwindcss/postcss": "^4.2.2", "@types/bun": "^1.3.11", "@types/debug": "^4.1.13", - "@types/node": "^25.5.0", - "@types/nodemailer": "^7.0.11", + "@types/node": "^25.5.2", + "@types/nodemailer": "^8.0.0", "@types/pg": "^8.20.0", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", + "auth": "^1.6.0", "babel-plugin-react-compiler": "1.0.0", "cross-env": "^10.1.0", "drizzle-kit": "^0.31.10", "react-email": "5.2.10", - "shadcn": "^4.1.1", + "shadcn": "^4.2.0", "tailwindcss": "^4.2.2", "tsx": "^4.21.0", "tw-animate-css": "^1.4.0", diff --git a/playwright.config.ts b/playwright.config.ts index b642bc9..af7d832 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -17,7 +17,7 @@ export default defineConfig({ }, webServer: { command: 'cross-env NODE_ENV=test tsx src/server.ts', - url: 'http://localhost:3000', + url: process.env.BETTER_AUTH_URL, reuseExistingServer: !process.env.CI, }, projects: [ diff --git a/src/app/PostTestForm.tsx b/src/app/PostTestForm.tsx index 9965c2d..d58dbe5 100644 --- a/src/app/PostTestForm.tsx +++ b/src/app/PostTestForm.tsx @@ -34,23 +34,29 @@ export function PostTestForm() { } }; - const body = JSON.stringify({ - method: "REGISTER", + const registerDiscoverPayload = { + method: "REGISTER" as const, url: process.env.BETTER_AUTH_URL!, publicKey: process.env.FEDERATION_PUBLIC_KEY!, encryptionPublicKey: process.env.FEDERATION_ENCRYPTION_PUBLIC_KEY!, - }); + }; - async function forceDiscover(url: string) { - console.log("body", body); - const response = await fetch(`${url}/discover`, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: body, - }); - return response.json(); + async function forceDiscover(peerBaseUrl: string) { + setStatus("Relaying discover…"); + try { + const response = await fetch("/api/dev/relay-discover", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + target: peerBaseUrl, + payload: registerDiscoverPayload, + }), + }); + const data = await response.json(); + setStatus(`${response.status} ${response.statusText}\n${JSON.stringify(data, null, 2)}`); + } catch (err) { + setStatus(`Error: ${err instanceof Error ? err.message : String(err)}`); + } } return ( diff --git a/src/app/proxy/route.ts b/src/app/proxy/route.ts index b87cc78..bb33bf3 100644 --- a/src/app/proxy/route.ts +++ b/src/app/proxy/route.ts @@ -348,6 +348,7 @@ export async function POST(request: NextRequest) { createdAt: new Date(), followerServerUrl: peerRegistryUrlOrNull(senderUrl), followingServerUrl: peerRegistryUrlOrNull(targetUrl), + acknowledged: true, }).returning(); const row = following[0]; @@ -360,6 +361,7 @@ export async function POST(request: NextRequest) { followingId: row.followingId, accepted: row.accepted, followerServerUrl: row.followerServerUrl, + acknowledged: row.acknowledged }, federationUrl: senderUrl, method: "FEDERATE" as const, diff --git a/src/lib/auth.ts b/src/lib/auth.ts index 162ff42..dc6f439 100644 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -2,7 +2,6 @@ import { federation } from "@/plugins/server/federation"; import { sipherSocial } from '@/plugins/server/social'; import { drizzleAdapter } from "@better-auth/drizzle-adapter"; import { betterAuth } from "better-auth"; -import { createAuthMiddleware } from "better-auth/api"; import { bearer, haveIBeenPwned, openAPI, testUtils, twoFactor, username } from "better-auth/plugins"; import db from "./db"; import * as schema from "./db/schema"; @@ -65,20 +64,6 @@ const bAuth = betterAuth({ await getRedisClient().del(key); } }, - hooks: { - after: createAuthMiddleware(async (context) => { - if (!context.path) return; - const path = context.path; - - switch (true) { - case path.startsWith("/sign-up"): - // key generation logic - break; - default: - break; - } - }) - }, plugins: [ username(), twoFactor(), diff --git a/src/lib/bull/connection.ts b/src/lib/bull/connection.ts new file mode 100644 index 0000000..1dd42d4 --- /dev/null +++ b/src/lib/bull/connection.ts @@ -0,0 +1,10 @@ +import Redis from 'ioredis'; + +let _redis: Redis | null = null; + +export function getRedisConnection(): Redis { + if (!_redis) { + _redis = new Redis(process.env.REDIS_URL!, { maxRetriesPerRequest: null }); + } + return _redis; +} diff --git a/src/lib/bull/index.ts b/src/lib/bull/index.ts index 93489cd..2f0623d 100644 --- a/src/lib/bull/index.ts +++ b/src/lib/bull/index.ts @@ -1,373 +1,4 @@ -import db from '@/lib/db'; -import { blacklistedServers, deliveryJobs, follows, serverRegistry } from '@/lib/db/schema'; -import { FederationError, federationFetch, type FederationErrorCode } from '@/lib/federation/fetch'; -import { encryptPayload, getOwnSigningSecretKey, signMessage, verifySignature } from '@/lib/federation/keytools'; -import { discoverAndRegister, DiscoveryError, markServerHealthy } from '@/lib/federation/registry'; -import { getThreatPolicy } from '@/lib/federation/threat-model'; -import { Queue, UnrecoverableError, Worker, type Job } from 'bullmq'; -import createDebug from 'debug'; -import { and, eq } from 'drizzle-orm'; -import Redis from 'ioredis'; -import z from 'zod'; -import { FollowEnvelopeSchema } from '../zod/methods/FollowSchema'; +export { getFederationQueue, getHealthCheckQueue, scheduleHealthCheck } from './queues'; +export type { FederationDeliveryJob, HealthCheckJob } from './queues'; +export { startFederationWorker } from './worker'; -const debug = createDebug('app:federation:worker'); - -// --------------------------------------------------------------------------- -// Shared Redis -// --------------------------------------------------------------------------- - -function createRedisConnection() { - return new Redis(process.env.REDIS_URL!, { maxRetriesPerRequest: null }); -} - -// --------------------------------------------------------------------------- -// Federation delivery queue (existing) -// --------------------------------------------------------------------------- - -export interface FederationDeliveryJob { - deliveryJobId: string; - targetUrl: string; - serverUrl: string; - payload: string; -} - -const DELIVERY_QUEUE_NAME = 'federation-delivery'; - -let _deliveryQueue: Queue | null = null; - -export function getFederationQueue(): Queue { - if (!_deliveryQueue) { - _deliveryQueue = new Queue(DELIVERY_QUEUE_NAME, { - connection: createRedisConnection() as never, - defaultJobOptions: { - attempts: 5, - backoff: { - type: 'exponential', - delay: 5_000, - }, - removeOnComplete: { age: 60 * 60 * 24 }, - removeOnFail: { age: 60 * 60 * 24 * 7 }, - }, - }); - } - return _deliveryQueue; -} - -// --------------------------------------------------------------------------- -// Health-check queue -// --------------------------------------------------------------------------- - -export interface HealthCheckJob { - serverUrl: string; -} - -const HEALTH_CHECK_QUEUE_NAME = 'federation-health-check'; - -let _healthCheckQueue: Queue | null = null; - -export function getHealthCheckQueue(): Queue { - if (!_healthCheckQueue) { - _healthCheckQueue = new Queue(HEALTH_CHECK_QUEUE_NAME, { - connection: createRedisConnection() as never, - }); - } - return _healthCheckQueue; -} - -export async function scheduleHealthCheck(serverUrl: string, attempt: number): Promise { - const delayMinutes = 5 + (attempt * 10); - const delayMs = delayMinutes * 60 * 1000; - debug('scheduling health check for %s in %d minutes (attempt %d)', serverUrl, delayMinutes, attempt); - - const safeId = serverUrl.replace(/[^a-zA-Z0-9._-]/g, '_'); - await getHealthCheckQueue().add('health-check', { serverUrl }, { - delay: delayMs, - jobId: `health-check_${safeId}_${attempt}`, - removeOnComplete: true, - removeOnFail: true, - }); -} - -// --------------------------------------------------------------------------- -// Delivery worker processor -// --------------------------------------------------------------------------- - -async function processFederationDelivery(job: Job) { - const { deliveryJobId, targetUrl, serverUrl, payload } = job.data; - debug('processing job %s (%s) → %s (attempt %d)', job.id, job.name, targetUrl, job.attemptsMade + 1); - - const [blacklisted] = await db - .select({ id: blacklistedServers.id }) - .from(blacklistedServers) - .where(eq(blacklistedServers.serverUrl, serverUrl)) - .limit(1); - - if (blacklisted) { - debug('server %s is blacklisted, dropping job %s', serverUrl, job.id); - await db.delete(deliveryJobs).where(eq(deliveryJobs.id, deliveryJobId)); - throw new UnrecoverableError(`Server ${serverUrl} is blacklisted, skipping delivery`); - } - - let encryptionPublicKey: string; - - const [server] = await db - .select({ encryptionPublicKey: serverRegistry.encryptionPublicKey, publicKey: serverRegistry.publicKey }) - .from(serverRegistry) - .where(eq(serverRegistry.url, serverUrl)) - .limit(1); - - if (server) { - encryptionPublicKey = server.encryptionPublicKey; - } else { - debug('server %s not in registry, attempting auto-discovery', serverUrl); - try { - encryptionPublicKey = await discoverAndRegister(serverUrl); - } catch (err) { - if (err instanceof DiscoveryError) { - debug('auto-discovery of %s failed: %s', serverUrl, err.message); - throw new Error(`Auto-discovery of ${serverUrl} failed: ${err.message}`); - } - throw err; - } - } - - debug('encrypting payload for %s (key: %s…)', serverUrl, encryptionPublicKey.slice(0, 8)); - const recipientKey = new Uint8Array(Buffer.from(encryptionPublicKey, 'base64')); - const encrypted = encryptPayload(payload, recipientKey); - - await db.update(deliveryJobs).set({ - lastAttemptedAt: new Date(), - attempts: job.attemptsMade + 1, - }).where(eq(deliveryJobs.id, deliveryJobId)); - - debug('sending encrypted payload to %s', targetUrl); - - const method = JSON.parse(payload).method; - if (!method || !["FEDERATE", "FEDERATE_POST", "INSERT", "UNFOLLOW"].includes(method)) { - debug('invalid method: %s, dropping job %s', method, job.id); - await db.delete(deliveryJobs).where(eq(deliveryJobs.id, deliveryJobId)); - debug('job %s dropped because of invalid method', job.id); - throw new UnrecoverableError(`Invalid method: ${method}, dropping job ${job.id}`); - } - - const signature = signMessage(payload, getOwnSigningSecretKey()); - - const { response } = await federationFetch(targetUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Origin': process.env.BETTER_AUTH_URL!, - 'X-Federation-Origin': process.env.BETTER_AUTH_URL!, - 'X-Federation-Target': targetUrl, - }, - body: JSON.stringify({ method, payload: encrypted, signature }), - timeout: 15_000, - proxyFallback: true, - serverUrl, - }); - - if (!response.ok) { - debug('delivery to %s failed with status %d', targetUrl, response.status); - throw new Error(`Federation delivery to ${targetUrl} failed: ${response.status}`); - } - - const responseBody = await response.json(); - debug('delivery to %s response body: %o', targetUrl, responseBody); - debug('responseBody.payload: %s', responseBody.payload); - - const ackPayload = - responseBody.payload?.method === "PROXY_RESPONSE" - ? responseBody.payload - : responseBody.method === "PROXY_RESPONSE" - ? responseBody - : null; - - if (!ackPayload || ackPayload.method !== "PROXY_RESPONSE") { - debug('delivery to %s not acknowledged', targetUrl); - throw new UnrecoverableError(`Federation delivery to ${targetUrl} failed: ${response.status} - ${JSON.stringify(responseBody)}`); - } - - if (job.name === 'deliver-follow') { - let followPayload: z.infer; - debug('delivery to %s is a follow, updating follow', targetUrl); - debug('ackPayload: %o', ackPayload); - - if (ackPayload.method === "PROXY_RESPONSE") { - // Decrypt the payload - const decrypted = FollowEnvelopeSchema.safeParse(ackPayload.data) - if (!decrypted.success) { - debug('failed to parse follow payload: %s', ackPayload.data); - await db.delete(deliveryJobs).where(eq(deliveryJobs.id, deliveryJobId)); - throw new UnrecoverableError(`Failed to parse follow payload, dropping job ${job.id}`); - } - - debug("payload data: %o", decrypted.data); - // Decrypt the signature - const signature = verifySignature(decrypted.data._raw, ackPayload.signature, new Uint8Array(Buffer.from(server.publicKey!, 'base64'))); - - if (!signature) { - debug('signature verification failed, dropping job %s', job.id); - await db.delete(deliveryJobs).where(eq(deliveryJobs.id, deliveryJobId)); - throw new UnrecoverableError(`Signature verification failed, dropping job ${job.id}`); - } - - followPayload = decrypted.data as z.infer; - } else { - const validated = FollowEnvelopeSchema.safeParse(ackPayload); - if (!validated.success) { - debug('failed to parse follow payload: %s', ackPayload); - await db.delete(deliveryJobs).where(eq(deliveryJobs.id, deliveryJobId)); - throw new UnrecoverableError(`Failed to parse follow payload, dropping job ${job.id}`); - } - - followPayload = validated.data as z.infer; - } - - const followData = followPayload.following; - if (followData && followData.accepted) { - await db.update(follows).set({ accepted: followData.accepted }) - .where( - and( - eq(follows.followerId, followData.followerId), - eq(follows.followingId, followData.followingId), - eq(follows.followerServerUrl, serverUrl), - ) - ); - debug('updated follow %s accepted=%s', followData.id, followData.accepted); - } - } - - debug('job %s delivered successfully to %s', job.id, targetUrl); -} - -// --------------------------------------------------------------------------- -// Health-check worker processor -// --------------------------------------------------------------------------- - -const MAX_HEALTH_CHECK_ATTEMPTS = 5; - -async function processHealthCheck(job: Job) { - const { serverUrl } = job.data; - - const [server] = await db.select() - .from(serverRegistry) - .where(eq(serverRegistry.url, serverUrl)) - .limit(1); - - if (!server) { - debug('health-check: server %s not found in registry, skipping', serverUrl); - return; - } - - if (server.isHealthy) { - debug('health-check: server %s is already healthy, skipping', serverUrl); - return; - } - - if (server.unhealthyReason) { - const policy = getThreatPolicy(server.unhealthyReason as FederationErrorCode); - if (!policy.directHealthCheckable) { - debug('health-check: server %s has reason %s (not direct-checkable), skipping', serverUrl, server.unhealthyReason); - return; - } - } - - debug('health-check: pinging %s (attempt %d/%d)', serverUrl, server.healthCheckAttempts + 1, MAX_HEALTH_CHECK_ATTEMPTS); - - try { - const { response } = await federationFetch(serverUrl + '/discover', { - serverUrl, - timeout: 8_000, - skipHealthUpdate: true, - }); - - if (response.ok) { - debug('health-check: %s is reachable, marking healthy', serverUrl); - await markServerHealthy(serverUrl); - return; - } - - debug('health-check: %s returned HTTP %d', serverUrl, response.status); - } catch (err) { - debug('health-check: %s failed: %s', serverUrl, err instanceof FederationError ? err.code : err); - } - - const nextAttempt = server.healthCheckAttempts + 1; - await db.update(serverRegistry).set({ - healthCheckAttempts: nextAttempt, - updatedAt: new Date(), - }).where(eq(serverRegistry.url, serverUrl)); - - if (nextAttempt < MAX_HEALTH_CHECK_ATTEMPTS) { - await scheduleHealthCheck(serverUrl, nextAttempt); - } else { - debug('health-check: %s exhausted all %d attempts, stopping', serverUrl, MAX_HEALTH_CHECK_ATTEMPTS); - console.warn(`[federation] health-check exhausted for ${serverUrl} after ${MAX_HEALTH_CHECK_ATTEMPTS} attempts`); - } -} - -// --------------------------------------------------------------------------- -// Worker startup -// --------------------------------------------------------------------------- - -export function startFederationWorker() { - createDebug.enable(process.env.DEBUG || ''); - console.log('[federation] Starting workers...'); - - const deliveryWorker = new Worker( - DELIVERY_QUEUE_NAME, - processFederationDelivery, - { - connection: createRedisConnection() as never, - concurrency: 10, - }, - ); - - deliveryWorker.on('ready', () => { - console.log('[federation] Delivery worker connected to Redis and ready'); - }); - - deliveryWorker.on('failed', (job, err) => { - const retriesLeft = (job?.opts.attempts ?? 0) - (job?.attemptsMade ?? 0); - debug('delivery job %s (%s) to %s failed (attempt %d, %d retries left): %s', job?.id, job?.name, job?.data.targetUrl, job?.attemptsMade, retriesLeft, err.message); - if (err.cause) debug('cause: %O', err.cause); - }); - - deliveryWorker.on('completed', async (job) => { - debug('delivery job %s (%s) completed, cleaning up delivery record %s', job.id, job.name, job.data.deliveryJobId); - try { - await db.delete(deliveryJobs).where(eq(deliveryJobs.id, job.data.deliveryJobId)); - } catch (err) { - debug('failed to clean up delivery job %s: %O', job.data.deliveryJobId, err); - } - }); - - deliveryWorker.on('error', (err) => { - console.error('[federation] Delivery worker error:', err); - }); - - const healthCheckWorker = new Worker( - HEALTH_CHECK_QUEUE_NAME, - processHealthCheck, - { - connection: createRedisConnection() as never, - concurrency: 3, - }, - ); - - healthCheckWorker.on('ready', () => { - console.log('[federation] Health-check worker connected to Redis and ready'); - }); - - healthCheckWorker.on('failed', (job, err) => { - debug('health-check job %s failed: %s', job?.id, err.message); - }); - - healthCheckWorker.on('error', (err) => { - console.error('[federation] Health-check worker error:', err); - }); - - debug('all workers started'); - return { deliveryWorker, healthCheckWorker }; -} diff --git a/src/lib/bull/processors/delivery.ts b/src/lib/bull/processors/delivery.ts new file mode 100644 index 0000000..c2d8fce --- /dev/null +++ b/src/lib/bull/processors/delivery.ts @@ -0,0 +1,160 @@ +import db from '@/lib/db'; +import { blacklistedServers, deliveryJobs, serverRegistry } from '@/lib/db/schema'; +import { federationFetch } from '@/lib/federation/fetch'; +import { encryptPayload, getOwnSigningSecretKey, signMessage } from '@/lib/federation/keytools'; +import { discoverAndRegister, DiscoveryError } from '@/lib/federation/registry'; +import type { FederationDeliveryJob } from '../queues'; +import { handleFollowAck } from './handlers/follow'; +import { UnrecoverableError, type Job } from 'bullmq'; +import createDebug from 'debug'; +import { eq } from 'drizzle-orm'; + +const debug = createDebug('app:federation:worker'); + +const ALLOWED_METHODS = new Set(['FEDERATE', 'FEDERATE_POST', 'INSERT', 'UNFOLLOW']); + +// --------------------------------------------------------------------------- +// Ack handlers keyed by job name +// --------------------------------------------------------------------------- + +type AckPayload = { method: 'PROXY_RESPONSE'; data: unknown; signature: string }; + +type AckHandler = ( + ackPayload: AckPayload, + serverUrl: string, + serverPublicKey: string | undefined, + deliveryJobId: string, + jobId: string | undefined, +) => Promise; + +const ackHandlers: Record = { + 'deliver-follow': handleFollowAck, +}; + +// --------------------------------------------------------------------------- +// Main processor +// --------------------------------------------------------------------------- + +export async function processFederationDelivery(job: Job): Promise { + const { deliveryJobId, targetUrl, serverUrl, payload } = job.data; + debug('processing job %s (%s) → %s (attempt %d)', job.id, job.name, targetUrl, job.attemptsMade + 1); + + // 1. Validate method early — before any I/O. + let method: string; + try { + method = JSON.parse(payload).method; + } catch { + await db.delete(deliveryJobs).where(eq(deliveryJobs.id, deliveryJobId)); + throw new UnrecoverableError(`Malformed payload JSON, dropping job ${job.id}`); + } + + if (!method || !ALLOWED_METHODS.has(method)) { + debug('invalid method: %s, dropping job %s', method, job.id); + await db.delete(deliveryJobs).where(eq(deliveryJobs.id, deliveryJobId)); + throw new UnrecoverableError(`Invalid method: ${method}, dropping job ${job.id}`); + } + + // 2. Blacklist check. + const [blacklisted] = await db + .select({ id: blacklistedServers.id }) + .from(blacklistedServers) + .where(eq(blacklistedServers.serverUrl, serverUrl)) + .limit(1); + + if (blacklisted) { + debug('server %s is blacklisted, dropping job %s', serverUrl, job.id); + await db.delete(deliveryJobs).where(eq(deliveryJobs.id, deliveryJobId)); + throw new UnrecoverableError(`Server ${serverUrl} is blacklisted, skipping delivery`); + } + + // 3. Resolve encryption key (and keep the full server row for later). + let encryptionPublicKey: string; + let serverPublicKey: string | undefined; + + const [server] = await db + .select({ + encryptionPublicKey: serverRegistry.encryptionPublicKey, + publicKey: serverRegistry.publicKey, + }) + .from(serverRegistry) + .where(eq(serverRegistry.url, serverUrl)) + .limit(1); + + if (server) { + encryptionPublicKey = server.encryptionPublicKey; + serverPublicKey = server.publicKey; + } else { + debug('server %s not in registry, attempting auto-discovery', serverUrl); + try { + encryptionPublicKey = await discoverAndRegister(serverUrl); + } catch (err) { + if (err instanceof DiscoveryError) { + debug('auto-discovery of %s failed: %s', serverUrl, err.message); + throw new Error(`Auto-discovery of ${serverUrl} failed: ${err.message}`); + } + throw err; + } + // serverPublicKey stays undefined; follow handler will re-fetch it. + } + + // 4. Encrypt payload and record the attempt. + debug('encrypting payload for %s (key: %s…)', serverUrl, encryptionPublicKey.slice(0, 8)); + const recipientKey = new Uint8Array(Buffer.from(encryptionPublicKey, 'base64')); + const encrypted = encryptPayload(payload, recipientKey); + + await db.update(deliveryJobs).set({ + lastAttemptedAt: new Date(), + attempts: job.attemptsMade + 1, + }).where(eq(deliveryJobs.id, deliveryJobId)); + + // 5. Send. + debug('sending encrypted payload to %s', targetUrl); + const signature = signMessage(payload, getOwnSigningSecretKey()); + + const { response } = await federationFetch(targetUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Origin': process.env.BETTER_AUTH_URL!, + 'X-Federation-Origin': process.env.BETTER_AUTH_URL!, + 'X-Federation-Target': targetUrl, + }, + body: JSON.stringify({ method, payload: encrypted, signature }), + timeout: 15_000, + proxyFallback: true, + serverUrl, + }); + + if (!response.ok) { + debug('delivery to %s failed with status %d', targetUrl, response.status); + throw new Error(`Federation delivery to ${targetUrl} failed: ${response.status}`); + } + + // 6. Parse ack. + const responseBody = await response.json(); + debug('delivery to %s response body: %o', targetUrl, responseBody); + + const ackPayload: AckPayload | null = + responseBody.payload?.method === 'PROXY_RESPONSE' + ? responseBody.payload + : responseBody.method === 'PROXY_RESPONSE' + ? responseBody + : null; + + if (!ackPayload) { + debug('delivery to %s not acknowledged', targetUrl); + throw new UnrecoverableError( + `Federation delivery to ${targetUrl} not acknowledged: ${JSON.stringify(responseBody)}`, + ); + } + + // 7. Dispatch to job-specific ack handler (if any). + const handleAck = ackHandlers[job.name]; + if (handleAck) { + await handleAck(ackPayload, serverUrl, serverPublicKey, deliveryJobId, job.id); + } else { + debug('job %s has no ack handler, skipping ack processing', job.name); + } + + debug('job %s delivered successfully to %s', job.id, targetUrl); +} diff --git a/src/lib/bull/processors/handlers/follow.ts b/src/lib/bull/processors/handlers/follow.ts new file mode 100644 index 0000000..e0621f3 --- /dev/null +++ b/src/lib/bull/processors/handlers/follow.ts @@ -0,0 +1,126 @@ +import db from '@/lib/db'; +import { deliveryJobs, follows, serverRegistry } from '@/lib/db/schema'; +import { verifySignature } from '@/lib/federation/keytools'; +import { FollowEnvelopeSchema } from '@/lib/zod/methods/FollowSchema'; +import { UnrecoverableError } from 'bullmq'; +import createDebug from 'debug'; +import { and, eq } from 'drizzle-orm'; + +const debug = createDebug('app:federation:worker'); + +interface AckPayload { + method: 'PROXY_RESPONSE'; + data: unknown; + signature: string; +} + +/** + * Resolves the signing public key for a server. If the server was not in the + * registry at delivery time (auto-discovered), we re-fetch it now that + * discoverAndRegister has run and inserted the row. + */ +async function resolveServerPublicKey( + serverUrl: string, + cachedPublicKey: string | undefined, +): Promise { + if (cachedPublicKey) return cachedPublicKey; + + const [fresh] = await db + .select({ publicKey: serverRegistry.publicKey }) + .from(serverRegistry) + .where(eq(serverRegistry.url, serverUrl)) + .limit(1); + + if (!fresh?.publicKey) { + throw new UnrecoverableError( + `Cannot verify follow ack from ${serverUrl}: server has no signing public key in registry`, + ); + } + + return fresh.publicKey; +} + +export async function handleFollowAck( + ackPayload: AckPayload, + serverUrl: string, + cachedServerPublicKey: string | undefined, + deliveryJobId: string, + jobId: string | undefined, +): Promise { + debug('handling follow ack from %s', serverUrl); + debug('ackPayload: %o', ackPayload); + + const decrypted = FollowEnvelopeSchema.safeParse(ackPayload.data); + if (!decrypted.success) { + debug('failed to parse follow payload: %s', ackPayload.data); + await db.delete(deliveryJobs).where(eq(deliveryJobs.id, deliveryJobId)); + throw new UnrecoverableError(`Failed to parse follow payload, dropping job ${jobId}`); + } + + debug('payload data: %o', decrypted.data); + + const publicKey = await resolveServerPublicKey(serverUrl, cachedServerPublicKey); + const signatureValid = verifySignature( + decrypted.data._raw, + ackPayload.signature, + new Uint8Array(Buffer.from(publicKey, 'base64')), + ); + + if (!signatureValid) { + debug('signature verification failed, dropping job %s', jobId); + await db.delete(deliveryJobs).where(eq(deliveryJobs.id, deliveryJobId)); + throw new UnrecoverableError(`Signature verification failed, dropping job ${jobId}`); + } + + const followData = decrypted.data.following; + + + // Verify the row exists locally before applying the remote's accepted flag. + const [existing] = await db + .select({ id: follows.id }) + .from(follows) + .where( + and( + eq(follows.followerId, followData.followerId), + eq(follows.followingId, followData.followingId), + eq(follows.followerServerUrl, serverUrl), + ), + ) + .limit(1); + + if (!existing) { + debug( + 'follow ack references unknown follow (%s → %s from %s), ignoring', + followData.followerId, + followData.followingId, + serverUrl, + ); + return; + } + + if (!followData?.accepted) { + debug('follow %s is not accepted but was acknowledged, setting acknowledged to true', followData.id); + await db.update(follows).set({ acknowledged: true }).where( + and( + eq(follows.followerId, followData.followerId), + eq(follows.followingId, followData.followingId), + eq(follows.followerServerUrl, serverUrl), + ), + ); + debug('follow %s acknowledged', existing.id); + return; + } + + await db + .update(follows) + .set({ accepted: followData.accepted }) + .where( + and( + eq(follows.followerId, followData.followerId), + eq(follows.followingId, followData.followingId), + eq(follows.followerServerUrl, serverUrl), + ), + ); + + debug('updated follow %s accepted=%s', followData.id, followData.accepted); +} diff --git a/src/lib/bull/processors/health-check.ts b/src/lib/bull/processors/health-check.ts new file mode 100644 index 0000000..d1fb614 --- /dev/null +++ b/src/lib/bull/processors/health-check.ts @@ -0,0 +1,75 @@ +// This is to be actually tested, but I'm not sure how to do it without testing it at the battlefield. + +import db from '@/lib/db'; +import { serverRegistry } from '@/lib/db/schema'; +import { FederationError, federationFetch, type FederationErrorCode } from '@/lib/federation/fetch'; +import { markServerHealthy } from '@/lib/federation/registry'; +import { getThreatPolicy } from '@/lib/federation/threat-model'; +import type { Job } from 'bullmq'; +import createDebug from 'debug'; +import { eq } from 'drizzle-orm'; +import { scheduleHealthCheck, type HealthCheckJob } from '../queues'; + +const debug = createDebug('app:federation:worker'); + +const MAX_HEALTH_CHECK_ATTEMPTS = 5; + +export async function processHealthCheck(job: Job): Promise { + const { serverUrl } = job.data; + + const [server] = await db.select() + .from(serverRegistry) + .where(eq(serverRegistry.url, serverUrl)) + .limit(1); + + if (!server) { + debug('health-check: server %s not found in registry, skipping', serverUrl); + return; + } + + if (server.isHealthy) { + debug('health-check: server %s is already healthy, skipping', serverUrl); + return; + } + + if (server.unhealthyReason) { + const policy = getThreatPolicy(server.unhealthyReason as FederationErrorCode); + if (!policy.directHealthCheckable) { + debug('health-check: server %s has reason %s (not direct-checkable), skipping', serverUrl, server.unhealthyReason); + return; + } + } + + debug('health-check: pinging %s (attempt %d/%d)', serverUrl, server.healthCheckAttempts + 1, MAX_HEALTH_CHECK_ATTEMPTS); + + try { + const { response } = await federationFetch(serverUrl + '/discover', { + serverUrl, + timeout: 8_000, + skipHealthUpdate: true, + }); + + if (response.ok) { + debug('health-check: %s is reachable, marking healthy', serverUrl); + await markServerHealthy(serverUrl); + return; + } + + debug('health-check: %s returned HTTP %d', serverUrl, response.status); + } catch (err) { + debug('health-check: %s failed: %s', serverUrl, err instanceof FederationError ? err.code : err); + } + + const nextAttempt = server.healthCheckAttempts + 1; + await db.update(serverRegistry).set({ + healthCheckAttempts: nextAttempt, + updatedAt: new Date(), + }).where(eq(serverRegistry.url, serverUrl)); + + if (nextAttempt < MAX_HEALTH_CHECK_ATTEMPTS) { + await scheduleHealthCheck(serverUrl, nextAttempt); + } else { + debug('health-check: %s exhausted all %d attempts, stopping', serverUrl, MAX_HEALTH_CHECK_ATTEMPTS); + console.warn(`[federation] health-check exhausted for ${serverUrl} after ${MAX_HEALTH_CHECK_ATTEMPTS} attempts`); + } +} diff --git a/src/lib/bull/queues.ts b/src/lib/bull/queues.ts new file mode 100644 index 0000000..9e23bb2 --- /dev/null +++ b/src/lib/bull/queues.ts @@ -0,0 +1,73 @@ +import { Queue } from 'bullmq'; +import createDebug from 'debug'; +import { getRedisConnection } from './connection'; + +const debug = createDebug('app:federation:worker'); + +// --------------------------------------------------------------------------- +// Federation delivery queue +// --------------------------------------------------------------------------- + +export interface FederationDeliveryJob { + deliveryJobId: string; + targetUrl: string; + serverUrl: string; + payload: string; +} + +export const DELIVERY_QUEUE_NAME = 'federation-delivery'; + +let _deliveryQueue: Queue | null = null; + +export function getFederationQueue(): Queue { + if (!_deliveryQueue) { + _deliveryQueue = new Queue(DELIVERY_QUEUE_NAME, { + connection: getRedisConnection() as never, + defaultJobOptions: { + attempts: 5, + backoff: { + type: 'exponential', + delay: 5_000, + }, + removeOnComplete: { age: 60 * 60 * 24 }, + removeOnFail: { age: 60 * 60 * 24 * 7 }, + }, + }); + } + return _deliveryQueue; +} + +// --------------------------------------------------------------------------- +// Health-check queue +// --------------------------------------------------------------------------- + +export interface HealthCheckJob { + serverUrl: string; +} + +export const HEALTH_CHECK_QUEUE_NAME = 'federation-health-check'; + +let _healthCheckQueue: Queue | null = null; + +export function getHealthCheckQueue(): Queue { + if (!_healthCheckQueue) { + _healthCheckQueue = new Queue(HEALTH_CHECK_QUEUE_NAME, { + connection: getRedisConnection() as never, + }); + } + return _healthCheckQueue; +} + +export async function scheduleHealthCheck(serverUrl: string, attempt: number): Promise { + const delayMinutes = 5 + (attempt * 10); + const delayMs = delayMinutes * 60 * 1000; + debug('scheduling health check for %s in %d minutes (attempt %d)', serverUrl, delayMinutes, attempt); + + const safeId = serverUrl.replace(/[^a-zA-Z0-9._-]/g, '_'); + await getHealthCheckQueue().add('health-check', { serverUrl }, { + delay: delayMs, + jobId: `health-check_${safeId}_${attempt}`, + removeOnComplete: true, + removeOnFail: true, + }); +} diff --git a/src/lib/bull/worker.ts b/src/lib/bull/worker.ts new file mode 100644 index 0000000..7d12a7f --- /dev/null +++ b/src/lib/bull/worker.ts @@ -0,0 +1,89 @@ +import db from '@/lib/db'; +import { deliveryJobs } from '@/lib/db/schema'; +import { Worker } from 'bullmq'; +import createDebug from 'debug'; +import { eq } from 'drizzle-orm'; +import { getRedisConnection } from './connection'; +import { processFederationDelivery } from './processors/delivery'; +import { processHealthCheck } from './processors/health-check'; +import { DELIVERY_QUEUE_NAME, HEALTH_CHECK_QUEUE_NAME, type FederationDeliveryJob, type HealthCheckJob } from './queues'; + +const debug = createDebug('app:federation:worker'); + +interface WorkerHandles { + deliveryWorker: Worker; + healthCheckWorker: Worker; +} + +let _workers: WorkerHandles | null = null; + +export function startFederationWorker(): WorkerHandles { + if (_workers) { + debug('workers already running, skipping duplicate startup'); + return _workers; + } + + createDebug.enable(process.env.DEBUG || ''); + console.log('[federation] Starting workers...'); + + const deliveryWorker = new Worker( + DELIVERY_QUEUE_NAME, + processFederationDelivery, + { + connection: getRedisConnection() as never, + concurrency: 10, + }, + ); + + deliveryWorker.on('ready', () => { + console.log('[federation] Delivery worker connected to Redis and ready'); + }); + + deliveryWorker.on('failed', (job, err) => { + const retriesLeft = (job?.opts.attempts ?? 0) - (job?.attemptsMade ?? 0); + debug( + 'delivery job %s (%s) to %s failed (attempt %d, %d retries left): %s', + job?.id, job?.name, job?.data.targetUrl, job?.attemptsMade, retriesLeft, err.message, + ); + if (err.cause) debug('cause: %O', err.cause); + }); + + deliveryWorker.on('completed', async (job) => { + debug('delivery job %s (%s) completed, cleaning up delivery record %s', job.id, job.name, job.data.deliveryJobId); + try { + await db.delete(deliveryJobs).where(eq(deliveryJobs.id, job.data.deliveryJobId)); + } catch (err) { + debug('failed to clean up delivery job %s: %O', job.data.deliveryJobId, err); + } + }); + + deliveryWorker.on('error', (err) => { + console.error('[federation] Delivery worker error:', err); + }); + + const healthCheckWorker = new Worker( + HEALTH_CHECK_QUEUE_NAME, + processHealthCheck, + { + connection: getRedisConnection() as never, + concurrency: 3, + }, + ); + + healthCheckWorker.on('ready', () => { + console.log('[federation] Health-check worker connected to Redis and ready'); + }); + + healthCheckWorker.on('failed', (job, err) => { + debug('health-check job %s failed: %s', job?.id, err.message); + }); + + healthCheckWorker.on('error', (err) => { + console.error('[federation] Health-check worker error:', err); + }); + + _workers = { deliveryWorker, healthCheckWorker }; + + debug('all workers started'); + return _workers; +} diff --git a/src/lib/db/schema/index.ts b/src/lib/db/schema/index.ts index 4029d7b..68449dd 100644 --- a/src/lib/db/schema/index.ts +++ b/src/lib/db/schema/index.ts @@ -106,6 +106,7 @@ export const follows = pgTable( () => serverRegistry.url, { onDelete: "cascade" }, ), + acknowledged: boolean("acknowledged").default(false).notNull(), }, (table) => [ index("follows_followerServerUrl_idx").on(table.followerServerUrl), diff --git a/src/lib/federation/fetch.ts b/src/lib/federation/fetch.ts index 77fceeb..c20ef62 100644 --- a/src/lib/federation/fetch.ts +++ b/src/lib/federation/fetch.ts @@ -18,6 +18,7 @@ export type FederationErrorCode = | "CONN_RESET" | "TIMEOUT" | "TLS_ERROR" + | "INVALID_RESPONSE_FROM_TARGET" | "UNKNOWN"; export class FederationError extends Error { @@ -243,6 +244,9 @@ async function attemptProxyRoute( }); if (!proxyResponse.ok) { + if (proxyResponse.status === 502) { + throw new FederationError("INVALID_RESPONSE_FROM_TARGET", proxyPeer.url); + } throw new Error(`Proxy ${proxyPeer.url} returned ${proxyResponse.status}`); } diff --git a/src/lib/federation/registry.ts b/src/lib/federation/registry.ts index 308c823..e8a20fa 100644 --- a/src/lib/federation/registry.ts +++ b/src/lib/federation/registry.ts @@ -1,6 +1,6 @@ import db from '@/lib/db'; import { serverRegistry } from '@/lib/db/schema'; -import { federationFetch, FederationError, type FederationErrorCode } from '@/lib/federation/fetch'; +import { FederationError, federationFetch, type FederationErrorCode } from '@/lib/federation/fetch'; import { assertSafeUrl } from '@/lib/federation/url-guard'; import createDebug from 'debug'; import { eq } from 'drizzle-orm'; @@ -112,7 +112,7 @@ export async function discoverAndRegister(serverUrl: string): Promise { debug('sending mutual REGISTER to %s', serverUrl); try { - await federationFetch(serverUrl + '/discover', { + const { response: registerResponse } = await federationFetch(serverUrl + '/discover', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ @@ -123,6 +123,19 @@ export async function discoverAndRegister(serverUrl: string): Promise { }), serverUrl, }); + + // The echo carries the remote server's canonical keys. Use them to + // confirm the registration we inserted from the GET above. + if (!registerResponse.ok) { + throw new DiscoveryError(`Failed to register with ${serverUrl}: ${registerResponse.status}`); + } + + const echoBody = await registerResponse.json().catch(() => null); + const echo = echoBody?.echo as { url?: string; publicKey?: string; encryptionPublicKey?: string } | undefined; + if (echo?.url && echo.publicKey && echo.encryptionPublicKey) { + debug('updating registration for %s from echo', serverUrl); + await upsertServer(echo.url, echo.publicKey, echo.encryptionPublicKey); + } } catch (err) { debug('mutual REGISTER to %s failed (non-fatal): %s', serverUrl, err instanceof Error ? err.message : err); } diff --git a/src/lib/federation/threat-model.ts b/src/lib/federation/threat-model.ts index 909f8b7..891205b 100644 --- a/src/lib/federation/threat-model.ts +++ b/src/lib/federation/threat-model.ts @@ -32,6 +32,11 @@ export const DEFAULT_THREAT_MODEL: Record = { directHealthCheckable: true, description: "TLS failure -- possible MITM, do not proxy", }, + INVALID_RESPONSE_FROM_TARGET: { + proxyEligible: true, + directHealthCheckable: false, + description: "Invalid response from target -- relay-eligible but you should check the data you're trying to relay, it might be invalid.", + }, UNKNOWN: { proxyEligible: true, directHealthCheckable: true, diff --git a/src/lib/federation/url-guard.ts b/src/lib/federation/url-guard.ts index fc0fdfb..592ccde 100644 --- a/src/lib/federation/url-guard.ts +++ b/src/lib/federation/url-guard.ts @@ -1,4 +1,5 @@ import createDebug from "debug"; +import { z } from "zod"; const debug = createDebug("app:federation:url-guard"); @@ -12,7 +13,17 @@ const BLOCKED_HOSTNAMES = new Set([ "169.254.169.254", ]); -const SSRF_BYPASS = process.env.FEDERATION_ALLOW_PRIVATE_URLS === "true"; +/** Normalize allowlist tokens so `host:port` and full URLs map to URL.hostname. */ +function allowlistHostname(entry: string): string | null { + const t = entry.trim(); + if (!t) return null; + try { + if (t.includes("://")) return new URL(t).hostname; + return new URL(`http://${t}`).hostname; + } catch { + return t; + } +} const DEV_ALLOWED_HOSTNAMES = new Set([ "localhost", @@ -21,17 +32,24 @@ const DEV_ALLOWED_HOSTNAMES = new Set([ if (typeof process.env.DEV_ALLOWED_HOSTNAMES === "string" && process.env.DEV_ALLOWED_HOSTNAMES.trim() !== "") { for (const h of process.env.DEV_ALLOWED_HOSTNAMES.split(",")) { - const hostname = h.trim(); + const hostname = allowlistHostname(h); if (hostname) DEV_ALLOWED_HOSTNAMES.add(hostname); } } -debug("SSRF bypass: %s, DEV_ALLOWED_HOSTNAMES: %s", SSRF_BYPASS, [...DEV_ALLOWED_HOSTNAMES].join(", ")); -function isPrivateIPv4(hostname: string): boolean { - const parts = hostname.split(".").map(Number); - if (parts.length !== 4 || parts.some((p) => isNaN(p))) return false; +debug("DEV_ALLOWED_HOSTNAMES: %s", [...DEV_ALLOWED_HOSTNAMES].join(", ")); - const [a, b] = parts; +const ipv4Octet = z.number().int().min(0).max(255); +const ipv4OctetsSchema = z + .ipv4() + .transform((s) => s.split(".").map((octet) => Number.parseInt(octet, 10))) + .pipe(z.tuple([ipv4Octet, ipv4Octet, ipv4Octet, ipv4Octet])); + +function isPrivateIPv4(hostname: string): boolean { + const parsed = ipv4OctetsSchema.safeParse(hostname); + if (!parsed.success) return false; + + const [a, b] = parsed.data; if (a === 127) return true; // 127.0.0.0/8 if (a === 10) return true; // 10.0.0.0/8 if (a === 172 && b >= 16 && b <= 31) return true; // 172.16.0.0/12 @@ -42,18 +60,46 @@ function isPrivateIPv4(hostname: string): boolean { return false; } +const ipv6HostNormalized = z + .string() + .transform((h) => h.replace(/^\[|\]$/g, "").toLowerCase()) + .pipe(z.ipv6()); + +const ipv6Hextet16 = z + .string() + .regex(/^[0-9a-f]{1,4}$/) + .transform((s) => Number.parseInt(s, 16)) + .pipe(z.number().int().min(0).max(0xffff)); + +/** First 16-bit group, or null if address starts with `::` (no leading hextet) / not colon-shaped. */ +function ipv6LeadingHextet(bare: string): string | null { + if (bare.startsWith("::")) return null; + const colon = bare.indexOf(":"); + if (colon === -1) return null; + return bare.slice(0, colon); +} + function isPrivateIPv6(hostname: string): boolean { - const bare = hostname.replace(/^\[|\]$/g, "").toLowerCase(); + const host = ipv6HostNormalized.safeParse(hostname); + if (!host.success) return false; + + const bare = host.data; if (bare === "::1" || bare === "::0" || bare === "::") return true; - if (bare.startsWith("fc") || bare.startsWith("fd")) return true; // ULA - if (bare.startsWith("fe80")) return true; // link-local - return false; + + const first = ipv6LeadingHextet(bare); + if (first === null) return false; + + const hextet = ipv6Hextet16.safeParse(first); + if (!hextet.success) return false; + + const n = hextet.data; + return (n >= 0xfc00 && n <= 0xfdff) || (n >= 0xfe80 && n <= 0xfebf); // ULA fc00::/7, link-local fe80::/10 } /** * Throws if the URL points to a private/internal address or uses a - * non-HTTP(S) protocol. Set FEDERATION_ALLOW_PRIVATE_URLS=true to - * allow localhost/127.0.0.1 for local federation testing. + * non-HTTP(S) protocol. Hosts listed in `DEV_ALLOWED_HOSTNAMES` are always allowed + * (use bare host or `host:port` / full URL — port is stripped to match `URL.hostname`). */ export function assertSafeUrl(url: string): void { let parsed: URL; @@ -69,7 +115,8 @@ export function assertSafeUrl(url: string): void { const hostname = parsed.hostname; - if (SSRF_BYPASS && DEV_ALLOWED_HOSTNAMES.has(hostname)) { + // Explicit dev allowlist wins (host-only match; list entries may use host:port — see allowlistHostname). + if (DEV_ALLOWED_HOSTNAMES.has(hostname)) { return; } diff --git a/src/lib/plugins/server/helpers/social/social.ts b/src/lib/plugins/server/helpers/social/social.ts index 61b8d0a..572f2ab 100644 --- a/src/lib/plugins/server/helpers/social/social.ts +++ b/src/lib/plugins/server/helpers/social/social.ts @@ -155,6 +155,12 @@ export default { field: "url" } }, + acknowledged: { + type: "boolean", + required: true, + index: false, + defaultValue: false, + }, } }, deliveryJobs: { diff --git a/src/server.ts b/src/server.ts index 55862d5..b807025 100644 --- a/src/server.ts +++ b/src/server.ts @@ -24,7 +24,7 @@ app.prepare().then(async () => { server.listen(port) console.log( - `> Server listening at http://localhost:${port} as ${dev ? 'development' : process.env.NODE_ENV + `> Server listening at ${process.env.BETTER_AUTH_URL!} as ${dev ? 'development' : process.env.NODE_ENV }` ) }) \ No newline at end of file diff --git a/tests/auth.test.ts b/tests/auth.test.ts index ee68d46..cb7d776 100644 --- a/tests/auth.test.ts +++ b/tests/auth.test.ts @@ -1,45 +1,43 @@ -import { auth } from "@/lib/auth" -import { expect, test } from "@playwright/test" // NOTICE: Does not work, will fix it later -test("create and login user", async ({ context, page }) => { - const ctx = await auth.$context - const testUtils = ctx.test +// test("create and login user", async ({ context, page }) => { +// const ctx = await auth.$context +// const testUtils = ctx.test - // Go to home page - await page.goto("/") - // Check if we are redirected to the auth page - await expect(page).toHaveURL("/auth") +// // Go to home page +// await page.goto("/") +// // Check if we are redirected to the auth page +// await expect(page).toHaveURL("/auth") - // Create and save user - const user = testUtils.createUser({ - email: "e2e@example.com", - name: "E2E User" - }) - await testUtils.saveUser(user) +// // Create and save user +// const user = testUtils.createUser({ +// email: "e2e@example.com", +// name: "E2E User" +// }) +// await testUtils.saveUser(user) - // Get cookies and inject into browser - const cookies = await testUtils.getCookies({ - userId: user.id, - domain: "localhost" - }) - await context.addCookies(cookies) +// // Get cookies and inject into browser +// const cookies = await testUtils.getCookies({ +// userId: user.id, +// domain: "localhost" +// }) +// await context.addCookies(cookies) - // Login - await testUtils.login({ userId: user.id }) - // Check if we got redirected to the home page - await expect(page).toHaveURL("/") +// // Login +// await testUtils.login({ userId: user.id }) +// // Check if we got redirected to the home page +// await expect(page).toHaveURL("/") - // Check if we are logged in - const headers = await testUtils.getAuthHeaders({ userId: user.id }) - expect(headers).toBeDefined() - expect(headers.get("Authorization")).toBeDefined() +// // Check if we are logged in +// const headers = await testUtils.getAuthHeaders({ userId: user.id }) +// expect(headers).toBeDefined() +// expect(headers.get("Authorization")).toBeDefined() - // Delete user - await testUtils.deleteUser(user.id) +// // Delete user +// await testUtils.deleteUser(user.id) - // Check if user is deleted - const deletedUser = await ctx.internalAdapter.findUserById(user.id) - expect(deletedUser).toBeNull() -}) \ No newline at end of file +// // Check if user is deleted +// const deletedUser = await ctx.internalAdapter.findUserById(user.id) +// expect(deletedUser).toBeNull() +// }) \ No newline at end of file diff --git a/tests/discover.test.ts b/tests/discover.test.ts index b128784..47b619e 100644 --- a/tests/discover.test.ts +++ b/tests/discover.test.ts @@ -5,6 +5,10 @@ import { clearServerRegistry, getServerByUrl, insertServerEcho, } from "./helper const debug = createDebug("test:discover"); const url = "http://172.21.157.201:3001"; +const serverUrl = process.env.BETTER_AUTH_URL!; +if (!serverUrl) { + throw new Error("BETTER_AUTH_URL is not set"); +} test.beforeEach(async () => { await clearServerRegistry() @@ -14,7 +18,7 @@ test.afterEach(async () => { }) test("discover server", async ({ request, page }) => { - const response = await request.post(`http://192.168.3.26:3000/discover`, { + const response = await request.post(`${serverUrl}/discover`, { data: { method: "REGISTER", url: new URL(url).toString(), @@ -31,12 +35,12 @@ test("discover server", async ({ request, page }) => { expect(body.echo).toBeInstanceOf(Object) await insertServerEcho( - "http://192.168.3.26:3000", + serverUrl, body.echo.publicKey as string, body.echo.encryptionPublicKey as string, ); - const server = await getServerByUrl("http://192.168.3.26:3000"); + const server = await getServerByUrl(serverUrl); expect(server).toBeDefined() expect(server?.publicKey).toBe(body.echo.publicKey as string) });