feat: enhance federation functionality by reworking the workers.

- Introduced single Redis connection for managing federation delivery jobs, improving reliability and performance.
- Updated environment configuration to include Redis connection details and allowed hostnames for CORS.
- Refactored existing code to streamline federation processes and improve error handling.
- Enhanced database schema to track acknowledgment status for follow requests.

This update aims to strengthen the federation's communication capabilities and ensure better handling of server interactions.

#3 #4
This commit is contained in:
Nixyan 2026-04-08 10:35:59 -03:00
parent cb95c9cdba
commit 7049a40870
23 changed files with 846 additions and 565 deletions

View file

@ -1,7 +1,15 @@
BETTER_AUTH_SECRET=
BETTER_AUTH_URL=
DATABASE_URL=
# Should follow the format: redis://<host>:<port>
# 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://<username>:<password>@<host>:<port>/<database>
EMAIL_HOST=
EMAIL_PORT=
@ -9,7 +17,7 @@ EMAIL_SECURE=
EMAIL_USER=
EMAIL_PASSWORD=
DEBUG=
DEBUG=app:*,test:*
MINIO_BUCKET=
MINIO_ENDPOINT=

227
bun.lock
View file

@ -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=="],

View file

@ -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",

View file

@ -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: [

View file

@ -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`, {
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: body,
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
target: peerBaseUrl,
payload: registerDiscoverPayload,
}),
});
return response.json();
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 (

View file

@ -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,

View file

@ -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(),

View file

@ -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;
}

View file

@ -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<FederationDeliveryJob> | null = null;
export function getFederationQueue(): Queue<FederationDeliveryJob> {
if (!_deliveryQueue) {
_deliveryQueue = new Queue<FederationDeliveryJob>(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<HealthCheckJob> | null = null;
export function getHealthCheckQueue(): Queue<HealthCheckJob> {
if (!_healthCheckQueue) {
_healthCheckQueue = new Queue<HealthCheckJob>(HEALTH_CHECK_QUEUE_NAME, {
connection: createRedisConnection() as never,
});
}
return _healthCheckQueue;
}
export async function scheduleHealthCheck(serverUrl: string, attempt: number): Promise<void> {
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<FederationDeliveryJob>) {
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<typeof FollowEnvelopeSchema>;
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<typeof FollowEnvelopeSchema>;
} 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<typeof FollowEnvelopeSchema>;
}
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<HealthCheckJob>) {
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<FederationDeliveryJob>(
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<HealthCheckJob>(
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 };
}

View file

@ -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<void>;
const ackHandlers: Record<string, AckHandler> = {
'deliver-follow': handleFollowAck,
};
// ---------------------------------------------------------------------------
// Main processor
// ---------------------------------------------------------------------------
export async function processFederationDelivery(job: Job<FederationDeliveryJob>): Promise<void> {
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);
}

View file

@ -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<string> {
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<void> {
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);
}

View file

@ -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<HealthCheckJob>): Promise<void> {
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`);
}
}

73
src/lib/bull/queues.ts Normal file
View file

@ -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<FederationDeliveryJob> | null = null;
export function getFederationQueue(): Queue<FederationDeliveryJob> {
if (!_deliveryQueue) {
_deliveryQueue = new Queue<FederationDeliveryJob>(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<HealthCheckJob> | null = null;
export function getHealthCheckQueue(): Queue<HealthCheckJob> {
if (!_healthCheckQueue) {
_healthCheckQueue = new Queue<HealthCheckJob>(HEALTH_CHECK_QUEUE_NAME, {
connection: getRedisConnection() as never,
});
}
return _healthCheckQueue;
}
export async function scheduleHealthCheck(serverUrl: string, attempt: number): Promise<void> {
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,
});
}

89
src/lib/bull/worker.ts Normal file
View file

@ -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<FederationDeliveryJob>;
healthCheckWorker: Worker<HealthCheckJob>;
}
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<FederationDeliveryJob>(
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<HealthCheckJob>(
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;
}

View file

@ -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),

View file

@ -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}`);
}

View file

@ -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<string> {
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<string> {
}),
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);
}

View file

@ -32,6 +32,11 @@ export const DEFAULT_THREAT_MODEL: Record<FederationErrorCode, ThreatPolicy> = {
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,

View file

@ -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;
}

View file

@ -155,6 +155,12 @@ export default {
field: "url"
}
},
acknowledged: {
type: "boolean",
required: true,
index: false,
defaultValue: false,
},
}
},
deliveryJobs: {

View file

@ -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
}`
)
})

View file

@ -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()
})
// // Check if user is deleted
// const deletedUser = await ctx.internalAdapter.findUserById(user.id)
// expect(deletedUser).toBeNull()
// })

View file

@ -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)
});