refactor: modularize plugins with federation and encryption infrastructure

Major changes:
- Restructure plugin architecture: moved federation logic into a dedicated `federation` plugin with Better Auth integration, defining schemas for server registry, key rotation, and blacklist management
- Extract encryption layer: new `oven` plugin handles end-to-end encryption (E2EE) with OLM client/server implementations
- Reorganize social features: consolidated social endpoints (posts, follows, blocks, mutes) and removed legacy plugin patterns in favor of unified plugin structure
- Decentralized key management: refactored `keytools` and `keygen` to support federation key rotation with challenge tokens and health checks

Infrastructure updates:
- Upgrade dependencies: bump Better Auth to 1.6.9, React to 19.2.5, Next.js to 16.2.3, Tailwind to 4.2.4
- Add cryptographic libraries: @scure/bip39, @signalapp/libsignal-client, @matrix-org/matrix-sdk-crypto-wasm for enhanced federation security
- Add utilities: base58-js, uuid for federation identifier handling
- Update database schema with new federation tables (serverRegistry, rotateChallengeTokens, blacklistedServers)

Minor updates: test suite alignment, storage client cleanup, PostFederationSchema refinements

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Nixyan 2026-05-05 11:40:14 -03:00
parent 7049a40870
commit 66ebebd105
47 changed files with 1753 additions and 246 deletions

11
.cursor/mcp.json Normal file
View file

@ -0,0 +1,11 @@
{
"mcpServers": {
"next-devtools": {
"command": "npx",
"args": [
"-y",
"next-devtools-mcp@latest"
]
}
}
}

View file

@ -24,4 +24,6 @@ MINIO_ENDPOINT=
MINIO_PORT= MINIO_PORT=
MINIO_USE_SSL= MINIO_USE_SSL=
MINIO_ACCESS_KEY= MINIO_ACCESS_KEY=
MINIO_SECRET_KEY= MINIO_SECRET_KEY=
NEXT_PUBLIC_GIT_URL=

255
bun.lock
View file

@ -5,59 +5,64 @@
"": { "": {
"name": "sipher", "name": "sipher",
"dependencies": { "dependencies": {
"@better-auth/drizzle-adapter": "latest", "@better-auth/drizzle-adapter": "^1.6.9",
"@hookform/resolvers": "latest", "@hookform/resolvers": "^5.2.2",
"@nanostores/react": "latest", "@matrix-org/matrix-sdk-crypto-wasm": "^18.2.0",
"@react-email/components": "latest", "@nanostores/react": "^1.1.0",
"better-auth": "latest", "@react-email/components": "1.0.12",
"bullmq": "latest", "@scure/bip39": "^2.2.0",
"class-variance-authority": "latest", "@signalapp/libsignal-client": "^0.92.2",
"clsx": "latest", "base58-js": "^3.0.3",
"debug": "latest", "better-auth": "^1.6.9",
"dexie": "latest", "bullmq": "^5.76.5",
"dexie-react-hooks": "latest", "class-variance-authority": "^0.7.1",
"dotenv": "latest", "clsx": "^2.1.1",
"drizzle-orm": "latest", "debug": "^4.4.3",
"framer-motion": "latest", "dexie": "^4.4.2",
"ioredis": "latest", "dexie-react-hooks": "^4.4.0",
"lucide-react": "latest", "dotenv": "^17.4.2",
"minio": "latest", "drizzle-orm": "^0.45.2",
"nanostores": "latest", "framer-motion": "^12.38.0",
"next": "latest", "ioredis": "^5.10.1",
"next-themes": "latest", "lucide-react": "^1.14.0",
"nodemailer": "latest", "minio": "^8.0.7",
"pg": "latest", "nanostores": "^1.3.0",
"radix-ui": "latest", "next": "16.2.3",
"react": "latest", "next-themes": "^0.4.6",
"react-dom": "latest", "nodemailer": "^8.0.7",
"react-hook-form": "latest", "pg": "^8.20.0",
"socket.io": "latest", "radix-ui": "^1.4.3",
"socket.io-client": "latest", "react": "19.2.5",
"sonner": "latest", "react-dom": "19.2.5",
"tailwind-merge": "latest", "react-hook-form": "^7.75.0",
"tweetnacl": "latest", "socket.io": "^4.8.3",
"tweetnacl-util": "latest", "socket.io-client": "^4.8.3",
"zod": "latest", "sonner": "^2.0.7",
"tailwind-merge": "^3.5.0",
"tweetnacl": "^1.0.3",
"tweetnacl-util": "^0.15.1",
"uuid": "^14.0.0",
"zod": "^4.4.3",
}, },
"devDependencies": { "devDependencies": {
"@tailwindcss/postcss": "latest", "@tailwindcss/postcss": "^4.2.4",
"@types/bun": "latest", "@types/bun": "^1.3.13",
"@types/debug": "latest", "@types/debug": "^4.1.13",
"@types/node": "latest", "@types/node": "^25.6.0",
"@types/nodemailer": "latest", "@types/nodemailer": "^8.0.0",
"@types/pg": "latest", "@types/pg": "^8.20.0",
"@types/react": "latest", "@types/react": "^19.2.14",
"@types/react-dom": "latest", "@types/react-dom": "^19.2.3",
"auth": "latest", "auth": "^1.6.9",
"babel-plugin-react-compiler": "latest", "babel-plugin-react-compiler": "1.0.0",
"cross-env": "latest", "cross-env": "^10.1.0",
"drizzle-kit": "latest", "drizzle-kit": "^0.31.10",
"react-email": "latest", "react-email": "5.2.10",
"shadcn": "latest", "shadcn": "^4.6.0",
"tailwindcss": "latest", "tailwindcss": "^4.2.4",
"tsx": "latest", "tsx": "^4.21.0",
"tw-animate-css": "latest", "tw-animate-css": "^1.4.0",
"typescript": "latest", "typescript": "^6.0.3",
}, },
}, },
}, },
@ -133,19 +138,19 @@
"@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=="], "@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.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/core": ["@better-auth/core@1.6.9", "", { "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", "@opentelemetry/api"] }, "sha512-ADFk5pwmLybmc+LvYvXJ6M1x2oY/EyYLkwLuH0x28FUq12DfjL0wnE7g+WRDf3yozDO+qIxTpFGXDGwLKbfz0w=="],
"@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/drizzle-adapter": ["@better-auth/drizzle-adapter@1.6.9", "", { "peerDependencies": { "@better-auth/core": "^1.6.9", "@better-auth/utils": "0.4.0", "drizzle-orm": "^0.45.2" }, "optionalPeers": ["drizzle-orm"] }, "sha512-Lcco5hOGrMgc4XKAkvB6x72eQm4wCcya8IevMg4wBHY9W9GVg8pu23rpRX6VsVQSO4Ux13S7lFwUWtF7/r9aKw=="],
"@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/kysely-adapter": ["@better-auth/kysely-adapter@1.6.9", "", { "peerDependencies": { "@better-auth/core": "^1.6.9", "@better-auth/utils": "0.4.0", "kysely": "^0.28.14" }, "optionalPeers": ["kysely"] }, "sha512-gyjuuxJtZ4o9G9z9q4kqn24X2kvMSp7F+KHogYxF03SnXY/2WleAcuj57iC4wP3e9mGDbjPOrnM5K6Kr3Ktdpw=="],
"@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/memory-adapter": ["@better-auth/memory-adapter@1.6.9", "", { "peerDependencies": { "@better-auth/core": "^1.6.9", "@better-auth/utils": "0.4.0" } }, "sha512-XmIG4tUnOXZ+KEcWjHUjOI9Z5donD09dC2t/AQTXifAUIqx7cySg86w0KTM09ArzAxRx1fCqO36Wkt5nULnrkQ=="],
"@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/mongo-adapter": ["@better-auth/mongo-adapter@1.6.9", "", { "peerDependencies": { "@better-auth/core": "^1.6.9", "@better-auth/utils": "0.4.0", "mongodb": "^6.0.0 || ^7.0.0" }, "optionalPeers": ["mongodb"] }, "sha512-h+AiRJ/TsBSi+ZDjySASBpbJ/9QCXBre34PSKgCz7QmTHrFM9Cg2EM4AM7LjR5lPXipEE+2rWPBc9wfnUBjhcw=="],
"@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/prisma-adapter": ["@better-auth/prisma-adapter@1.6.9", "", { "peerDependencies": { "@better-auth/core": "^1.6.9", "@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-XHks01ntK20orqK/jICq8wmEbJ/zT6dct49Fk8zTQKN9QNGDc+Ix5+7z/Kvui0DXGFf790GfvRozquzaLtXa8Q=="],
"@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/telemetry": ["@better-auth/telemetry@1.6.9", "", { "peerDependencies": { "@better-auth/core": "^1.6.9", "@better-auth/utils": "0.4.0", "@better-fetch/fetch": "1.1.21" } }, "sha512-0u5zkhSCAQFoN3DHvUkLHOF6MBbVTDAa6mU8mhPwiysdz1x21vMzhzfaAKN/ZGWaQ09v91/F+2qu42G/bhUV4A=="],
"@better-auth/utils": ["@better-auth/utils@0.4.0", "", { "dependencies": { "@noble/hashes": "^2.0.1" } }, "sha512-RpMtLUIQAEWMgdPLNVbIF5ON2mm+CH0U3rCdUCU1VyeAUui4m38DyK7/aXMLZov2YDjG684pS1D0MBllrmgjQA=="], "@better-auth/utils": ["@better-auth/utils@0.4.0", "", { "dependencies": { "@noble/hashes": "^2.0.1" } }, "sha512-RpMtLUIQAEWMgdPLNVbIF5ON2mm+CH0U3rCdUCU1VyeAUui4m38DyK7/aXMLZov2YDjG684pS1D0MBllrmgjQA=="],
@ -319,6 +324,8 @@
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
"@matrix-org/matrix-sdk-crypto-wasm": ["@matrix-org/matrix-sdk-crypto-wasm@18.2.0", "", {}, "sha512-puyZefvq6sHfqlmkri8umhA44724H2JL0YtX8wlvhGuNl8awX/Q1tZyW2Iekm9ZJP7BtuOqlNdg9oQd6iaGbNw=="],
"@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.27.1", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-sr6GbP+4edBwFndLbM60gf07z0FQ79gaExpnsjMGePXqFcSSb7t6iscpjk9DhFhwd+mTEQrzNafGP8/iGGFYaA=="], "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.27.1", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-sr6GbP+4edBwFndLbM60gf07z0FQ79gaExpnsjMGePXqFcSSb7t6iscpjk9DhFhwd+mTEQrzNafGP8/iGGFYaA=="],
"@mrleebo/prisma-ast": ["@mrleebo/prisma-ast@0.13.1", "", { "dependencies": { "chevrotain": "^10.5.0", "lilconfig": "^2.1.0" } }, "sha512-XyroGQXcHrZdvmrGJvsA9KNeOOgGMg1Vg9OlheUsBOSKznLMDl+YChxbkboRHvtFYJEMRYmlV3uoo/njCw05iw=="], "@mrleebo/prisma-ast": ["@mrleebo/prisma-ast@0.13.1", "", { "dependencies": { "chevrotain": "^10.5.0", "lilconfig": "^2.1.0" } }, "sha512-XyroGQXcHrZdvmrGJvsA9KNeOOgGMg1Vg9OlheUsBOSKznLMDl+YChxbkboRHvtFYJEMRYmlV3uoo/njCw05iw=="],
@ -339,29 +346,29 @@
"@nanostores/react": ["@nanostores/react@1.1.0", "", { "peerDependencies": { "nanostores": "^1.2.0", "react": ">=18.0.0" } }, "sha512-MbH35fjhcf7LAubYX5vhOChYUfTLzNLqH/mBGLVsHkcvjy0F8crO1WQwdmQ2xKbAmtpalDa2zBt3Hlg5kqr8iw=="], "@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.2", "", {}, "sha512-LqSGz5+xGk9EL/iBDr2yo/CgNQV6cFsNhRR2xhSXYh7B/hb4nePCxlmDvGEKG30NMHDFf0raqSyOZiQrO7BkHQ=="], "@next/env": ["@next/env@16.2.3", "", {}, "sha512-ZWXyj4uNu4GCWQw9cjRxWlbD+33mcDszIo9iQxFnBX3Wmgq9ulaSJcl6VhuWx5pCWqqD+9W6Wfz7N0lM5lYPMA=="],
"@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.2.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-B92G3ulrwmkDSEJEp9+XzGLex5wC1knrmCSIylyVeiAtCIfvEJYiN3v5kXPlYt5R4RFlsfO/v++aKV63Acrugg=="], "@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.2.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-u37KDKTKQ+OQLvY+z7SNXixwo4Q2/IAJFDzU1fYe66IbCE51aDSAzkNDkWmLN0yjTUh4BKBd+hb69jYn6qqqSg=="],
"@next/swc-darwin-x64": ["@next/swc-darwin-x64@16.2.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-7ZwSgNKJNQiwW0CKhNm9B1WS2L1Olc4B2XY0hPYCAL3epFnugMhuw5TMWzMilQ3QCZcCHoYm9NGWTHbr5REFxw=="], "@next/swc-darwin-x64": ["@next/swc-darwin-x64@16.2.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-gHjL/qy6Q6CG3176FWbAKyKh9IfntKZTB3RY/YOJdDFpHGsUDXVH38U4mMNpHVGXmeYW4wj22dMp1lTfmu/bTQ=="],
"@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-gnu": ["@next/swc-linux-arm64-gnu@16.2.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-U6vtblPtU/P14Y/b/n9ZY0GOxbbIhTFuaFR7F4/uMBidCi2nSdaOFhA0Go81L61Zd6527+yvuX44T4ksnf8T+Q=="],
"@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-arm64-musl": ["@next/swc-linux-arm64-musl@16.2.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-/YV0LgjHUmfhQpn9bVoGc4x4nan64pkhWR5wyEV8yCOfwwrH630KpvRg86olQHTwHIn1z59uh6JwKvHq1h4QEw=="],
"@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-gnu": ["@next/swc-linux-x64-gnu@16.2.3", "", { "os": "linux", "cpu": "x64" }, "sha512-/HiWEcp+WMZ7VajuiMEFGZ6cg0+aYZPqCJD3YJEfpVWQsKYSjXQG06vJP6F1rdA03COD9Fef4aODs3YxKx+RDQ=="],
"@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-linux-x64-musl": ["@next/swc-linux-x64-musl@16.2.3", "", { "os": "linux", "cpu": "x64" }, "sha512-Kt44hGJfZSefebhk/7nIdivoDr3Ugp5+oNz9VvF3GUtfxutucUIHfIO0ZYO8QlOPDQloUVQn4NVC/9JvHRk9hw=="],
"@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-arm64-msvc": ["@next/swc-win32-arm64-msvc@16.2.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-O2NZ9ie3Tq6xj5Z5CSwBT3+aWAMW2PIZ4egUi9MaWLkwaehgtB7YZjPm+UpcNpKOme0IQuqDcor7BsW6QBiQBw=="],
"@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.2.2", "", { "os": "win32", "cpu": "x64" }, "sha512-aHaKceJgdySReT7qeck5oShucxWRiiEuwCGK8HHALe6yZga8uyFpLkPgaRw3kkF04U7ROogL/suYCNt/+CuXGA=="], "@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.2.3", "", { "os": "win32", "cpu": "x64" }, "sha512-Ibm29/GgB/ab5n7XKqlStkm54qqZE8v2FnijUPBgrd67FWrac45o/RsNlaOWjme/B5UqeWt/8KM4aWBwA1D2Kw=="],
"@noble/ciphers": ["@noble/ciphers@2.1.1", "", {}, "sha512-bysYuiVfhxNJuldNXlFEitTVdNnYUc+XNJZd7Qm2a5j1vZHgY+fazadNFWFaMK/2vye0JVlxV3gHmC0WDfAOQw=="], "@noble/ciphers": ["@noble/ciphers@2.1.1", "", {}, "sha512-bysYuiVfhxNJuldNXlFEitTVdNnYUc+XNJZd7Qm2a5j1vZHgY+fazadNFWFaMK/2vye0JVlxV3gHmC0WDfAOQw=="],
"@noble/curves": ["@noble/curves@1.9.7", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw=="], "@noble/curves": ["@noble/curves@1.9.7", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw=="],
"@noble/hashes": ["@noble/hashes@2.0.1", "", {}, "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw=="], "@noble/hashes": ["@noble/hashes@2.2.0", "", {}, "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg=="],
"@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
@ -531,7 +538,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/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.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/components": ["@react-email/components@1.0.12", "", { "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.6", "@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-tH18JhPDWgE+3jnYkzyB6ZrZdfNnEsFe4PwmuXmlOw4NGIysP8wPY5aXZg++pTG9qUabXg1nzX/FGHGkObH8xQ=="],
"@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=="], "@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=="],
@ -553,7 +560,7 @@
"@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/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.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/render": ["@react-email/render@2.0.6", "", { "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-xOzaYkH3jLZKqN5MqrTXYnmqBYUnZSVbkxdb5PGGmDcK6sKDVMliaDiSwfXajRC9JtSHTcGc2tmGLHWuCgVpog=="],
"@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/row": ["@react-email/row@0.0.13", "", { "peerDependencies": { "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-bYnOac40vIKCId7IkwuLAAsa3fKfSfqCvv6epJKmPE0JBuu5qI4FHFCl9o9dVpIIS08s/ub+Y/txoMt0dYziGw=="],
@ -563,10 +570,16 @@
"@react-email/text": ["@react-email/text@0.1.6", "", { "peerDependencies": { "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-TYqkioRS45wTR5il3dYk/SbUjjEdhSwh9BtRNB99qNH1pXAwA45H7rAuxehiu8iJQJH0IyIr+6n62gBz9ezmsw=="], "@react-email/text": ["@react-email/text@0.1.6", "", { "peerDependencies": { "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-TYqkioRS45wTR5il3dYk/SbUjjEdhSwh9BtRNB99qNH1pXAwA45H7rAuxehiu8iJQJH0IyIr+6n62gBz9ezmsw=="],
"@scure/base": ["@scure/base@2.2.0", "", {}, "sha512-b8XEupJibegiXV+tDUseI8oLQc8ei3d/4Jkb2RpbHh3MfE054ov3uIz2dhFkB3FI8iwYkEh0gGCApkrYggkPNg=="],
"@scure/bip39": ["@scure/bip39@2.2.0", "", { "dependencies": { "@noble/hashes": "2.2.0", "@scure/base": "2.2.0" } }, "sha512-T/Bj/YvYMNkIPq6EENO6/rcs2e7qTNuyoUXf0KBFDmp0ZDu0H2X4Lq6yC3i0c8PcWkov5EbW+yQZZbdMmk154A=="],
"@sec-ant/readable-stream": ["@sec-ant/readable-stream@0.4.1", "", {}, "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg=="], "@sec-ant/readable-stream": ["@sec-ant/readable-stream@0.4.1", "", {}, "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg=="],
"@selderee/plugin-htmlparser2": ["@selderee/plugin-htmlparser2@0.11.0", "", { "dependencies": { "domhandler": "^5.0.3", "selderee": "^0.11.0" } }, "sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ=="], "@selderee/plugin-htmlparser2": ["@selderee/plugin-htmlparser2@0.11.0", "", { "dependencies": { "domhandler": "^5.0.3", "selderee": "^0.11.0" } }, "sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ=="],
"@signalapp/libsignal-client": ["@signalapp/libsignal-client@0.92.2", "", { "dependencies": { "node-gyp-build": "^4.8.0", "type-fest": "^4.26.0" } }, "sha512-mSYKpw32Rtmm+D1y8NKzNA9wkiuU60gXRGuum6NTGRN9C3NI4R1cb6xE9w7q+6rjR4zAb4qZWb9QUG5QcLr7pg=="],
"@sindresorhus/merge-streams": ["@sindresorhus/merge-streams@4.0.0", "", {}, "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ=="], "@sindresorhus/merge-streams": ["@sindresorhus/merge-streams@4.0.0", "", {}, "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ=="],
"@socket.io/component-emitter": ["@socket.io/component-emitter@3.1.2", "", {}, "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA=="], "@socket.io/component-emitter": ["@socket.io/component-emitter@3.1.2", "", {}, "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA=="],
@ -577,39 +590,39 @@
"@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="], "@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="],
"@tailwindcss/node": ["@tailwindcss/node@4.2.2", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.19.0", "jiti": "^2.6.1", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.2.2" } }, "sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA=="], "@tailwindcss/node": ["@tailwindcss/node@4.2.4", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.19.0", "jiti": "^2.6.1", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.2.4" } }, "sha512-Ai7+yQPxz3ddrDQzFfBKdHEVBg0w3Zl83jnjuwxnZOsnH9pGn93QHQtpU0p/8rYWxvbFZHneni6p1BSLK4DkGA=="],
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.2.2", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.2.2", "@tailwindcss/oxide-darwin-arm64": "4.2.2", "@tailwindcss/oxide-darwin-x64": "4.2.2", "@tailwindcss/oxide-freebsd-x64": "4.2.2", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.2", "@tailwindcss/oxide-linux-arm64-gnu": "4.2.2", "@tailwindcss/oxide-linux-arm64-musl": "4.2.2", "@tailwindcss/oxide-linux-x64-gnu": "4.2.2", "@tailwindcss/oxide-linux-x64-musl": "4.2.2", "@tailwindcss/oxide-wasm32-wasi": "4.2.2", "@tailwindcss/oxide-win32-arm64-msvc": "4.2.2", "@tailwindcss/oxide-win32-x64-msvc": "4.2.2" } }, "sha512-qEUA07+E5kehxYp9BVMpq9E8vnJuBHfJEC0vPC5e7iL/hw7HR61aDKoVoKzrG+QKp56vhNZe4qwkRmMC0zDLvg=="], "@tailwindcss/oxide": ["@tailwindcss/oxide@4.2.4", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.2.4", "@tailwindcss/oxide-darwin-arm64": "4.2.4", "@tailwindcss/oxide-darwin-x64": "4.2.4", "@tailwindcss/oxide-freebsd-x64": "4.2.4", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.4", "@tailwindcss/oxide-linux-arm64-gnu": "4.2.4", "@tailwindcss/oxide-linux-arm64-musl": "4.2.4", "@tailwindcss/oxide-linux-x64-gnu": "4.2.4", "@tailwindcss/oxide-linux-x64-musl": "4.2.4", "@tailwindcss/oxide-wasm32-wasi": "4.2.4", "@tailwindcss/oxide-win32-arm64-msvc": "4.2.4", "@tailwindcss/oxide-win32-x64-msvc": "4.2.4" } }, "sha512-9El/iI069DKDSXwTvB9J4BwdO5JhRrOweGaK25taBAvBXyXqJAX+Jqdvs8r8gKpsI/1m0LeJLyQYTf/WLrBT1Q=="],
"@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.2.2", "", { "os": "android", "cpu": "arm64" }, "sha512-dXGR1n+P3B6748jZO/SvHZq7qBOqqzQ+yFrXpoOWWALWndF9MoSKAT3Q0fYgAzYzGhxNYOoysRvYlpixRBBoDg=="], "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.2.4", "", { "os": "android", "cpu": "arm64" }, "sha512-e7MOr1SAn9U8KlZzPi1ZXGZHeC5anY36qjNwmZv9pOJ8E4Q6jmD1vyEHkQFmNOIN7twGPEMXRHmitN4zCMN03g=="],
"@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.2.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-iq9Qjr6knfMpZHj55/37ouZeykwbDqF21gPFtfnhCCKGDcPI/21FKC9XdMO/XyBM7qKORx6UIhGgg6jLl7BZlg=="], "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-tSC/Kbqpz/5/o/C2sG7QvOxAKqyd10bq+ypZNf+9Fi2TvbVbv1zNpcEptcsU7DPROaSbVgUXmrzKhurFvo5eDg=="],
"@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.2.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-BlR+2c3nzc8f2G639LpL89YY4bdcIdUmiOOkv2GQv4/4M0vJlpXEa0JXNHhCHU7VWOKWT/CjqHdTP8aUuDJkuw=="], "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-yPyUXn3yO/ufR6+Kzv0t4fCg2qNr90jxXc5QqBpjlPNd0NqyDXcmQb/6weunH/MEDXW5dhyEi+agTDiqa3WsGg=="],
"@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.2.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-YUqUgrGMSu2CDO82hzlQ5qSb5xmx3RUrke/QgnoEx7KvmRJHQuZHZmZTLSuuHwFf0DJPybFMXMYf+WJdxHy/nQ=="], "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.2.4", "", { "os": "freebsd", "cpu": "x64" }, "sha512-BoMIB4vMQtZsXdGLVc2z+P9DbETkiopogfWZKbWwM8b/1Vinbs4YcUwo+kM/KeLkX3Ygrf4/PsRndKaYhS8Eiw=="],
"@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.2.2", "", { "os": "linux", "cpu": "arm" }, "sha512-FPdhvsW6g06T9BWT0qTwiVZYE2WIFo2dY5aCSpjG/S/u1tby+wXoslXS0kl3/KXnULlLr1E3NPRRw0g7t2kgaQ=="], "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.2.4", "", { "os": "linux", "cpu": "arm" }, "sha512-7pIHBLTHYRAlS7V22JNuTh33yLH4VElwKtB3bwchK/UaKUPpQ0lPQiOWcbm4V3WP2I6fNIJ23vABIvoy2izdwA=="],
"@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.2.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-4og1V+ftEPXGttOO7eCmW7VICmzzJWgMx+QXAJRAhjrSjumCwWqMfkDrNu1LXEQzNAwz28NCUpucgQPrR4S2yw=="], "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-+E4wxJ0ZGOzSH325reXTWB48l42i93kQqMvDyz5gqfRzRZ7faNhnmvlV4EPGJU3QJM/3Ab5jhJ5pCRUsKn6OQw=="],
"@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.2.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag=="], "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-bBADEGAbo4ASnppIziaQJelekCxdMaxisrk+fB7Thit72IBnALp9K6ffA2G4ruj90G9XRS2VQ6q2bCKbfFV82g=="],
"@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.2.2", "", { "os": "linux", "cpu": "x64" }, "sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg=="], "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-7Mx25E4WTfnht0TVRTyC00j3i0M+EeFe7wguMDTlX4mRxafznw0CA8WJkFjWYH5BlgELd1kSjuU2JiPnNZbJDA=="],
"@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.2.2", "", { "os": "linux", "cpu": "x64" }, "sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ=="], "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-2wwJRF7nyhOR0hhHoChc04xngV3iS+akccHTGtz965FwF0up4b2lOdo6kI1EbDaEXKgvcrFBYcYQQ/rrnWFVfA=="],
"@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.2.2", "", { "dependencies": { "@emnapi/core": "^1.8.1", "@emnapi/runtime": "^1.8.1", "@emnapi/wasi-threads": "^1.1.0", "@napi-rs/wasm-runtime": "^1.1.1", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q=="], "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.2.4", "", { "dependencies": { "@emnapi/core": "^1.8.1", "@emnapi/runtime": "^1.8.1", "@emnapi/wasi-threads": "^1.1.0", "@napi-rs/wasm-runtime": "^1.1.1", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-FQsqApeor8Fo6gUEklzmaa9994orJZZDBAlQpK2Mq+DslRKFJeD6AjHpBQ0kZFQohVr8o85PPh8eOy86VlSCmw=="],
"@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.2.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-qPmaQM4iKu5mxpsrWZMOZRgZv1tOZpUm+zdhhQP0VhJfyGGO3aUKdbh3gDZc/dPLQwW4eSqWGrrcWNBZWUWaXQ=="], "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.2.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-L9BXqxC4ToVgwMFqj3pmZRqyHEztulpUJzCxUtLjobMCzTPsGt1Fa9enKbOpY2iIyVtaHNeNvAK8ERP/64sqGQ=="],
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.2.2", "", { "os": "win32", "cpu": "x64" }, "sha512-1T/37VvI7WyH66b+vqHj/cLwnCxt7Qt3WFu5Q8hk65aOvlwAhs7rAp1VkulBJw/N4tMirXjVnylTR72uI0HGcA=="], "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.2.4", "", { "os": "win32", "cpu": "x64" }, "sha512-ESlKG0EpVJQwRjXDDa9rLvhEAh0mhP1sF7sap9dNZT0yyl9SAG6T7gdP09EH0vIv0UNTlo6jPWyujD6559fZvw=="],
"@tailwindcss/postcss": ["@tailwindcss/postcss@4.2.2", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.2.2", "@tailwindcss/oxide": "4.2.2", "postcss": "^8.5.6", "tailwindcss": "4.2.2" } }, "sha512-n4goKQbW8RVXIbNKRB/45LzyUqN451deQK0nzIeauVEqjlI49slUlgKYJM2QyUzap/PcpnS7kzSUmPb1sCRvYQ=="], "@tailwindcss/postcss": ["@tailwindcss/postcss@4.2.4", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.2.4", "@tailwindcss/oxide": "4.2.4", "postcss": "^8.5.6", "tailwindcss": "4.2.4" } }, "sha512-wgAVj6nUWAolAu8YFvzT2cTBIElWHkjZwFYovF+xsqKsW2ADxM/X2opxj5NsF/qVccAOjRNe8X2IdPzMsWyHTg=="],
"@ts-morph/common": ["@ts-morph/common@0.27.0", "", { "dependencies": { "fast-glob": "^3.3.3", "minimatch": "^10.0.1", "path-browserify": "^1.0.1" } }, "sha512-Wf29UqxWDpc+i61k3oIOzcUfQt79PIT9y/MWfAGlrkjg6lBC1hwDECLXPVJAhWjiGbfBCxZd65F/LIZF3+jeJQ=="], "@ts-morph/common": ["@ts-morph/common@0.27.0", "", { "dependencies": { "fast-glob": "^3.3.3", "minimatch": "^10.0.1", "path-browserify": "^1.0.1" } }, "sha512-Wf29UqxWDpc+i61k3oIOzcUfQt79PIT9y/MWfAGlrkjg6lBC1hwDECLXPVJAhWjiGbfBCxZd65F/LIZF3+jeJQ=="],
"@types/bun": ["@types/bun@1.3.11", "", { "dependencies": { "bun-types": "1.3.11" } }, "sha512-5vPne5QvtpjGpsGYXiFyycfpDF2ECyPcTSsFBMa0fraoxiQyMJ3SmuQIGhzPg2WJuWxVBoxWJ2kClYTcw/4fAg=="], "@types/bun": ["@types/bun@1.3.13", "", { "dependencies": { "bun-types": "1.3.13" } }, "sha512-9fqXWk5YIHGGnUau9TEi+qdlTYDAnOj+xLCmSTwXfAIqXr2x4tytJb43E9uCvt09zJURKXwAtkoH4nLQfzeTXw=="],
"@types/cors": ["@types/cors@2.8.19", "", { "dependencies": { "@types/node": "*" } }, "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg=="], "@types/cors": ["@types/cors@2.8.19", "", { "dependencies": { "@types/node": "*" } }, "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg=="],
@ -617,7 +630,7 @@
"@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="],
"@types/node": ["@types/node@25.5.2", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-tO4ZIRKNC+MDWV4qKVZe3Ql/woTnmHDr5JD8UI5hn2pwBrHEwOEMZK7WlNb5RKB6EoJ02gwmQS9OrjuFnZYdpg=="], "@types/node": ["@types/node@25.6.0", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ=="],
"@types/nodemailer": ["@types/nodemailer@8.0.0", "", { "dependencies": { "@types/node": "*" } }, "sha512-fyf8jWULsCo0d0BuoQ75i6IeoHs47qcqxWc7yUdUcV0pOZGjUTTOvwdG1PRXUDqN/8A64yQdQdnA2pZgcdi+cA=="], "@types/nodemailer": ["@types/nodemailer@8.0.0", "", { "dependencies": { "@types/node": "*" } }, "sha512-fyf8jWULsCo0d0BuoQ75i6IeoHs47qcqxWc7yUdUcV0pOZGjUTTOvwdG1PRXUDqN/8A64yQdQdnA2pZgcdi+cA=="],
@ -653,7 +666,7 @@
"atomically": ["atomically@2.1.1", "", { "dependencies": { "stubborn-fs": "^2.0.0", "when-exit": "^2.1.4" } }, "sha512-P4w9o2dqARji6P7MHprklbfiArZAWvo07yW7qs3pdljb3BWr12FIB7W+p0zJiuiVsUpRO0iZn1kFFcpPegg0tQ=="], "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=="], "auth": ["auth@1.6.9", "", { "dependencies": { "@babel/core": "^7.29.0", "@babel/preset-react": "^7.28.5", "@babel/preset-typescript": "^7.28.5", "@better-auth/core": "1.6.9", "@better-auth/telemetry": "1.6.9", "@better-auth/utils": "0.4.0", "@clack/prompts": "^0.11.0", "@mrleebo/prisma-ast": "^0.13.1", "better-auth": "1.6.9", "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-VWsOu2QiUkJxuvhG+yth6oVpZWntjyYJTZd5CE1kHEVrRXKpAGeH1MKVlD5OpzGEveBrY9W2GLlYji3uYozLIw=="],
"aws-ssl-profiles": ["aws-ssl-profiles@1.1.2", "", {}, "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g=="], "aws-ssl-profiles": ["aws-ssl-profiles@1.1.2", "", {}, "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g=="],
@ -661,11 +674,13 @@
"balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], "balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
"base58-js": ["base58-js@3.0.3", "", {}, "sha512-3hf42BysHnUqmZO7mK6e5X/hs1AvyEJIhdVLbG/Mxn/fhFnhGxOO37mWbMHg1RT4TxqcPKXgqj9/bp1YG0GBXA=="],
"base64id": ["base64id@2.0.0", "", {}, "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog=="], "base64id": ["base64id@2.0.0", "", {}, "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog=="],
"baseline-browser-mapping": ["baseline-browser-mapping@2.10.0", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA=="], "baseline-browser-mapping": ["baseline-browser-mapping@2.10.0", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA=="],
"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-auth": ["better-auth@1.6.9", "", { "dependencies": { "@better-auth/core": "1.6.9", "@better-auth/drizzle-adapter": "1.6.9", "@better-auth/kysely-adapter": "1.6.9", "@better-auth/memory-adapter": "1.6.9", "@better-auth/mongo-adapter": "1.6.9", "@better-auth/prisma-adapter": "1.6.9", "@better-auth/telemetry": "1.6.9", "@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.45.2", "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-EBFURtglyiEZxbx4NJBoqUD8J65dX24yC+6I9AUbIXNgUkt76mshzGbHkxZ3n/lB7Dwq3kBC+hHt0hUQsnL7HA=="],
"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=="], "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=="],
@ -685,9 +700,9 @@
"buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
"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=="], "bullmq": ["bullmq@5.76.5", "", { "dependencies": { "cron-parser": "4.9.0", "ioredis": "5.10.1", "msgpackr": "1.11.12", "node-abort-controller": "3.1.1", "semver": "7.7.4", "tslib": "2.8.1" } }, "sha512-2OKJP2+ckc+TygsWdxxeZYYgM9xYnVXgIAx+perflhamZ6FEBu/cSrvpqM8++fJI5OgsIFLfxA9UO7BDZ74Inw=="],
"bun-types": ["bun-types@1.3.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-1KGPpoxQWl9f6wcZh57LvrPIInQMn2TQ7jsgxqpRzg+l0QPOFvJVH7HmvHo/AiPgwXy+/Thf6Ov3EdVn1vOabg=="], "bun-types": ["bun-types@1.3.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-QXKeHLlOLqQX9LgYaHJfzdBaV21T63HhFJnvuRCcjZiaUDpbs5ED1MgxbMra71CsryN/1dAoXuJJJwIv/2drVA=="],
"bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="], "bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="],
@ -815,7 +830,7 @@
"dot-prop": ["dot-prop@10.1.0", "", { "dependencies": { "type-fest": "^5.0.0" } }, "sha512-MVUtAugQMOff5RnBy2d9N31iG0lNwg1qAoAOn7pOK5wf94WIaE3My2p3uwTQuvS2AcqchkcR3bHByjaM0mmi7Q=="], "dot-prop": ["dot-prop@10.1.0", "", { "dependencies": { "type-fest": "^5.0.0" } }, "sha512-MVUtAugQMOff5RnBy2d9N31iG0lNwg1qAoAOn7pOK5wf94WIaE3My2p3uwTQuvS2AcqchkcR3bHByjaM0mmi7Q=="],
"dotenv": ["dotenv@17.4.1", "", {}, "sha512-k8DaKGP6r1G30Lx8V4+pCsLzKr8vLmV2paqEj1Y55GdAgJuIqpRp5FfajGF8KtwMxCz9qJc6wUIJnm053d/WCw=="], "dotenv": ["dotenv@17.4.2", "", {}, "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw=="],
"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=="], "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=="],
@ -1105,7 +1120,7 @@
"lru.min": ["lru.min@1.1.4", "", {}, "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA=="], "lru.min": ["lru.min@1.1.4", "", {}, "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA=="],
"lucide-react": ["lucide-react@1.7.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-yI7BeItCLZJTXikmK4KNUGCKoGzSvbKlfCvw44bU4fXAL6v3gYS4uHD1jzsLkfwODYwI6Drw5Tu9Z5ulDe0TSg=="], "lucide-react": ["lucide-react@1.14.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-+1mdWcfSJVUsaTIjN9zoezmUhfXo5l0vP7ekBMPo3jcS/aIkxHnXqAPsByszMZx/Y8oQBRJxJx5xg+RH3urzxA=="],
"luxon": ["luxon@3.7.2", "", {}, "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew=="], "luxon": ["luxon@3.7.2", "", {}, "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew=="],
@ -1147,7 +1162,7 @@
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"msgpackr": ["msgpackr@1.11.5", "", { "optionalDependencies": { "msgpackr-extract": "^3.0.2" } }, "sha512-UjkUHN0yqp9RWKy0Lplhh+wlpdt9oQBYgULZOiFhV3VclSF1JnSQWZ5r9gORQlNYaUKQoR8itv7g7z1xDDuACA=="], "msgpackr": ["msgpackr@1.11.12", "", { "optionalDependencies": { "msgpackr-extract": "^3.0.2" } }, "sha512-RBdJ1Un7yGlXWajrkxcSa93nvQ0w4zBf60c0yYv7YtBelP8H2FA7XsfBbMHtXKXUMUxH7zV3Zuozh+kUQWhHvg=="],
"msgpackr-extract": ["msgpackr-extract@3.0.3", "", { "dependencies": { "node-gyp-build-optional-packages": "5.2.2" }, "optionalDependencies": { "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3", "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3", "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3", "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3", "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3", "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3" }, "bin": { "download-msgpackr-prebuilds": "bin/download-prebuilds.js" } }, "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA=="], "msgpackr-extract": ["msgpackr-extract@3.0.3", "", { "dependencies": { "node-gyp-build-optional-packages": "5.2.2" }, "optionalDependencies": { "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3", "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3", "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3", "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3", "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3", "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3" }, "bin": { "download-msgpackr-prebuilds": "bin/download-prebuilds.js" } }, "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA=="],
@ -1161,11 +1176,11 @@
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
"nanostores": ["nanostores@1.2.0", "", {}, "sha512-F0wCzbsH80G7XXo0Jd9/AVQC7ouWY6idUCTnMwW5t/Rv9W8qmO6endavDwg7TNp5GbugwSukFMVZqzPSrSMndg=="], "nanostores": ["nanostores@1.3.0", "", {}, "sha512-XPUa/jz+P1oJvN9VBxw4L9MtdFfaH3DAryqPssqhb2kXjmb9npz0dly6rCsgFWOPr4Yg9mTfM3MDZgZZ+7A3lA=="],
"negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], "negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="],
"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": ["next@16.2.3", "", { "dependencies": { "@next/env": "16.2.3", "@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.3", "@next/swc-darwin-x64": "16.2.3", "@next/swc-linux-arm64-gnu": "16.2.3", "@next/swc-linux-arm64-musl": "16.2.3", "@next/swc-linux-x64-gnu": "16.2.3", "@next/swc-linux-x64-musl": "16.2.3", "@next/swc-win32-arm64-msvc": "16.2.3", "@next/swc-win32-x64-msvc": "16.2.3", "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-9V3zV4oZFza3PVev5/poB9g0dEafVcgNyQ8eTRop8GvxZjV2G15FC5ARuG1eFD42QgeYkzJBJzHghNP8Ad9xtA=="],
"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=="], "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=="],
@ -1177,11 +1192,13 @@
"node-fetch-native": ["node-fetch-native@1.6.7", "", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="], "node-fetch-native": ["node-fetch-native@1.6.7", "", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="],
"node-gyp-build": ["node-gyp-build@4.8.4", "", { "bin": { "node-gyp-build": "bin.js", "node-gyp-build-optional": "optional.js", "node-gyp-build-test": "build-test.js" } }, "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ=="],
"node-gyp-build-optional-packages": ["node-gyp-build-optional-packages@5.2.2", "", { "dependencies": { "detect-libc": "^2.0.1" }, "bin": { "node-gyp-build-optional-packages": "bin.js", "node-gyp-build-optional-packages-optional": "optional.js", "node-gyp-build-optional-packages-test": "build-test.js" } }, "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw=="], "node-gyp-build-optional-packages": ["node-gyp-build-optional-packages@5.2.2", "", { "dependencies": { "detect-libc": "^2.0.1" }, "bin": { "node-gyp-build-optional-packages": "bin.js", "node-gyp-build-optional-packages-optional": "optional.js", "node-gyp-build-optional-packages-test": "build-test.js" } }, "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw=="],
"node-releases": ["node-releases@2.0.36", "", {}, "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA=="], "node-releases": ["node-releases@2.0.36", "", {}, "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA=="],
"nodemailer": ["nodemailer@8.0.5", "", {}, "sha512-0PF8Yb1yZuQfQbq+5/pZJrtF6WQcjTd5/S4JOHs9PGFxuTqoB/icwuB44pOdURHJbRKX1PPoJZtY7R4VUoCC8w=="], "nodemailer": ["nodemailer@8.0.7", "", {}, "sha512-pkjE4mkBzQjdJT4/UmlKl3pX0rC9fZmjh7c6C9o7lv66Ac6w9WCnzPzhbPNxwZAzlF4mdq4CSWB5+FbK6FWCow=="],
"normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="],
@ -1305,13 +1322,13 @@
"rc9": ["rc9@2.1.2", "", { "dependencies": { "defu": "^6.1.4", "destr": "^2.0.3" } }, "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg=="], "rc9": ["rc9@2.1.2", "", { "dependencies": { "defu": "^6.1.4", "destr": "^2.0.3" } }, "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg=="],
"react": ["react@19.2.4", "", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="], "react": ["react@19.2.5", "", {}, "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA=="],
"react-dom": ["react-dom@19.2.4", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.4" } }, "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ=="], "react-dom": ["react-dom@19.2.5", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.5" } }, "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag=="],
"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-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.1", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-RhwBoy2ygeVZje+C+bwJ8g0NjTdBmDlJvAUHTxRjTmSUKPYsKfMphkS2sgEMotsY03bP358yEYlnUeZy//D9Ig=="], "react-hook-form": ["react-hook-form@7.75.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-Ovv94H+0p3sJ7B9B5QxPuCP1u8V/cHuVGyH55cSwodYDtoJwK+fqk3vjfIgSX59I2U/bU4z0nRJ9HMLpNiWEmw=="],
"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=="], "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=="],
@ -1379,7 +1396,7 @@
"setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="],
"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=="], "shadcn": ["shadcn@4.7.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-70fwnesNrY1GgeD7Kdzn+3SsYeyfibm8immsA5L68+OusoPTvYF01oWExl8/latKpMpvVXcbgdbbE6VFBJQ38w=="],
"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=="], "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=="],
@ -1461,7 +1478,7 @@
"tailwind-merge": ["tailwind-merge@3.5.0", "", {}, "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A=="], "tailwind-merge": ["tailwind-merge@3.5.0", "", {}, "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A=="],
"tailwindcss": ["tailwindcss@4.2.2", "", {}, "sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q=="], "tailwindcss": ["tailwindcss@4.2.4", "", {}, "sha512-HhKppgO81FQof5m6TEnuBWCZGgfRAWbaeOaGT00KOy/Pf/j6oUihdvBpA7ltCeAvZpFhW3j0PTclkxsd4IXYDA=="],
"tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="], "tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="],
@ -1495,15 +1512,15 @@
"tweetnacl-util": ["tweetnacl-util@0.15.1", "", {}, "sha512-RKJBIj8lySrShN4w6i/BonWp2Z/uxwC3h4y7xsRrpP59ZboCd0GpEVsOnMDYLMmKBpYhb5TgHzZXy7wTfYFBRw=="], "tweetnacl-util": ["tweetnacl-util@0.15.1", "", {}, "sha512-RKJBIj8lySrShN4w6i/BonWp2Z/uxwC3h4y7xsRrpP59ZboCd0GpEVsOnMDYLMmKBpYhb5TgHzZXy7wTfYFBRw=="],
"type-fest": ["type-fest@5.4.4", "", { "dependencies": { "tagged-tag": "^1.0.0" } }, "sha512-JnTrzGu+zPV3aXIUhnyWJj4z/wigMsdYajGLIYakqyOW1nPllzXEJee0QQbHj+CTIQtXGlAjuK0UY+2xTyjVAw=="], "type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="],
"type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="], "type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="],
"typescript": ["typescript@6.0.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ=="], "typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="],
"uint8array-extras": ["uint8array-extras@1.5.0", "", {}, "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A=="], "uint8array-extras": ["uint8array-extras@1.5.0", "", {}, "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A=="],
"undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], "undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="],
"unicorn-magic": ["unicorn-magic@0.3.0", "", {}, "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA=="], "unicorn-magic": ["unicorn-magic@0.3.0", "", {}, "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA=="],
@ -1523,7 +1540,7 @@
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
"uuid": ["uuid@11.1.0", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A=="], "uuid": ["uuid@14.0.0", "", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg=="],
"valibot": ["valibot@1.2.0", "", { "peerDependencies": { "typescript": ">=5" }, "optionalPeers": ["typescript"] }, "sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg=="], "valibot": ["valibot@1.2.0", "", { "peerDependencies": { "typescript": ">=5" }, "optionalPeers": ["typescript"] }, "sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg=="],
@ -1569,7 +1586,7 @@
"zeptomatch": ["zeptomatch@2.1.0", "", { "dependencies": { "grammex": "^3.1.11", "graphmatch": "^1.1.0" } }, "sha512-KiGErG2J0G82LSpniV0CtIzjlJ10E04j02VOudJsPyPwNZgGnRKQy7I1R7GMyg/QswnE4l7ohSGrQbQbjXPPDA=="], "zeptomatch": ["zeptomatch@2.1.0", "", { "dependencies": { "grammex": "^3.1.11", "graphmatch": "^1.1.0" } }, "sha512-KiGErG2J0G82LSpniV0CtIzjlJ10E04j02VOudJsPyPwNZgGnRKQy7I1R7GMyg/QswnE4l7ohSGrQbQbjXPPDA=="],
"zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], "zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="],
"zod-to-json-schema": ["zod-to-json-schema@3.25.1", "", { "peerDependencies": { "zod": "^3.25 || ^4" } }, "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA=="], "zod-to-json-schema": ["zod-to-json-schema@3.25.1", "", { "peerDependencies": { "zod": "^3.25 || ^4" } }, "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA=="],
@ -1601,6 +1618,10 @@
"@babel/traverse/@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/traverse/@babel/parser": ["@babel/parser@7.29.0", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww=="],
"@better-auth/core/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
"@better-auth/utils/@noble/hashes": ["@noble/hashes@2.0.1", "", {}, "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw=="],
"@dotenvx/dotenvx/commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="], "@dotenvx/dotenvx/commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="],
"@dotenvx/dotenvx/dotenv": ["dotenv@17.3.1", "", {}, "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA=="], "@dotenvx/dotenvx/dotenv": ["dotenv@17.3.1", "", {}, "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA=="],
@ -1613,6 +1634,8 @@
"@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="], "@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="],
"@modelcontextprotocol/sdk/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
"@noble/curves/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], "@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/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=="],
@ -1623,6 +1646,8 @@
"@prisma/get-platform/@prisma/debug": ["@prisma/debug@7.2.0", "", {}, "sha512-YSGTiSlBAVJPzX4ONZmMotL+ozJwQjRmZweQNIq/ER0tQJKJynNkRB3kyvt37eOfsbMCXk3gnLF6J9OJ4QWftw=="], "@prisma/get-platform/@prisma/debug": ["@prisma/debug@7.2.0", "", {}, "sha512-YSGTiSlBAVJPzX4ONZmMotL+ozJwQjRmZweQNIq/ER0tQJKJynNkRB3kyvt37eOfsbMCXk3gnLF6J9OJ4QWftw=="],
"@react-email/tailwind/tailwindcss": ["tailwindcss@4.2.2", "", {}, "sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q=="],
"@tailwindcss/node/jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], "@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=="], "@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=="],
@ -1639,11 +1664,17 @@
"@types/cors/@types/node": ["@types/node@20.19.35", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-Uarfe6J91b9HAUXxjvSOdiO2UPOKLm07Q1oh0JHxoZ1y8HoqxDAu3gVrsrOHeiio0kSsoVBt4wFrKOm0dKxVPQ=="], "@types/cors/@types/node": ["@types/node@20.19.35", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-Uarfe6J91b9HAUXxjvSOdiO2UPOKLm07Q1oh0JHxoZ1y8HoqxDAu3gVrsrOHeiio0kSsoVBt4wFrKOm0dKxVPQ=="],
"@types/nodemailer/@types/node": ["@types/node@25.5.2", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-tO4ZIRKNC+MDWV4qKVZe3Ql/woTnmHDr5JD8UI5hn2pwBrHEwOEMZK7WlNb5RKB6EoJ02gwmQS9OrjuFnZYdpg=="],
"@types/pg/@types/node": ["@types/node@25.5.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw=="], "@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=="], "accepts/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
"bun-types/@types/node": ["@types/node@25.5.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw=="], "auth/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
"better-auth/@noble/hashes": ["@noble/hashes@2.0.1", "", {}, "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw=="],
"better-auth/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
"c12/chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="], "c12/chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="],
@ -1659,6 +1690,8 @@
"cosmiconfig/env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="], "cosmiconfig/env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="],
"dot-prop/type-fest": ["type-fest@5.4.4", "", { "dependencies": { "tagged-tag": "^1.0.0" } }, "sha512-JnTrzGu+zPV3aXIUhnyWJj4z/wigMsdYajGLIYakqyOW1nPllzXEJee0QQbHj+CTIQtXGlAjuK0UY+2xTyjVAw=="],
"eciesjs/@noble/ciphers": ["@noble/ciphers@1.3.0", "", {}, "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw=="], "eciesjs/@noble/ciphers": ["@noble/ciphers@1.3.0", "", {}, "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw=="],
"eciesjs/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], "eciesjs/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="],
@ -1677,6 +1710,8 @@
"minio/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], "minio/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
"msw/type-fest": ["type-fest@5.4.4", "", { "dependencies": { "tagged-tag": "^1.0.0" } }, "sha512-JnTrzGu+zPV3aXIUhnyWJj4z/wigMsdYajGLIYakqyOW1nPllzXEJee0QQbHj+CTIQtXGlAjuK0UY+2xTyjVAw=="],
"next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="], "next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="],
"npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="], "npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="],
@ -1793,6 +1828,10 @@
"@types/cors/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], "@types/cors/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
"@types/nodemailer/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
"@types/pg/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
"accepts/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], "accepts/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
"c12/chokidar/readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="], "c12/chokidar/readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="],

View file

@ -8,6 +8,13 @@
"when": 1772741645429, "when": 1772741645429,
"tag": "0000_lazy_slyde", "tag": "0000_lazy_slyde",
"breakpoints": true "breakpoints": true
},
{
"idx": 1,
"version": "7",
"when": 1746392520000,
"tag": "0001_otk_schema_fix",
"breakpoints": true
} }
] ]
} }

View file

@ -7,6 +7,9 @@ const nextConfig: NextConfig = {
reactCompiler: true, reactCompiler: true,
allowedDevOrigins, allowedDevOrigins,
output: "standalone", output: "standalone",
experimental: {
webpackMemoryOptimizations: true
}
}; };
export default nextConfig; export default nextConfig;

View file

@ -10,7 +10,7 @@
} }
], ],
"license": "AGPL-3.0", "license": "AGPL-3.0",
"version": "0.1.0", "version": "0.1.1",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "cross-env NODE_ENV=development FEDERATION_ALLOW_PRIVATE_URLS=true tsx src/server.ts", "dev": "cross-env NODE_ENV=development FEDERATION_ALLOW_PRIVATE_URLS=true tsx src/server.ts",
@ -23,6 +23,7 @@
"test:discover": "cross-env NODE_ENV=test playwright test tests/discover.test.ts", "test:discover": "cross-env NODE_ENV=test playwright test tests/discover.test.ts",
"test:attacks": "cross-env NODE_ENV=test playwright test tests/attacks.test.ts", "test:attacks": "cross-env NODE_ENV=test playwright test tests/attacks.test.ts",
"build": "next build", "build": "next build",
"build:matrix": "cd node_modules/@matrix-org/matrix-sdk-crypto-nodejs && node download-lib.js",
"start": "cross-env NODE_ENV=production node src/server.ts", "start": "cross-env NODE_ENV=production node src/server.ts",
"db:push": "drizzle-kit push", "db:push": "drizzle-kit push",
"db:migrate": "bun run db:push && bun run drizzle-kit migrate", "db:migrate": "bun run db:push && bun run drizzle-kit migrate",
@ -30,59 +31,64 @@
"db:update": "bun run db:generate && bun run db:push" "db:update": "bun run db:generate && bun run db:push"
}, },
"dependencies": { "dependencies": {
"@better-auth/drizzle-adapter": "^1.6.0", "@better-auth/drizzle-adapter": "^1.6.9",
"@hookform/resolvers": "^5.2.2", "@hookform/resolvers": "^5.2.2",
"@matrix-org/matrix-sdk-crypto-wasm": "^18.2.0",
"@nanostores/react": "^1.1.0", "@nanostores/react": "^1.1.0",
"@react-email/components": "1.0.11", "@react-email/components": "1.0.12",
"better-auth": "^1.6.0", "@scure/bip39": "^2.2.0",
"bullmq": "^5.73.1", "@signalapp/libsignal-client": "^0.92.2",
"base58-js": "^3.0.3",
"better-auth": "^1.6.9",
"bullmq": "^5.76.5",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"debug": "^4.4.3", "debug": "^4.4.3",
"dexie": "^4.4.2", "dexie": "^4.4.2",
"dexie-react-hooks": "^4.4.0", "dexie-react-hooks": "^4.4.0",
"dotenv": "^17.4.1", "dotenv": "^17.4.2",
"drizzle-orm": "^0.45.2", "drizzle-orm": "^0.45.2",
"framer-motion": "^12.38.0", "framer-motion": "^12.38.0",
"ioredis": "^5.10.1", "ioredis": "^5.10.1",
"lucide-react": "^1.7.0", "lucide-react": "^1.14.0",
"minio": "^8.0.7", "minio": "^8.0.7",
"nanostores": "^1.2.0", "nanostores": "^1.3.0",
"next": "16.2.2", "next": "16.2.3",
"next-themes": "^0.4.6", "next-themes": "^0.4.6",
"nodemailer": "^8.0.5", "nodemailer": "^8.0.7",
"pg": "^8.20.0", "pg": "^8.20.0",
"radix-ui": "^1.4.3", "radix-ui": "^1.4.3",
"react": "19.2.4", "react": "19.2.5",
"react-dom": "19.2.4", "react-dom": "19.2.5",
"react-hook-form": "^7.72.1", "react-hook-form": "^7.75.0",
"socket.io": "^4.8.3", "socket.io": "^4.8.3",
"socket.io-client": "^4.8.3", "socket.io-client": "^4.8.3",
"sonner": "^2.0.7", "sonner": "^2.0.7",
"tailwind-merge": "^3.5.0", "tailwind-merge": "^3.5.0",
"tweetnacl": "^1.0.3", "tweetnacl": "^1.0.3",
"tweetnacl-util": "^0.15.1", "tweetnacl-util": "^0.15.1",
"zod": "^4.3.6" "uuid": "^14.0.0",
"zod": "^4.4.3"
}, },
"devDependencies": { "devDependencies": {
"@tailwindcss/postcss": "^4.2.2", "@tailwindcss/postcss": "^4.2.4",
"@types/bun": "^1.3.11", "@types/bun": "^1.3.13",
"@types/debug": "^4.1.13", "@types/debug": "^4.1.13",
"@types/node": "^25.5.2", "@types/node": "^25.6.0",
"@types/nodemailer": "^8.0.0", "@types/nodemailer": "^8.0.0",
"@types/pg": "^8.20.0", "@types/pg": "^8.20.0",
"@types/react": "^19.2.14", "@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3", "@types/react-dom": "^19.2.3",
"auth": "^1.6.0", "auth": "^1.6.9",
"babel-plugin-react-compiler": "1.0.0", "babel-plugin-react-compiler": "1.0.0",
"cross-env": "^10.1.0", "cross-env": "^10.1.0",
"drizzle-kit": "^0.31.10", "drizzle-kit": "^0.31.10",
"react-email": "5.2.10", "react-email": "5.2.10",
"shadcn": "^4.2.0", "shadcn": "^4.7.0",
"tailwindcss": "^4.2.2", "tailwindcss": "^4.2.4",
"tsx": "^4.21.0", "tsx": "^4.21.0",
"tw-animate-css": "^1.4.0", "tw-animate-css": "^1.4.0",
"typescript": "^6.0.2" "typescript": "^6.0.3"
}, },
"ignoreScripts": [ "ignoreScripts": [
"sharp", "sharp",

View file

@ -32,7 +32,7 @@ interface FedKeys {
encryptionSecretKey: string; encryptionSecretKey: string;
} }
function generateKeypair(): FedKeys { function generateEnvKeyPair(): FedKeys {
const signing = nacl.sign.keyPair(); const signing = nacl.sign.keyPair();
const encryption = nacl.box.keyPair(); const encryption = nacl.box.keyPair();
return { return {
@ -131,7 +131,7 @@ if (resumeIdx !== -1) {
process.exit(1); process.exit(1);
} }
} else { } else {
newFedKeys = generateKeypair(); newFedKeys = generateEnvKeyPair();
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------

View file

@ -5,12 +5,23 @@ import { authClient } from "@/lib/auth-client";
import { useState } from "react"; import { useState } from "react";
export function PostTestForm() { export function PostTestForm() {
const { data: session } = authClient.useSession();
const [text, setText] = useState(""); const [text, setText] = useState("");
const [files, setFiles] = useState<File[]>([]); const [files, setFiles] = useState<File[]>([]);
const [password, setPassword] = useState("");
const [status, setStatus] = useState<string | null>(null); const [status, setStatus] = useState<string | null>(null);
const handleSubmit = async () => { const handleSubmit = async () => {
setStatus("Submitting..."); if (!session?.user.id) {
setStatus("Not signed in.");
return;
}
if (!password) {
setStatus("Enter your master password to unlock the signing key.");
return;
}
setStatus("Signing & submitting...");
try { try {
const content: { type: "text" | "image"; value: string | File }[] = []; const content: { type: "text" | "image"; value: string | File }[] = [];
@ -27,7 +38,7 @@ export function PostTestForm() {
return; return;
} }
const result = await authClient.createPost(content); const result = await authClient.createPost(content, session.user.id, password);
setStatus(`Done: ${JSON.stringify(result)}`); setStatus(`Done: ${JSON.stringify(result)}`);
} catch (err) { } catch (err) {
setStatus(`Error: ${err instanceof Error ? err.message : String(err)}`); setStatus(`Error: ${err instanceof Error ? err.message : String(err)}`);
@ -90,6 +101,19 @@ export function PostTestForm() {
)} )}
</div> </div>
<div style={{ marginBottom: 12 }}>
<label style={{ display: "block", marginBottom: 4, fontWeight: 600 }}>
Master password (unlocks signing key)
</label>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="••••••••••••"
style={{ width: "100%", padding: 8, fontSize: 14 }}
/>
</div>
<button <button
onClick={handleSubmit} onClick={handleSubmit}
style={{ style={{

View file

@ -0,0 +1,56 @@
import { discoverAndRegister, DiscoveryError } from "@/lib/federation/registry";
import { assertSafeUrl, UrlGuardError } from "@/lib/federation/url-guard";
import createDebug from "debug";
import { NextRequest, NextResponse } from "next/server";
import { z } from "zod";
const debug = createDebug("app:api:dev:relay-discover");
const bodySchema = z.object({
target: z.url(),
});
/**
* Dev-only: browser calls same origin; server runs the full mutual-registration
* flow (GET keys register locally POST REGISTER process echo) so that
* both sides end up knowing each other, mirroring the production path.
*/
export async function POST(request: NextRequest) {
if (process.env.NODE_ENV === "production") {
return NextResponse.json({ error: "Not found" }, { status: 404 });
}
let json: unknown;
try {
json = await request.json();
} catch {
return NextResponse.json({ error: "Invalid JSON" }, { status: 400 });
}
const parsed = bodySchema.safeParse(json);
if (!parsed.success) {
return NextResponse.json({ error: parsed.error.message }, { status: 400 });
}
const { target } = parsed.data;
try {
assertSafeUrl(target);
} catch (err) {
if (err instanceof UrlGuardError) {
return NextResponse.json({ error: err.message }, { status: 400 });
}
throw err;
}
try {
await discoverAndRegister(target);
debug("relay-discover: mutual registration with %s complete", target);
return NextResponse.json({ message: "Server registered successfully" });
} catch (err) {
debug("relay-discover failed: %o", err);
if (err instanceof DiscoveryError) {
return NextResponse.json({ error: err.message }, { status: 502 });
}
throw err;
}
}

View file

@ -46,12 +46,23 @@ export function SignUpForm({
return; return;
} }
const username = email.split("@")[0];
// Check if username is already taken
const check = await authClient.isUsernameAvailable({ username });
if (!check) {
form.setError("root", { message: "Username is already taken" });
toast.error("Username is already taken");
return;
}
await authClient.signUp.email({ await authClient.signUp.email({
email, email,
password, password,
name: email.split("@")[0], name: username,
username,
}, { }, {
onSuccess: () => { onSuccess: (res) => {
console.debug(JSON.stringify(res, null, 2));
console.debug(`[SignUpForm] registration successful for ${email}`); console.debug(`[SignUpForm] registration successful for ${email}`);
toast.success("Registration successful, please check your email for the verification link!"); toast.success("Registration successful, please check your email for the verification link!");
onSuccess(email); onSuccess(email);

View file

@ -110,7 +110,7 @@ function AuthPageContent() {
<div className="flex flex-col items-center gap-1"> <div className="flex flex-col items-center gap-1">
<span className="font-mono text-[10px] text-muted-foreground tracking-[0.25em] uppercase w-full">© 2026 Sipher. All rights reserved.</span> <span className="font-mono text-[10px] text-muted-foreground tracking-[0.25em] uppercase w-full">© 2026 Sipher. All rights reserved.</span>
<span className="font-mono text-[10px] text-muted-foreground tracking-[0.25em] uppercase w-full"> <span className="font-mono text-[10px] text-muted-foreground tracking-[0.25em] uppercase w-full">
Refuse to be dominated. Be free. <Link href="" target="_blank" className="text-primary underline">Create your own network.</Link> Refuse to be dominated. Be free. <Link href={process.env.PUBLIC_GIT_URL ?? ""} target="_blank" className="text-primary underline">Create your own network.</Link>
</span> </span>
</div> </div>
</div> </div>

View file

@ -1,3 +1,4 @@
import { TooltipProvider } from "@/components/ui/tooltip";
import type { Metadata } from "next"; import type { Metadata } from "next";
import { ThemeProvider } from "next-themes"; import { ThemeProvider } from "next-themes";
import { Bebas_Neue, DM_Sans, Space_Mono } from "next/font/google"; import { Bebas_Neue, DM_Sans, Space_Mono } from "next/font/google";
@ -46,8 +47,10 @@ export default function RootLayout({
enableSystem enableSystem
disableTransitionOnChange disableTransitionOnChange
> >
<Toaster /> <TooltipProvider>
{children} <Toaster />
{children}
</TooltipProvider>
</ThemeProvider> </ThemeProvider>
</body> </body>
</html> </html>

25
src/app/manifest.ts Normal file
View file

@ -0,0 +1,25 @@
import type { MetadataRoute } from 'next'
export default function manifest(): MetadataRoute.Manifest {
return {
name: 'Silent Whisper',
short_name: 'SiPher',
description: 'A federated social media platform for the modern age.',
start_url: '/',
display: 'standalone',
background_color: '#080808',
theme_color: '#080808',
icons: [
{
src: '/logo/sipher.svg',
sizes: '192x192',
type: 'image/svg+xml',
},
{
src: '/logo/sipher.svg',
sizes: '512x512',
type: 'image/svg+xml',
},
],
}
}

View file

@ -1,23 +1,31 @@
"use server"; "use server";
import CreateIdentity from "@/components/main/CreateIdentity";
import { auth } from "@/lib/auth"; import { auth } from "@/lib/auth";
import { headers } from "next/headers"; import { headers } from "next/headers";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { PostTestForm } from "./PostTestForm"; import { PostTestForm } from "./PostTestForm";
export default async function Home() { export default async function Home() {
const reqHeaders = await headers();
const session = await auth.api.getSession({ const session = await auth.api.getSession({ headers: reqHeaders });
headers: await headers(),
});
if (!session) redirect(`/auth`); if (!session) redirect(`/auth`);
// Server components can't talk to the browser-side `authClient`, so we hit
// the plugin endpoint via `auth.api`. This only tells us whether the
// identity is registered remotely; the local Dexie half is verified inside
// `CreateIdentity` (client component) when needed.
const result = await auth.api.checkIdentity({ headers: reqHeaders });
const hasIdentity = "exists" in result && result.exists;
if (!hasIdentity) {
console.debug(`[Home] user ${session.user.id} has no identity, showing create identity modal`);
return <CreateIdentity />;
}
return ( return (
<> <>
<PostTestForm /> <PostTestForm />
</> </>
); );
} }

View file

@ -0,0 +1,207 @@
"use client";
import { authClient } from "@/lib/auth-client";
import { zodResolver } from "@hookform/resolvers/zod";
import { Eye, EyeOff, KeyRound, Loader2, TriangleAlert } from "lucide-react";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "../ui/accordion";
import { Button } from "../ui/button";
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "../ui/dialog";
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "../ui/form";
import { Input } from "../ui/input";
const createIdentityFormSchema = z.object({
password: z
.string()
.min(8, "Password must be at least 8 characters")
.max(64, "Password cannot exceed 64 characters")
.refine((val) => /[A-Z]/.test(val), {
message: "Must contain at least one uppercase letter",
})
.refine((val) => /[a-z]/.test(val), {
message: "Must contain at least one lowercase letter",
})
.refine((val) => /[0-9]/.test(val), {
message: "Must contain at least one number",
})
.refine((val) => /[^a-zA-Z0-9]/.test(val), {
message: "Must contain at least one special character",
})
});
const requirements = [
{ label: "864 characters", test: (v: string) => v.length >= 8 && v.length <= 64 },
{ label: "Uppercase letter", test: (v: string) => /[A-Z]/.test(v) },
{ label: "Lowercase letter", test: (v: string) => /[a-z]/.test(v) },
{ label: "Number", test: (v: string) => /[0-9]/.test(v) },
{ label: "Special character", test: (v: string) => /[^a-zA-Z0-9]/.test(v) },
];
export default function CreateIdentity() {
const [isOpen, setIsOpen] = useState(true);
const [showPassword, setShowPassword] = useState(false);
const { data: session } = authClient.useSession();
const router = useRouter();
const form = useForm<z.infer<typeof createIdentityFormSchema>>({
resolver: zodResolver(createIdentityFormSchema),
defaultValues: {
password: "",
},
});
const password = form.watch("password");
const passwordRequirementsMet = requirements.every((req) => req.test(password));
async function onSubmit(values: z.infer<typeof createIdentityFormSchema>) {
const userId = session?.user.id;
if (!userId) return;
try {
const { mnemonic } = await authClient.createOvenIdentity(userId, values.password);
console.log("[CreateIdentity]", mnemonic);
toast.success("Identity created successfully.");
setIsOpen(false);
router.refresh();
} catch (err) {
console.error("[CreateIdentity]", err);
toast.error("Failed to create identity. Please try again.");
}
}
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogContent onInteractOutside={(e) => e.preventDefault()} className="sm:max-w-md border-border bg-card p-0 overflow-hidden [&>button]:hidden">
<div className="px-6 pt-6 pb-2 border-b border-border/60">
<div className="flex items-center gap-3 mb-3">
<div className="flex items-center justify-center w-8 h-8 rounded bg-primary/10 border border-primary/20">
<KeyRound className="w-4 h-4 text-primary" />
</div>
<span className="font-mono text-[10px] text-muted-foreground tracking-[0.25em] uppercase">
Identity Setup
</span>
</div>
<DialogHeader className="text-left space-y-1">
<DialogTitle className="font-display text-3xl tracking-[0.06em] text-foreground">
Create your Sipher identity
</DialogTitle>
<p className="text-sm text-muted-foreground leading-relaxed">
This password encrypts your local identity key. It never leaves your device. <span className="font-bold">DO NOT FORGET THIS PASSWORD.</span>
</p>
<p className="text-sm text-muted-foreground leading-relaxed">
You may use the same password for your Sipher account and your identity, although it is not recommended.
</p>
</DialogHeader>
</div>
<div className="px-6 py-5">
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-5">
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem className="space-y-1.5">
<FormLabel className="font-mono text-[11px] tracking-[0.15em] uppercase text-muted-foreground">
Master Password
</FormLabel>
<FormControl>
<div className="relative">
<Input
{...field}
type={showPassword ? "text" : "password"}
className="h-11 text-base bg-background border-border/60 pr-10 focus-visible:ring-primary/50 focus-visible:border-primary/50"
placeholder="••••••••••••"
/>
<button
type="button"
onClick={() => setShowPassword((v) => !v)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground transition-colors"
tabIndex={-1}
>
{showPassword
? <EyeOff className="w-4 h-4" />
: <Eye className="w-4 h-4" />
}
</button>
</div>
</FormControl>
<FormMessage className="font-mono text-[10px] tracking-wide" />
</FormItem>
)}
/>
{password.length > 0 && (
<div className="space-y-1.5">
<span className="font-mono text-[10px] tracking-[0.15em] uppercase text-muted-foreground">
Requirements
</span>
<ul className="grid grid-cols-2 gap-x-4 gap-y-1">
{requirements.map((req) => {
const met = req.test(password);
return (
<li
key={req.label}
className={`font-mono text-[10px] tracking-wide flex items-center gap-1.5 transition-colors ${met ? "text-primary" : "text-muted-foreground/60"}`}
>
<span className={`inline-block w-1 h-1 rounded-full shrink-0 ${met ? "bg-primary" : "bg-border"}`} />
{req.label}
</li>
);
})}
</ul>
</div>
)}
<Accordion type="single" collapsible className="border border-destructive/30 rounded bg-destructive/5">
<AccordionItem value="lost-password" className="border-none">
<AccordionTrigger className="px-3 py-2.5 hover:no-underline hover:bg-destructive/10 rounded transition-colors">
<span className="flex items-center gap-2 font-mono text-[10px] tracking-[0.15em] uppercase text-destructive/80">
<TriangleAlert className="w-3.5 h-3.5 shrink-0" />
What if I lose my password?
</span>
</AccordionTrigger>
<AccordionContent className="px-3 pb-3 pt-0">
<ul className="space-y-1.5 font-mono text-[10px] tracking-wide text-muted-foreground leading-relaxed">
<li className="flex gap-2">
<span className="text-destructive/60 shrink-0"></span>
Your identity key is encrypted locally with this password. There is no recovery mechanism.
</li>
<li className="flex gap-2">
<span className="text-destructive/60 shrink-0"></span>
Losing it means permanent loss of access to your encrypted messages and posts.
</li>
<li className="flex gap-2">
<span className="text-destructive/60 shrink-0"></span>
Store it somewhere safe a password manager or written offline.
</li>
<li className="flex gap-2">
<span className="text-destructive/60 shrink-0"></span>
Losing your identity means that all your messages are permanently lost and your old posts won't hold a valid signature.
</li>
</ul>
</AccordionContent>
</AccordionItem>
</Accordion>
<Button
type="submit"
className="w-full h-11 font-mono text-[11px] tracking-[0.2em] uppercase"
disabled={form.formState.isSubmitting || !passwordRequirementsMet}
>
{form.formState.isSubmitting
? <Loader2 className="w-4 h-4 animate-spin" />
: "Generate Identity"
}
</Button>
</form>
</Form>
</div>
</DialogContent>
</Dialog>
)
}

View file

@ -0,0 +1,66 @@
"use client"
import * as React from "react"
import { ChevronDownIcon } from "lucide-react"
import { Accordion as AccordionPrimitive } from "radix-ui"
import { cn } from "@/lib/utils"
function Accordion({
...props
}: React.ComponentProps<typeof AccordionPrimitive.Root>) {
return <AccordionPrimitive.Root data-slot="accordion" {...props} />
}
function AccordionItem({
className,
...props
}: React.ComponentProps<typeof AccordionPrimitive.Item>) {
return (
<AccordionPrimitive.Item
data-slot="accordion-item"
className={cn("border-b last:border-b-0", className)}
{...props}
/>
)
}
function AccordionTrigger({
className,
children,
...props
}: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {
return (
<AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Trigger
data-slot="accordion-trigger"
className={cn(
"flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180",
className
)}
{...props}
>
{children}
<ChevronDownIcon className="pointer-events-none size-4 shrink-0 translate-y-0.5 text-muted-foreground transition-transform duration-200" />
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
)
}
function AccordionContent({
className,
children,
...props
}: React.ComponentProps<typeof AccordionPrimitive.Content>) {
return (
<AccordionPrimitive.Content
data-slot="accordion-content"
className="overflow-hidden text-sm data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
{...props}
>
<div className={cn("pt-0 pb-4", className)}>{children}</div>
</AccordionPrimitive.Content>
)
}
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }

View file

@ -0,0 +1,35 @@
"use client"
import * as React from "react"
import { Switch as SwitchPrimitive } from "radix-ui"
import { cn } from "@/lib/utils"
function Switch({
className,
size = "default",
...props
}: React.ComponentProps<typeof SwitchPrimitive.Root> & {
size?: "sm" | "default"
}) {
return (
<SwitchPrimitive.Root
data-slot="switch"
data-size={size}
className={cn(
"peer group/switch inline-flex shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-[1.15rem] data-[size=default]:w-8 data-[size=sm]:h-3.5 data-[size=sm]:w-6 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input dark:data-[state=unchecked]:bg-input/80",
className
)}
{...props}
>
<SwitchPrimitive.Thumb
data-slot="switch-thumb"
className={cn(
"pointer-events-none block rounded-full bg-background ring-0 transition-transform group-data-[size=default]/switch:size-4 group-data-[size=sm]/switch:size-3 data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0 dark:data-[state=checked]:bg-primary-foreground dark:data-[state=unchecked]:bg-foreground"
)}
/>
</SwitchPrimitive.Root>
)
}
export { Switch }

View file

@ -0,0 +1,57 @@
"use client"
import * as React from "react"
import { Tooltip as TooltipPrimitive } from "radix-ui"
import { cn } from "@/lib/utils"
function TooltipProvider({
delayDuration = 0,
...props
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
return (
<TooltipPrimitive.Provider
data-slot="tooltip-provider"
delayDuration={delayDuration}
{...props}
/>
)
}
function Tooltip({
...props
}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
return <TooltipPrimitive.Root data-slot="tooltip" {...props} />
}
function TooltipTrigger({
...props
}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
}
function TooltipContent({
className,
sideOffset = 0,
children,
...props
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
return (
<TooltipPrimitive.Portal>
<TooltipPrimitive.Content
data-slot="tooltip-content"
sideOffset={sideOffset}
className={cn(
"z-50 w-fit origin-(--radix-tooltip-content-transform-origin) animate-in rounded-md bg-foreground px-3 py-1.5 text-xs text-balance text-background fade-in-0 zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95",
className
)}
{...props}
>
{children}
<TooltipPrimitive.Arrow className="z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px] bg-foreground fill-foreground" />
</TooltipPrimitive.Content>
</TooltipPrimitive.Portal>
)
}
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }

View file

@ -1,6 +1,6 @@
export async function register() { export async function register() {
if (process.env.NEXT_RUNTIME === 'nodejs') { if (process.env.NEXT_RUNTIME === 'nodejs') {
const { startFederationWorker } = await import('./lib/bull'); const { startFederationWorker } = await import('./lib/bull/worker');
startFederationWorker(); startFederationWorker();
} }
} }

View file

@ -1,6 +1,7 @@
import { twoFactorClient, usernameClient } from "better-auth/client/plugins"; import { twoFactorClient, usernameClient } from "better-auth/client/plugins";
import { createAuthClient } from "better-auth/react"; import { createAuthClient } from "better-auth/react";
import { sipherSocialClientPlugin } from "./plugins/client/social"; import { sipherOvenClientPlugin } from "./plugins/oven/client";
import { sipherSocialClientPlugin } from "./plugins/social/client/social";
export const authClient = createAuthClient({ export const authClient = createAuthClient({
fetchOptions: {}, fetchOptions: {},
@ -8,5 +9,6 @@ export const authClient = createAuthClient({
usernameClient(), usernameClient(),
twoFactorClient(), twoFactorClient(),
sipherSocialClientPlugin(), sipherSocialClientPlugin(),
sipherOvenClientPlugin(),
] ]
}) })

View file

@ -1,12 +1,13 @@
import { federation } from "@/plugins/server/federation"; import { federation } from "@/lib/plugins/federation/server/federation";
import { sipherSocial } from '@/plugins/server/social'; import { sipherOven } from "@/lib/plugins/oven/server/index";
import { sipherSocial } from '@/lib/plugins/social/server/social';
import { drizzleAdapter } from "@better-auth/drizzle-adapter"; import { drizzleAdapter } from "@better-auth/drizzle-adapter";
import { betterAuth } from "better-auth"; import { betterAuth } from "better-auth";
import { bearer, haveIBeenPwned, openAPI, testUtils, twoFactor, username } from "better-auth/plugins"; import { bearer, haveIBeenPwned, openAPI, testUtils, twoFactor, username } from "better-auth/plugins";
import db from "./db"; import db from "./db";
import * as schema from "./db/schema"; import * as schema from "./db/schema";
import EmailService from "./mail"; import EmailService from "./mail";
import minioClient from "./plugins/server/storage/minio.client"; import minioClient from "./plugins/storage/server/minio.client";
import getRedisClient from "./redis"; import getRedisClient from "./redis";
const isTest = process.env.NODE_ENV === "test"; const isTest = process.env.NODE_ENV === "test";
@ -73,6 +74,7 @@ const bAuth = betterAuth({
federation(), federation(),
openAPI(), openAPI(),
testUtils(), // TODO: Add a conditional plugin for test utils in development testUtils(), // TODO: Add a conditional plugin for test utils in development
sipherOven(),
bearer() bearer()
], ],
// This is disabled by default, but I'll keep this here for ease of mind. // This is disabled by default, but I'll keep this here for ease of mind.
@ -94,7 +96,7 @@ const bAuth = betterAuth({
required: false, required: false,
index: false, index: false,
enum: ["all", "followers", "none"] as const, enum: ["all", "followers", "none"] as const,
} },
} }
} }
}); });

View file

@ -1,4 +1,3 @@
export { getFederationQueue, getHealthCheckQueue, scheduleHealthCheck } from './queues'; export { getFederationQueue, getHealthCheckQueue, scheduleHealthCheck } from './queues';
export type { FederationDeliveryJob, HealthCheckJob } from './queues'; export type { FederationDeliveryJob, HealthCheckJob } from './queues';
export { startFederationWorker } from './worker';

View file

@ -61,6 +61,7 @@ export const twoFactor = pgTable(
userId: text("user_id") userId: text("user_id")
.notNull() .notNull()
.references(() => user.id, { onDelete: "cascade" }), .references(() => user.id, { onDelete: "cascade" }),
verified: boolean("verified").default(true),
}, },
(table) => [ (table) => [
index("twoFactor_secret_idx").on(table.secret), index("twoFactor_secret_idx").on(table.secret),
@ -83,6 +84,7 @@ export const posts = pgTable(
createdAt: timestamp("created_at").notNull(), createdAt: timestamp("created_at").notNull(),
federationUrl: text("federation_url"), federationUrl: text("federation_url"),
federationPostId: text("federation_post_id"), federationPostId: text("federation_post_id"),
authorSignature: text("author_signature"),
}, },
(table) => [ (table) => [
index("posts_federationUrl_idx").on(table.federationUrl), index("posts_federationUrl_idx").on(table.federationUrl),
@ -198,12 +200,37 @@ export const blacklistedServers = pgTable(
(table) => [index("blacklistedServers_serverUrl_idx").on(table.serverUrl)], (table) => [index("blacklistedServers_serverUrl_idx").on(table.serverUrl)],
); );
export const userIdentityKeys = pgTable("user_identity_keys", {
id: text("id").primaryKey(),
userId: text("user_id")
.notNull()
.unique()
.references(() => user.id, { onDelete: "cascade" }),
signingPublicKey: text("signing_public_key").notNull().unique(),
fingerprint: text("fingerprint").notNull().unique(),
createdAt: timestamp("created_at").notNull(),
updatedAt: timestamp("updated_at").notNull(),
});
export const olmDeviceKeys = pgTable("olm_device_keys", {
id: text("id").primaryKey(),
userId: text("user_id")
.notNull()
.references(() => user.id, { onDelete: "cascade" }),
deviceId: text("device_id").notNull().unique(),
bundleJson: text("bundle_json").notNull(),
createdAt: timestamp("created_at").notNull(),
updatedAt: timestamp("updated_at").notNull(),
});
export const userRelations = relations(user, ({ many }) => ({ export const userRelations = relations(user, ({ many }) => ({
accounts: many(account), accounts: many(account),
twoFactors: many(twoFactor), twoFactors: many(twoFactor),
postss: many(posts), postss: many(posts),
mutess: many(mutes), mutess: many(mutes),
blockss: many(blocks), blockss: many(blocks),
userIdentityKeys: many(userIdentityKeys),
olmDeviceKeyss: many(olmDeviceKeys),
})); }));
export const accountRelations = relations(account, ({ one }) => ({ export const accountRelations = relations(account, ({ one }) => ({
@ -281,3 +308,20 @@ export const serverRegistryRelations = relations(
followss: many(follows), followss: many(follows),
}), }),
); );
export const userIdentityKeysRelations = relations(
userIdentityKeys,
({ one }) => ({
user: one(user, {
fields: [userIdentityKeys.userId],
references: [user.id],
}),
}),
);
export const olmDeviceKeysRelations = relations(olmDeviceKeys, ({ one }) => ({
user: one(user, {
fields: [olmDeviceKeys.userId],
references: [user.id],
}),
}));

112
src/lib/dexie/index.ts Normal file
View file

@ -0,0 +1,112 @@
import Dexie, { type EntityTable } from "dexie";
/**
* Encrypted local identity, AES-GCM sealed with a PBKDF2-derived key from the
* user's master password.
*
* The plaintext is JSON of the form:
* {
* mnemonic: string, // BIP-39 recovery phrase
* fingerprint: string, // base58 of publicKey
* publicKey: number[], // 32-byte Ed25519 public key
* secretKey: number[], // 64-byte NaCl Ed25519 secret key
* }
*
* The secret key never leaves this record in plaintext: it is decrypted
* transiently inside `useSigningKey` (see `client/index.ts`), used for a
* single signing operation, then zeroed. The OlmMachine keeps its own
* separate IndexedDB-backed state managed by the rust-sdk crypto wasm bundle.
*/
export interface IdentityRecord {
/** Better Auth user ID — primary key. */
userId: string;
/** PBKDF2 salt (16 bytes, stored as number[]). */
salt: number[];
/** AES-GCM IV (12 bytes, stored as number[]). */
iv: number[];
/** AES-GCM ciphertext of the JSON payload described above. */
ciphertext: number[];
}
/** A decrypted and cached Matrix room event. */
interface DecryptedEvent {
/** Matrix event ID globally unique, used as primary key. */
eventId: string;
roomId: string;
senderUserId: string;
eventType: string;
/** JSON-serialised plaintext content. */
contentJson: string;
originServerTs: number;
}
/**
* Metadata for a room the local user participates in.
*
* Persisting `memberUserIds` lets the app call `updateTrackedUsers` on every
* startup without waiting for a full /sync, so the OlmMachine always has
* up-to-date device lists before the first encryption attempt.
*/
interface RoomRecord {
/** Matrix room ID primary key. */
roomId: string;
displayName: string;
/** User IDs of all E2EE members we need to track. */
memberUserIds: string[];
encryptionEnabled: boolean;
lastEventTs: number;
}
/**
* Sync cursor and per-room read position.
*
* Stored client-side because the homeserver does not track which events
* the local user has already rendered.
*/
interface SyncState {
/** Singleton row always use key "current". */
key: string;
/** The `next_batch` token from the last successful /sync response. */
nextBatch: string | null;
/** roomId → last event ID the user has read. */
readPositions: Record<string, string>;
}
class SipherDb extends Dexie {
decryptedEvents!: EntityTable<DecryptedEvent, "eventId">;
rooms!: EntityTable<RoomRecord, "roomId">;
syncState!: EntityTable<SyncState, "key">;
identity!: EntityTable<IdentityRecord, "userId">;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
constructor(idb?: any, idbRange?: any) {
if (idb && idbRange) {
super("MatrixClientDB", { indexedDB: idb, IDBKeyRange: idbRange });
} else {
super("MatrixClientDB");
}
this.version(1).stores({
decryptedEvents: "eventId, roomId, senderUserId, originServerTs",
rooms: "roomId, lastEventTs, encryptionEnabled",
syncState: "key",
});
this.version(2).stores({
identity: "userId",
});
}
}
let IndexedDB: SipherDb | null = null;
export function getDb(): SipherDb {
if (IndexedDB) return IndexedDB;
if (typeof (globalThis as any).indexedDB !== "undefined") {
IndexedDB = new SipherDb((globalThis as any).indexedDB, (globalThis as any).IDBKeyRange);
} else {
IndexedDB = new SipherDb();
}
return IndexedDB;
}

View file

@ -1,7 +1,7 @@
import Bun from "bun"; import Bun from "bun";
import nacl from "tweetnacl"; import nacl from "tweetnacl";
export async function generateKeyPair() { export async function generateEnvKeyPair() {
const envFile = Bun.file(".env.local"); const envFile = Bun.file(".env.local");
if (!await envFile.exists()) { if (!await envFile.exists()) {
throw new Error("No .env.local file found"); throw new Error("No .env.local file found");
@ -40,4 +40,4 @@ export async function generateKeyPair() {
console.log("Federation keys generated and written to .env.local"); console.log("Federation keys generated and written to .env.local");
} }
generateKeyPair(); generateEnvKeyPair();

View file

@ -1,4 +1,5 @@
import { createCipheriv, createDecipheriv, hkdfSync, randomBytes } from "node:crypto"; import { binary_to_base58 } from "base58-js";
import { createCipheriv, createDecipheriv, createHash, hkdfSync, randomBytes } from "node:crypto";
import nacl from "tweetnacl"; import nacl from "tweetnacl";
export interface EncryptedEnvelope { export interface EncryptedEnvelope {
@ -79,12 +80,37 @@ export function decryptPayload(envelope: EncryptedEnvelope, ownX25519SecretKey:
} }
} }
import { createHash } from "node:crypto";
export function fingerprintKey(keyBase64: string): string { export function fingerprintKey(keyBase64: string): string {
const hash = createHash("sha256").update(fromBase64(keyBase64)).digest("hex"); const hash = createHash("sha256").update(fromBase64(keyBase64)).digest("hex");
return hash; return hash;
} }
export function generateUserKeyPair(): { fingerprint: string, signingPublicKey: string, signingSecretKey: string } {
const signing = nacl.sign.keyPair();
// hash the public key to get the fingerprint
const fingerprintBytes = createHash("sha256").update(toBase64(signing.publicKey)).digest();
// encode the fingerprint bytes as base58
const fingerprintString = binary_to_base58(fingerprintBytes);
// return the fingerprint string and the signing public key
return {
fingerprint: fingerprintString,
signingPublicKey: toBase64(signing.publicKey),
signingSecretKey: toBase64(signing.secretKey),
};
}
/**
* Generates a key blob for the user keys.
* The data encrypted is not readable to the federation or any other person that has access to the database.
* @param password - The user's password.
* @param keys - The user's keys.
* @returns The key blob.
*/
// export async function generateUserKeyBlob(password: string, keys: { signingPublicKey: string, signingSecretKey: string, encryptionPublicKey: string, encryptionSecretKey: string }): Promise<string> {
// const olm = new Olm();
// }
export function getOwnEncryptionPublicKey(): Uint8Array { export function getOwnEncryptionPublicKey(): Uint8Array {
return new Uint8Array(Buffer.from(process.env.FEDERATION_ENCRYPTION_PUBLIC_KEY!, "base64")) return new Uint8Array(Buffer.from(process.env.FEDERATION_ENCRYPTION_PUBLIC_KEY!, "base64"))
} }

View file

@ -0,0 +1,27 @@
/**
* Canonical bytes that the user's Ed25519 identity key signs when authoring
* a post. The same builder is used by the client (to produce the signature)
* and the server (to verify it), so any change here must ship to both sides
* simultaneously bump `v` to invalidate old signatures during a migration.
*
* V8 and JavaScriptCore both preserve string-key insertion order in
* `JSON.stringify`, which makes this output deterministic across the
* browsers and Node versions we care about.
*/
export interface PostSignaturePayload {
postId: string;
authorId: string;
publishedAt: string;
content: unknown;
}
export function canonicalPostBytes(payload: PostSignaturePayload): Uint8Array {
const canonical = JSON.stringify({
v: 1,
postId: payload.postId,
authorId: payload.authorId,
publishedAt: payload.publishedAt,
content: payload.content,
});
return new TextEncoder().encode(canonical);
}

64
src/lib/identity/sign.ts Normal file
View file

@ -0,0 +1,64 @@
import { getDb, type IdentityRecord } from "@/lib/dexie";
import { gcm } from "@noble/ciphers/aes.js";
import { pbkdf2Async } from "@noble/hashes/pbkdf2.js";
import { sha256 } from "@noble/hashes/sha2.js";
import nacl from "tweetnacl";
/**
* Plaintext shape inside the AES-GCM-sealed identity blob in Dexie.
* The mnemonic + secret key combined make this the only thing in the system
* capable of producing valid signatures for the user's identity.
*/
interface IdentityPlaintext {
mnemonic: string;
fingerprint: string;
publicKey: number[];
secretKey: number[];
}
/**
* Decrypt the user's local identity blob using their master password.
*
* The PBKDF2 cost (600k iters) makes a brute-force attack against a stolen
* Dexie copy expensive; the GCM tag detects tampering. Used by both the
* `useSigningKey` callback API in the Oven plugin and the one-shot
* `signWithLocalIdentity` helper below.
*/
export async function decryptIdentity(
record: IdentityRecord,
password: string,
): Promise<IdentityPlaintext> {
const aesKey = await pbkdf2Async(sha256, password, Uint8Array.from(record.salt), {
c: 600_000,
dkLen: 32,
});
const plaintext = gcm(aesKey, Uint8Array.from(record.iv)).decrypt(Uint8Array.from(record.ciphertext));
return JSON.parse(new TextDecoder().decode(plaintext)) as IdentityPlaintext;
}
/**
* Sign one message with the user's Ed25519 identity secret key.
*
* Decrypts the keypair, produces a single detached signature, then zeroes the
* secret bytes from memory before returning. Returns `null` if the user has
* no identity stored locally on this device.
*/
export async function signWithLocalIdentity(
userId: string,
password: string,
message: Uint8Array,
): Promise<{ signature: Uint8Array; publicKey: Uint8Array } | null> {
const record = await getDb().identity.get(userId);
if (!record) return null;
const parsed = await decryptIdentity(record, password);
const secretKey = new Uint8Array(parsed.secretKey);
const publicKey = new Uint8Array(parsed.publicKey);
try {
const signature = nacl.sign.detached(message, secretKey);
return { signature, publicKey };
} finally {
secretKey.fill(0);
}
}

View file

@ -130,7 +130,7 @@ export const federation = () => {
index: false index: false
} }
} }
} },
} }
} satisfies BetterAuthPlugin; } satisfies BetterAuthPlugin;
} }

View file

@ -0,0 +1,3 @@
## Oven
This is where the E2EE magic happens, here you'll find the Client and Server side OLM things

View file

@ -0,0 +1,195 @@
import { getDb } from "@/lib/dexie";
import { decryptIdentity } from "@/lib/identity/sign";
import type { KeysUploadRequest } from "@matrix-org/matrix-sdk-crypto-wasm";
import { gcm } from "@noble/ciphers/aes.js";
import { hkdf } from "@noble/hashes/hkdf.js";
import { pbkdf2Async } from "@noble/hashes/pbkdf2.js";
import { sha256 } from "@noble/hashes/sha2.js";
import { randomBytes } from "@noble/hashes/utils.js";
import * as bip39 from "@scure/bip39";
import { wordlist } from "@scure/bip39/wordlists/english.js";
import { binary_to_base58 } from "base58-js";
import type { BetterAuthClientPlugin } from "better-auth/client";
import nacl from "tweetnacl";
import type { sipherOven } from "../server";
type SipherOvenPlugin = typeof sipherOven;
/**
* Sipher Oven plugin client side.
*
* Security model
* --------------
* - The Ed25519 identity keypair is derived from a BIP-39 mnemonic via
* HKDF-SHA256 and stored AES-256-GCM encrypted in Dexie. The KEK is
* derived from the user's master password with PBKDF2 (600k iters).
* - The matching secret key is never returned from this plugin. The only
* way to use it is `useSigningKey`, which hands callers a `sign` closure
* and zeroes the in-memory secret immediately after the callback resolves.
* - Only the public Ed25519 key + base58 fingerprint are published to the
* server (federation). The matching secret never leaves this device.
* - The OlmMachine manages its own IndexedDB-backed state internally; we
* just forward the public OLM key bundle to the server via /oven/keys/upload.
*/
export const sipherOvenClientPlugin = () => {
return {
id: "sipher-oven",
$InferServerPlugin: {} as ReturnType<SipherOvenPlugin>,
getActions($fetch, _$store, _options) {
return {
createOvenIdentity: async (username: string, password: string) => {
const { DeviceId, OlmMachine, RequestType, UserId, initAsync } = await import("@matrix-org/matrix-sdk-crypto-wasm");
await initAsync();
// On a fresh signup attempt (no Dexie identity yet), wipe any
// stale OlmMachine IDB left over from a previous failed run —
// otherwise OlmMachine.initialize throws "the account in the
// store doesn't match the account in the constructor".
if (!(await getDb().identity.get(username)) && typeof globalThis.indexedDB !== "undefined") {
await new Promise<void>((resolve) => {
const req = globalThis.indexedDB.deleteDatabase("matrix-sdk-crypto:sipher");
req.onsuccess = req.onerror = req.onblocked = () => resolve();
});
}
const mnemonic = bip39.generateMnemonic(wordlist, 128);
const fullSeed = await bip39.mnemonicToSeed(mnemonic);
const derived = hkdf(sha256, fullSeed, new Uint8Array(32), new TextEncoder().encode("sipher-identity-v1"), 32);
const { publicKey, secretKey } = nacl.sign.keyPair.fromSeed(derived);
const fingerprint = binary_to_base58(publicKey);
const userId = new UserId(`@${username}:${fingerprint}`);
const deviceId = new DeviceId(Buffer.from(randomBytes(32)).toString("base64"));
const machine = await OlmMachine.initialize(userId, deviceId, "sipher", password);
const salt = randomBytes(16);
const iv = randomBytes(12);
const aesKey = await pbkdf2Async(sha256, password, salt, { c: 600_000, dkLen: 32 });
const plaintext = new TextEncoder().encode(JSON.stringify({
mnemonic,
fingerprint,
publicKey: Array.from(publicKey),
secretKey: Array.from(secretKey),
}));
const ciphertext = gcm(aesKey, iv).encrypt(plaintext);
await getDb().identity.put({
userId: username,
salt: Array.from(salt),
iv: Array.from(iv),
ciphertext: Array.from(ciphertext),
});
secretKey.fill(0);
const { error: registerError } = await $fetch<{ success: boolean }>("/oven/identity/register", {
method: "POST",
body: { signingPublicKey: binary_to_base58(publicKey), fingerprint },
});
if (registerError) {
console.error("[createOvenIdentity]", registerError);
throw new Error("Failed to register identity public key");
}
const requests = await machine.outgoingRequests();
for (const request of requests) {
switch (request.type) {
case RequestType.KeysUpload: {
const req = request as KeysUploadRequest;
const { data, error } = await $fetch<{
success: boolean;
message?: string;
one_time_key_counts: { signed_curve25519: number };
}>("/oven/keys/upload", {
method: "POST",
body: req.body,
});
if (error) throw new Error("Failed to upload keys");
if (!data?.success) throw new Error(data?.message ?? "Failed to upload keys.");
// `markRequestAsSent` expects the SERVER RESPONSE body, not
// the request body. Per the Matrix spec, the response must
// include `one_time_key_counts` so the OlmMachine knows how
// many OTKs the server is now holding.
machine.markRequestAsSent(
req.id,
RequestType.KeysUpload,
JSON.stringify({ one_time_key_counts: data.one_time_key_counts }),
);
break;
}
}
}
return { userId, deviceId, machine, fingerprint, publicKey, mnemonic };
},
/**
* Read-only accessor for non-secret identity material.
* Returns the mnemonic (so the user can view their recovery phrase),
* fingerprint, and public key. Never returns the secret key.
*/
loadLocalIdentity: async (userId: string, password: string) => {
const record = await getDb().identity.get(userId);
if (!record) return null;
const parsed = await decryptIdentity(record, password);
return {
mnemonic: parsed.mnemonic,
fingerprint: parsed.fingerprint,
publicKey: new Uint8Array(parsed.publicKey),
};
},
/**
* Briefly decrypt the signing keypair, hand the caller a `sign`
* closure for one or more signing operations, then wipe the
* secret bytes from memory. The secret never escapes this scope.
*/
useSigningKey: async <T>(
userId: string,
password: string,
fn: (sign: (message: Uint8Array) => Uint8Array, publicKey: Uint8Array) => Promise<T> | T,
): Promise<T | null> => {
const record = await getDb().identity.get(userId);
if (!record) return null;
const parsed = await decryptIdentity(record, password);
const secretKey = new Uint8Array(parsed.secretKey);
const publicKey = new Uint8Array(parsed.publicKey);
try {
return await fn(
(message) => nacl.sign.detached(message, secretKey),
publicKey,
);
} finally {
secretKey.fill(0);
}
},
/**
* Reports whether the user has an identity locally (encrypted
* Dexie record) and/or remotely (registered with the server).
*
* Both halves matter:
* - `local` is required to sign anything (the secret key lives
* only in the encrypted Dexie blob).
* - `server` is required for federation peers to verify those
* signatures.
*
* Callers usually want `local && server` to consider the identity
* fully provisioned; `local && !server` means the server-side
* registration was lost and can be re-published; `!local && server`
* means the user needs a recovery flow on this device.
*/
checkIdentity: async (userId: string) => {
const local = (await getDb().identity.get(userId)) !== undefined;
const { data } = await $fetch<{ exists: boolean }>("/oven/identity/check", {
method: "GET",
});
return { local, server: data?.exists ?? false };
},
};
},
} satisfies BetterAuthClientPlugin;
};

View file

@ -0,0 +1,296 @@
import db from "@/lib/db";
import { olmDeviceKeys, userIdentityKeys } from "@/lib/db/schema";
import type { BetterAuthPlugin } from "better-auth";
import { createAuthEndpoint, getSessionFromCtx } from "better-auth/api";
import { eq } from "drizzle-orm";
import { z } from "zod";
import {
IdentityRegisterBodySchema,
KeysUploadBodySchema,
type SignedFallbackKey,
type SignedKey,
} from "./schema";
/**
* Sipher Oven plugin server side.
*
* Security model
* --------------
* This plugin only ever touches PUBLIC cryptographic material:
*
* - `userIdentityKeys` stores the user's stable Ed25519 verification key
* derived client-side from their BIP-39 mnemonic. The matching secret
* key is encrypted in the client's Dexie store and never reaches us.
* - `olmDeviceKeys` stores one row per device. The single `bundleJson`
* column holds the full Matrix `{ device_keys, one_time_keys, fallback_keys }`
* blob published by the OlmMachine. The OlmMachine keeps its own private
* state in IndexedDB.
*
* The schema in `./schema.ts` rejects anything that isn't a 32-byte
* unpadded-base64 public key, which makes it structurally impossible for a
* client to land a 64-byte NaCl secret key in any of the OLM key fields.
*/
interface DeviceBundle {
device_keys: z.infer<typeof KeysUploadBodySchema>["device_keys"];
one_time_keys: Record<string, SignedKey>;
fallback_keys: Record<string, SignedFallbackKey>;
}
export const sipherOven = () => {
return {
id: "sipher-oven",
schema: {
/**
* Per-user stable identity keys.
* The Ed25519 signing key derived from the user's mnemonic seed.
* One row per user must remain stable across all devices.
*/
userIdentityKeys: {
fields: {
userId: {
type: "string",
required: true,
unique: true,
references: {
model: "user",
field: "id",
onDelete: "cascade",
},
},
signingPublicKey: {
type: "string",
required: true,
unique: true,
},
fingerprint: {
type: "string",
required: true,
unique: true,
},
createdAt: {
type: "date",
required: true,
},
updatedAt: {
type: "date",
required: true,
},
},
},
/**
* Per-device OLM key bundle. One row per device, single JSON blob
* holding `{ device_keys, one_time_keys, fallback_keys }` exactly
* as published by the OlmMachine. Incremental OTK uploads merge
* into the JSON map in place never spawn additional rows.
*/
olmDeviceKeys: {
fields: {
userId: {
type: "string",
required: true,
references: {
model: "user",
field: "id",
onDelete: "cascade",
},
},
deviceId: {
type: "string",
required: true,
unique: true,
},
bundleJson: {
type: "string",
required: true,
},
createdAt: {
type: "date",
required: true,
},
updatedAt: {
type: "date",
required: true,
},
},
},
},
endpoints: {
/**
* Register the user's stable identity public key.
*
* Called once when the client first generates its mnemonic-derived
* keypair. Subsequent calls upsert (so a client that re-derives the
* same key from the same mnemonic is idempotent), but the keys
* themselves should never change for a given user.
*
* Only public material is accepted; the body schema enforces this.
*/
registerIdentity: createAuthEndpoint("/oven/identity/register", {
method: "POST",
body: IdentityRegisterBodySchema,
}, async (context) => {
const session = await getSessionFromCtx(context);
if (!session) {
return context.json({ error: "Unauthorized" }, { status: 401 });
}
const { signingPublicKey, fingerprint } = context.body;
const now = new Date();
await db.transaction(async (tx) => {
const updated = await tx
.update(userIdentityKeys)
.set({ signingPublicKey, fingerprint, updatedAt: now })
.where(eq(userIdentityKeys.userId, session.user.id))
.returning({ id: userIdentityKeys.id });
if (updated.length === 0) {
await tx.insert(userIdentityKeys).values({
id: crypto.randomUUID(),
userId: session.user.id,
signingPublicKey,
fingerprint,
createdAt: now,
updatedAt: now,
});
}
});
return context.json({ success: true });
}),
/**
* Upload (or incrementally update) a device's OLM key bundle.
*
* Bundle structure persisted as `bundle_json`:
* {
* device_keys: { ... full Matrix DeviceKeys ... },
* one_time_keys: { "<algo>:<id>": SignedKey, ... },
* fallback_keys: { "<algo>:<id>": SignedFallbackKey, ... },
* }
*
* Matrix's incremental-upload semantics for OTKs/fallback keys are
* applied to the JSON map in place: a string value is treated as a
* "delete this key" marker, an object value adds/replaces the entry.
*/
keysUpload: createAuthEndpoint("/oven/keys/upload", {
method: "POST",
body: z.string().transform((val) => {
const parsed = KeysUploadBodySchema.safeParse(JSON.parse(val));
if (!parsed.success) {
throw new Error(parsed.error.message);
}
return parsed.data;
}),
}, async (context) => {
const session = await getSessionFromCtx(context);
if (!session) {
return context.json({ error: "Unauthorized" }, { status: 401 });
}
const { device_keys, one_time_keys, fallback_keys } = context.body;
if (!device_keys) {
return context.json({ error: "Device keys are required", code: "DEVICE_KEYS_REQUIRED" }, { status: 400 });
}
if (!one_time_keys) {
return context.json({ error: "One time keys are required", code: "ONE_TIME_KEYS_REQUIRED" }, { status: 400 });
}
if (!fallback_keys) {
return context.json({ error: "Fallback keys are required", code: "FALLBACK_KEYS_REQUIRED" }, { status: 400 });
}
const userId = session.user.id;
const deviceId = device_keys.device_id;
const now = new Date();
let otkCount = 0;
await db.transaction(async (tx) => {
const [existing] = await tx
.select({ bundleJson: olmDeviceKeys.bundleJson })
.from(olmDeviceKeys)
.where(eq(olmDeviceKeys.deviceId, deviceId))
.limit(1);
const previous: DeviceBundle = existing
? (JSON.parse(existing.bundleJson) as DeviceBundle)
: { device_keys, one_time_keys: {}, fallback_keys: {} };
const mergedOtks: Record<string, SignedKey> = { ...previous.one_time_keys };
for (const [keyId, value] of Object.entries(one_time_keys)) {
if (typeof value === "string") {
delete mergedOtks[keyId];
} else {
mergedOtks[keyId] = value;
}
}
const mergedFallback: Record<string, SignedFallbackKey> = { ...previous.fallback_keys };
for (const [keyId, value] of Object.entries(fallback_keys)) {
if (typeof value === "string") {
delete mergedFallback[keyId];
} else {
mergedFallback[keyId] = value;
}
}
const bundleJson = JSON.stringify({
device_keys,
one_time_keys: mergedOtks,
fallback_keys: mergedFallback,
} satisfies DeviceBundle);
if (existing) {
await tx
.update(olmDeviceKeys)
.set({ bundleJson, updatedAt: now })
.where(eq(olmDeviceKeys.deviceId, deviceId));
} else {
await tx.insert(olmDeviceKeys).values({
id: crypto.randomUUID(),
userId,
deviceId,
bundleJson,
createdAt: now,
updatedAt: now,
});
}
otkCount = Object.keys(mergedOtks).length;
});
return context.json({
success: true,
one_time_key_counts: {
signed_curve25519: otkCount,
},
});
}),
/**
* Returns whether the authenticated user has registered their
* mnemonic-derived identity public key with the server.
*
* Always responds 200 so the caller doesn't have to disambiguate
* "not registered" from a transport error.
*/
checkIdentity: createAuthEndpoint("/oven/identity/check", {
method: "GET",
}, async (context) => {
const session = await getSessionFromCtx(context);
if (!session) {
return context.json({ error: "Unauthorized" }, { status: 401 });
}
const [identity] = await db
.select({ id: userIdentityKeys.id })
.from(userIdentityKeys)
.where(eq(userIdentityKeys.userId, session.user.id))
.limit(1);
return context.json({ exists: !!identity });
}),
},
} satisfies BetterAuthPlugin;
};

View file

@ -0,0 +1,79 @@
import { z } from "zod";
/**
* Matrix-flavoured unpadded base64 for a 32-byte Curve25519/Ed25519 public key.
* 32 raw bytes encode to exactly 43 base64 characters (no padding).
*
* Anything longer (e.g. a 64-byte NaCl secret key 86 chars) or shorter is
* rejected, which is our cheap defence against a client trying to upload
* private key material in a field that should only hold a public key.
*/
const OLM_PUBLIC_KEY = z
.string()
.regex(/^[A-Za-z0-9+/]{43}$/, "Must be unpadded base64 of a 32-byte public key");
const SignaturesSchema = z.record(
z.string(),
z.record(z.string(), z.string()),
);
const DeviceKeysSchema = z.object({
user_id: z.string(),
device_id: z.string(),
algorithms: z.array(z.string()),
keys: z.record(z.string(), OLM_PUBLIC_KEY),
signatures: SignaturesSchema,
});
export const SignedKeySchema = z.object({
key: OLM_PUBLIC_KEY,
signatures: SignaturesSchema,
});
export const SignedFallbackKeySchema = z.object({
key: OLM_PUBLIC_KEY,
fallback: z.literal(true),
signatures: SignaturesSchema,
});
export type SignedKey = z.infer<typeof SignedKeySchema>;
export type SignedFallbackKey = z.infer<typeof SignedFallbackKeySchema>;
export const KeysUploadBodySchema = z
.object({
device_keys: DeviceKeysSchema.optional(),
one_time_keys: z
.record(z.string(), z.union([z.string(), SignedKeySchema]))
.optional(),
fallback_keys: z
.record(z.string(), z.union([z.string(), SignedFallbackKeySchema]))
.optional(),
})
.refine(
(b) =>
b.device_keys !== undefined ||
b.one_time_keys !== undefined ||
b.fallback_keys !== undefined,
{ message: "At least one of device_keys, one_time_keys, or fallback_keys must be present" },
);
export type KeysUploadBody = z.infer<typeof KeysUploadBodySchema>;
/**
* Body for `POST /oven/identity/register`.
*
* Carries the user's stable per-account identity material derived client-side
* from their BIP-39 mnemonic. Both fields are public; the corresponding secret
* key never leaves the client's encrypted Dexie store.
*
* - `signingPublicKey`: base58 of the Ed25519 verification key.
* - `fingerprint`: base58 of the same public key (kept distinct so we can later
* migrate to a separate human-readable fingerprint format without breaking
* the wire schema).
*/
export const IdentityRegisterBodySchema = z.object({
signingPublicKey: z.string().min(1),
fingerprint: z.string().min(1),
});
export type IdentityRegisterBody = z.infer<typeof IdentityRegisterBodySchema>;

View file

@ -1,4 +1,7 @@
import { canonicalPostBytes } from "@/lib/identity/postSignature";
import { signWithLocalIdentity } from "@/lib/identity/sign";
import type { BetterAuthClientPlugin } from "better-auth/client"; import type { BetterAuthClientPlugin } from "better-auth/client";
import { v4 } from "uuid";
import { z } from "zod"; import { z } from "zod";
import type { sipherSocial } from "../server/social"; import type { sipherSocial } from "../server/social";
@ -27,15 +30,31 @@ export const sipherSocialClientPlugin = () => {
$InferServerPlugin: {} as ReturnType<SipherSocialPlugin>, $InferServerPlugin: {} as ReturnType<SipherSocialPlugin>,
getActions($fetch, $store, options) { getActions($fetch, $store, options) {
return { return {
createPost: async (content: z.infer<typeof clientPostContentSchmema>) => { /**
* Author and submit a post.
*
* Each post is detached-Ed25519-signed by the user's mnemonic-derived
* identity key. The matching secret is decrypted in memory only for
* the duration of the signing call (see `signWithLocalIdentity`),
* then zeroed. The server verifies the signature against the user's
* registered `signingPublicKey` before persisting the post.
*
* @param content Content blocks (text/media/link).
* @param userId Better Auth user id of the author (the same id
* used when the identity was created).
* @param password Master password that unlocks the local identity.
*/
createPost: async (
content: z.infer<typeof clientPostContentSchmema>,
userId: string,
password: string,
) => {
// Allow only these combinations of content: // Allow only these combinations of content:
// 1. Text only // 1. Text only
// 2. Text and images // 2. Text and images
// 3. Text, images and videos // 3. Text, images and videos
// 4. Text and audio // 4. Text and audio
// No other combinations are allowed // No other combinations are allowed
// Check the content types and throw an error if the combination is not allowed
const contentTypes = content.map((block) => block.type); const contentTypes = content.map((block) => block.type);
if (contentTypes.length > 1) { if (contentTypes.length > 1) {
if (contentTypes.includes("image") && contentTypes.includes("audio")) { if (contentTypes.includes("image") && contentTypes.includes("audio")) {
@ -44,7 +63,6 @@ export const sipherSocialClientPlugin = () => {
throw new Error("Videos and audios cannot be combined under the same post.") throw new Error("Videos and audios cannot be combined under the same post.")
} }
} }
// Check if the content amount per type is under the allowed limits
const imageCount = content.filter((block) => block.type === "image").length; const imageCount = content.filter((block) => block.type === "image").length;
const videoCount = content.filter((block) => block.type === "video").length; const videoCount = content.filter((block) => block.type === "video").length;
const audioCount = content.filter((block) => block.type === "audio").length; const audioCount = content.filter((block) => block.type === "audio").length;
@ -98,47 +116,65 @@ export const sipherSocialClientPlugin = () => {
}); });
} }
const { data, error } = await $fetch<{ const postId = v4()
postId: string; const publishedAt = new Date().toISOString();
}>("/social/posts", {
method: "POST", const signed = await signWithLocalIdentity(
body: { userId,
content: resolvedContent, password,
canonicalPostBytes({ postId, authorId: userId, publishedAt, content: resolvedContent }),
);
if (!signed) {
throw new Error("No local identity found on this device. Create one before posting.");
}
const signature = Buffer.from(signed.signature).toString("base64");
const { data, error } = await $fetch<{ id: string; federationDeliveriesQueued: number }>(
"/social/posts",
{
method: "POST",
body: {
postId,
publishedAt,
signature,
content: resolvedContent,
},
}, },
}); );
if (error || !data) { if (error || !data) {
throw new Error("Failed to create post"); throw new Error("Failed to create post");
} }
return data.postId; return { id: data.id, federationDeliveriesQueued: data.federationDeliveriesQueued };
}, },
followUser: async (userId: string, federationUrl?: string) => { followUser: async (userId: string, federationUrl?: string) => {
const body: Record<string, string> = { const body: Record<string, string> = {
method: "INSERT", method: "INSERT",
userId, userId,
};
if (federationUrl) {
body.federationUrl = federationUrl;
}
const { data, error } = await $fetch<{
following: {
id: string;
createdAt: Date;
followerId: string;
followingId: string;
accepted: boolean;
}; };
}>("/social/follows", { if (federationUrl) {
method: "POST", body.federationUrl = federationUrl;
body, }
});
if (error || !data) { const { data, error } = await $fetch<{
throw new Error("Failed to follow user"); following: {
id: string;
createdAt: Date;
followerId: string;
followingId: string;
accepted: boolean;
};
}>("/social/follows", {
method: "POST",
body,
});
if (error || !data) {
throw new Error("Failed to follow user");
}
return data.following;
} }
return data.following;
}
} }
}, },
} satisfies BetterAuthClientPlugin; } satisfies BetterAuthClientPlugin;

View file

@ -1,14 +1,17 @@
import { getFederationQueue, type FederationDeliveryJob } from "@/lib/bull"; import { getFederationQueue, type FederationDeliveryJob } from "@/lib/bull";
import db from "@/lib/db"; import db from "@/lib/db";
import { deliveryJobs, follows, posts, serverRegistry } from "@/lib/db/schema"; import { deliveryJobs, follows, posts, serverRegistry, userIdentityKeys } from "@/lib/db/schema";
import { encryptPayload } from "@/lib/federation/keytools"; import { encryptPayload } from "@/lib/federation/keytools";
import { applyFederatedPostInTransaction } from "@/lib/federation/proxy-helpers/federated-post"; import { applyFederatedPostInTransaction } from "@/lib/federation/proxy-helpers/federated-post";
import { canonicalPostBytes } from "@/lib/identity/postSignature";
import minioClient from "@/lib/plugins/storage/server/minio.client";
import { EncryptedEnvelopeBaseSchema } from "@/lib/zod/EncryptedEnvelope"; import { EncryptedEnvelopeBaseSchema } from "@/lib/zod/EncryptedEnvelope";
import { PostEnvelopeSchema } from "@/lib/zod/methods/PostFederationSchema"; import { PostEnvelopeSchema } from "@/lib/zod/methods/PostFederationSchema";
import minioClient from "@/plugins/server/storage/minio.client"; import { base58_to_binary } from "base58-js";
import { createAuthEndpoint, getSessionFromCtx } from "better-auth/api"; import { createAuthEndpoint, getSessionFromCtx } from "better-auth/api";
import createDebug from "debug"; import createDebug from "debug";
import { and, eq } from "drizzle-orm"; import { and, eq } from "drizzle-orm";
import nacl from "tweetnacl";
import { z } from "zod"; import { z } from "zod";
import { postContentSchema } from "../social"; import { postContentSchema } from "../social";
@ -20,7 +23,14 @@ const federatedPostRequestSchema = z.object({
signature: z.string(), signature: z.string(),
}); });
const createPostBodySchema = z.union([federatedPostRequestSchema, postContentSchema]); const userPostRequestSchema = z.object({
postId: z.uuidv4(),
publishedAt: z.iso.datetime(),
signature: z.string().min(1),
content: postContentSchema,
});
const createPostBodySchema = z.union([federatedPostRequestSchema, userPostRequestSchema]);
export const createPost = createAuthEndpoint("/social/posts", { export const createPost = createAuthEndpoint("/social/posts", {
method: "POST", method: "POST",
@ -28,7 +38,7 @@ export const createPost = createAuthEndpoint("/social/posts", {
}, async (context) => { }, async (context) => {
const body = context.body; const body = context.body;
if (typeof body === "object" && body !== null && "method" in body && body.method === "FEDERATE_POST") { if ("method" in body) {
const { payload: encryptedPayload, signature } = body; const { payload: encryptedPayload, signature } = body;
const parsedEnvelope = PostEnvelopeSchema.safeParse(encryptedPayload); const parsedEnvelope = PostEnvelopeSchema.safeParse(encryptedPayload);
@ -80,13 +90,51 @@ export const createPost = createAuthEndpoint("/social/posts", {
); );
} }
const content = body; const { postId, publishedAt, signature, content } = body;
const user = await getSessionFromCtx(context); const user = await getSessionFromCtx(context);
if (!user) { if (!user) {
return context.json({ error: "Unauthorized" }, { status: 401 }); return context.json({ error: "Unauthorized" }, { status: 401 });
} }
// Verify the post against the user's registered identity key. Without a
// matching identity row the user cannot author posts — they must complete
// the identity-creation flow first.
const [identity] = await db
.select({ signingPublicKey: userIdentityKeys.signingPublicKey })
.from(userIdentityKeys)
.where(eq(userIdentityKeys.userId, user.user.id))
.limit(1);
if (!identity) {
return context.json(
{ error: "No identity registered for this user", code: "IDENTITY_NOT_REGISTERED" },
{ status: 412 },
);
}
let signatureValid = false;
try {
const publicKey = base58_to_binary(identity.signingPublicKey);
const signatureBytes = Uint8Array.from(Buffer.from(signature, "base64"));
const message = canonicalPostBytes({
postId,
authorId: user.user.id,
publishedAt,
content,
});
signatureValid = nacl.sign.detached.verify(message, signatureBytes, publicKey);
} catch (err) {
debug("signature verification threw: %o", err);
}
if (!signatureValid) {
return context.json(
{ error: "Invalid post signature", code: "INVALID_POST_SIGNATURE" },
{ status: 400 },
);
}
const isPrivate = user.user.isPrivate; const isPrivate = user.user.isPrivate;
const shouldPropagate = { const shouldPropagate = {
all: true, all: true,
@ -94,8 +142,7 @@ export const createPost = createAuthEndpoint("/social/posts", {
none: false, none: false,
}[user.user.postPropagationPolicy as "all" | "followers" | "none"] ?? true; }[user.user.postPropagationPolicy as "all" | "followers" | "none"] ?? true;
const postId = crypto.randomUUID(); const published = new Date(publishedAt);
const published = new Date();
const inserted = await db const inserted = await db
.insert(posts) .insert(posts)
.values({ .values({
@ -108,6 +155,7 @@ export const createPost = createAuthEndpoint("/social/posts", {
federationUrl: process.env.BETTER_AUTH_URL!, federationUrl: process.env.BETTER_AUTH_URL!,
federationPostId: postId, federationPostId: postId,
createdAt: new Date(), createdAt: new Date(),
authorSignature: signature,
}) })
.returning({ id: posts.id }); .returning({ id: posts.id });

View file

@ -111,7 +111,21 @@ export default {
type: "string", type: "string",
required: false, required: false,
index: true, index: true,
} },
/**
* Base64-encoded Ed25519 detached signature produced client-side by
* the author's mnemonic-derived identity key. Covers the canonical
* post payload defined in `src/lib/identity/postSignature.ts`.
*
* Optional so federated/legacy posts that arrive without a per-user
* signature can still be stored, but locally-authored posts always
* have one the createPost endpoint rejects requests that don't.
*/
authorSignature: {
type: "string",
required: false,
index: false,
},
} }
}, },
follows: { follows: {

View file

@ -1,7 +1,7 @@
import type { BetterAuthPlugin } from "better-auth"; import type { BetterAuthPlugin } from "better-auth";
import * as socialEndpoints from "./helpers/social/endpoints"; import * as socialEndpoints from "./helpers/endpoints";
import socialSchema from "./helpers/social/social"; import socialSchema from "./helpers/social";
export const sipherSocial = () => { export const sipherSocial = () => {
return { return {

View file

@ -1,4 +1,4 @@
import { postContentSchema } from "@/lib/plugins/server/helpers/social/social"; import { postContentSchema } from "@/lib/plugins/social/server/helpers/social";
import { z } from "zod"; import { z } from "zod";
import { createEncryptedEnvelopeSchema } from "../EncryptedEnvelope"; import { createEncryptedEnvelopeSchema } from "../EncryptedEnvelope";

View file

@ -18,7 +18,7 @@ import { expect, test } from "@playwright/test"
import http from "node:http" import http from "node:http"
import { import {
clearTables, clearTables,
generateKeypair, generateEnvKeyPair,
getBlacklistedServer, getBlacklistedServer,
getChallengesByServerUrl, getChallengesByServerUrl,
seedChallenge, seedChallenge,
@ -70,7 +70,7 @@ test.afterEach(async () => { await clearTables() })
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
test.describe("SSRF protection", () => { test.describe("SSRF protection", () => {
test("REGISTER rejects loopback URLs", async ({ request }) => { test("REGISTER rejects loopback URLs", async ({ request }) => {
const keys = generateKeypair() const keys = generateEnvKeyPair()
const trap = createTrapServer(keys.signingPublicKey, keys.encryptionPublicKey) const trap = createTrapServer(keys.signingPublicKey, keys.encryptionPublicKey)
const port = await trap.start() const port = await trap.start()
@ -102,7 +102,7 @@ test.describe("SSRF protection", () => {
] ]
for (const url of internalUrls) { for (const url of internalUrls) {
const keys = generateKeypair() const keys = generateEnvKeyPair()
const res = await request.post(`${BASE}/discover`, { const res = await request.post(`${BASE}/discover`, {
data: { data: {
method: "REGISTER", method: "REGISTER",
@ -119,7 +119,7 @@ test.describe("SSRF protection", () => {
}) })
test("DISCOVER rejects stored internal URLs", async ({ request }) => { test("DISCOVER rejects stored internal URLs", async ({ request }) => {
const keys = generateKeypair() const keys = generateEnvKeyPair()
await seedServer("http://127.0.0.1:9999", keys.signingPublicKey, keys.encryptionPublicKey) await seedServer("http://127.0.0.1:9999", keys.signingPublicKey, keys.encryptionPublicKey)
const envelopePayload = JSON.stringify({ const envelopePayload = JSON.stringify({
@ -176,12 +176,12 @@ test.describe("Blacklist enforcement (fixed)", () => {
} }
test("blacklisted server is rejected by rotate/init", async ({ request }) => { test("blacklisted server is rejected by rotate/init", async ({ request }) => {
const oldKeys = generateKeypair() const oldKeys = generateEnvKeyPair()
const serverUrl = "https://blacklisted-server.example" const serverUrl = "https://blacklisted-server.example"
await seedServer(serverUrl, oldKeys.signingPublicKey, oldKeys.encryptionPublicKey) await seedServer(serverUrl, oldKeys.signingPublicKey, oldKeys.encryptionPublicKey)
await blacklistServer(serverUrl, request as any) await blacklistServer(serverUrl, request as any)
const newKeys = generateKeypair() const newKeys = generateEnvKeyPair()
const initRes = await request.post(`${BASE}/discover/rotate/init`, { const initRes = await request.post(`${BASE}/discover/rotate/init`, {
data: { data: {
url: serverUrl, url: serverUrl,
@ -196,7 +196,7 @@ test.describe("Blacklist enforcement (fixed)", () => {
test("blacklisted server is rejected by rotate/confirm", async ({ request }) => { test("blacklisted server is rejected by rotate/confirm", async ({ request }) => {
const serverUrl = "https://blacklisted-confirm.example" const serverUrl = "https://blacklisted-confirm.example"
const keys = generateKeypair() const keys = generateEnvKeyPair()
await seedServer(serverUrl, keys.signingPublicKey, keys.encryptionPublicKey) await seedServer(serverUrl, keys.signingPublicKey, keys.encryptionPublicKey)
await blacklistServer(serverUrl, request as any) await blacklistServer(serverUrl, request as any)
@ -218,7 +218,7 @@ test.describe("Blacklist enforcement (fixed)", () => {
test.describe("Race condition fixed on rotate/confirm", () => { test.describe("Race condition fixed on rotate/confirm", () => {
test("concurrent requests are serialised by the row lock", async () => { test("concurrent requests are serialised by the row lock", async () => {
const serverUrl = "https://race-target.example" const serverUrl = "https://race-target.example"
const keys = generateKeypair() const keys = generateEnvKeyPair()
await seedServer(serverUrl, keys.signingPublicKey, keys.encryptionPublicKey) await seedServer(serverUrl, keys.signingPublicKey, keys.encryptionPublicKey)
await seedChallenge({ await seedChallenge({
@ -257,11 +257,11 @@ test.describe("Race condition fixed on rotate/confirm", () => {
test.describe("Challenge deduplication (fixed)", () => { test.describe("Challenge deduplication (fixed)", () => {
test("second init is rejected while a challenge is pending", async ({ request }) => { test("second init is rejected while a challenge is pending", async ({ request }) => {
const serverUrl = "https://dedup-target.example" const serverUrl = "https://dedup-target.example"
const keys = generateKeypair() const keys = generateEnvKeyPair()
await seedServer(serverUrl, keys.signingPublicKey, keys.encryptionPublicKey) await seedServer(serverUrl, keys.signingPublicKey, keys.encryptionPublicKey)
const newKeys1 = generateKeypair() const newKeys1 = generateEnvKeyPair()
const newKeys2 = generateKeypair() const newKeys2 = generateEnvKeyPair()
const res1 = await request.post(`${BASE}/discover/rotate/init`, { const res1 = await request.post(`${BASE}/discover/rotate/init`, {
data: { data: {
@ -289,7 +289,7 @@ test.describe("Challenge deduplication (fixed)", () => {
test("init succeeds after the previous challenge expires", async ({ request }) => { test("init succeeds after the previous challenge expires", async ({ request }) => {
const serverUrl = "https://dedup-expire.example" const serverUrl = "https://dedup-expire.example"
const keys = generateKeypair() const keys = generateEnvKeyPair()
await seedServer(serverUrl, keys.signingPublicKey, keys.encryptionPublicKey) await seedServer(serverUrl, keys.signingPublicKey, keys.encryptionPublicKey)
await seedChallenge({ await seedChallenge({
@ -297,7 +297,7 @@ test.describe("Challenge deduplication (fixed)", () => {
expiresAt: new Date(Date.now() - 1000), expiresAt: new Date(Date.now() - 1000),
}) })
const newKeys = generateKeypair() const newKeys = generateEnvKeyPair()
const res = await request.post(`${BASE}/discover/rotate/init`, { const res = await request.post(`${BASE}/discover/rotate/init`, {
data: { data: {
url: serverUrl, url: serverUrl,
@ -314,7 +314,7 @@ test.describe("Challenge deduplication (fixed)", () => {
test("blacklisted server cannot reset attempts via new init", async ({ request }) => { test("blacklisted server cannot reset attempts via new init", async ({ request }) => {
const serverUrl = "https://reset-blocked.example" const serverUrl = "https://reset-blocked.example"
const keys = generateKeypair() const keys = generateEnvKeyPair()
await seedServer(serverUrl, keys.signingPublicKey, keys.encryptionPublicKey) await seedServer(serverUrl, keys.signingPublicKey, keys.encryptionPublicKey)
await seedChallenge({ await seedChallenge({
@ -338,7 +338,7 @@ test.describe("Challenge deduplication (fixed)", () => {
const bl = await getBlacklistedServer(serverUrl) const bl = await getBlacklistedServer(serverUrl)
expect(bl).toBeDefined() expect(bl).toBeDefined()
const freshKeys = generateKeypair() const freshKeys = generateEnvKeyPair()
const initRes = await request.post(`${BASE}/discover/rotate/init`, { const initRes = await request.post(`${BASE}/discover/rotate/init`, {
data: { data: {
url: serverUrl, url: serverUrl,
@ -355,7 +355,7 @@ test.describe("Challenge deduplication (fixed)", () => {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
test.describe("Envelope validation (fixed)", () => { test.describe("Envelope validation (fixed)", () => {
test("envelope with mismatched publicKey fingerprint is rejected", async ({ request }) => { test("envelope with mismatched publicKey fingerprint is rejected", async ({ request }) => {
const keys = generateKeypair() const keys = generateEnvKeyPair()
await seedServer("https://sig-test.example", keys.signingPublicKey, keys.encryptionPublicKey) await seedServer("https://sig-test.example", keys.signingPublicKey, keys.encryptionPublicKey)
const badEnvelope = encryptPayload( const badEnvelope = encryptPayload(
@ -380,7 +380,7 @@ test.describe("Envelope validation (fixed)", () => {
}) })
test("envelope with placeholder values is rejected", async ({ request }) => { test("envelope with placeholder values is rejected", async ({ request }) => {
const keys = generateKeypair() const keys = generateEnvKeyPair()
await seedServer("https://sig-test2.example", keys.signingPublicKey, keys.encryptionPublicKey) await seedServer("https://sig-test2.example", keys.signingPublicKey, keys.encryptionPublicKey)
const forgeryEnvelope = encryptPayload( const forgeryEnvelope = encryptPayload(
@ -401,7 +401,7 @@ test.describe("Envelope validation (fixed)", () => {
}) })
test("envelope with correct fingerprints passes validation", async ({ request }) => { test("envelope with correct fingerprints passes validation", async ({ request }) => {
const keys = generateKeypair() const keys = generateEnvKeyPair()
const trap = createTrapServer(keys.signingPublicKey, keys.encryptionPublicKey) const trap = createTrapServer(keys.signingPublicKey, keys.encryptionPublicKey)
const port = await trap.start() const port = await trap.start()
const peerUrl = `http://127.0.0.1:${port}` const peerUrl = `http://127.0.0.1:${port}`
@ -442,8 +442,8 @@ test.describe("Envelope validation (fixed)", () => {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
test.describe("Information disclosure", () => { test.describe("Information disclosure", () => {
test("GET /discover only returns url and isHealthy for peers", async ({ request }) => { test("GET /discover only returns url and isHealthy for peers", async ({ request }) => {
const keys1 = generateKeypair() const keys1 = generateEnvKeyPair()
const keys2 = generateKeypair() const keys2 = generateEnvKeyPair()
await seedServer("https://peer-one.example", keys1.signingPublicKey, keys1.encryptionPublicKey) await seedServer("https://peer-one.example", keys1.signingPublicKey, keys1.encryptionPublicKey)
await seedServer("https://peer-two.example", keys2.signingPublicKey, keys2.encryptionPublicKey) await seedServer("https://peer-two.example", keys2.signingPublicKey, keys2.encryptionPublicKey)

View file

@ -4,7 +4,7 @@ import { blacklistedServers, rotateChallengeTokens, serverRegistry } from "@/lib
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import nacl from "tweetnacl"; import nacl from "tweetnacl";
export function generateKeypair() { export function generateEnvKeyPair() {
const signing = nacl.sign.keyPair(); const signing = nacl.sign.keyPair();
const encryption = nacl.box.keyPair(); const encryption = nacl.box.keyPair();
return { return {
@ -29,7 +29,7 @@ export async function seedServer(url: string, publicKey: string, encryptionPubli
} }
export async function seedChallenge(overrides?: Partial<typeof rotateChallengeTokens.$inferInsert>) { export async function seedChallenge(overrides?: Partial<typeof rotateChallengeTokens.$inferInsert>) {
const keys = generateKeypair() const keys = generateEnvKeyPair()
const defaults = { const defaults = {
id: crypto.randomUUID(), id: crypto.randomUUID(),
serverUrl: "https://test-server.com", serverUrl: "https://test-server.com",

View file

@ -9,11 +9,11 @@
* - Blacklists server after too many failed attempts * - Blacklists server after too many failed attempts
* - Full init confirm happy path that rotates both keys * - Full init confirm happy path that rotates both keys
*/ */
import { expect, test } from "@playwright/test"
import createDebug from "debug"
import type { EncryptedEnvelope } from "@/lib/federation/keytools" import type { EncryptedEnvelope } from "@/lib/federation/keytools"
import { decryptPayload, encryptPayload, signMessage } from "@/lib/federation/keytools" import { decryptPayload, encryptPayload, signMessage } from "@/lib/federation/keytools"
import { clearTables, generateKeypair, getServerByUrl, seedChallenge, seedServer } from "./helpers/db" import { expect, test } from "@playwright/test"
import createDebug from "debug"
import { clearTables, generateEnvKeyPair, getServerByUrl, seedChallenge, seedServer } from "./helpers/db"
const debug = createDebug("test:key") const debug = createDebug("test:key")
@ -53,8 +53,8 @@ interface InitChallenges {
function solveInitChallenges( function solveInitChallenges(
challenges: InitChallenges, challenges: InitChallenges,
oldKeys: ReturnType<typeof generateKeypair>, oldKeys: ReturnType<typeof generateEnvKeyPair>,
newKeys: ReturnType<typeof generateKeypair>, newKeys: ReturnType<typeof generateEnvKeyPair>,
) { ) {
const oldSigningSecret = new Uint8Array(Buffer.from(oldKeys.signingSecretKey, "base64")) const oldSigningSecret = new Uint8Array(Buffer.from(oldKeys.signingSecretKey, "base64"))
const newSigningSecret = new Uint8Array(Buffer.from(newKeys.signingSecretKey, "base64")) const newSigningSecret = new Uint8Array(Buffer.from(newKeys.signingSecretKey, "base64"))
@ -73,7 +73,7 @@ function solveInitChallenges(
// rotate/init tests // rotate/init tests
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
test("init rejects unregistered server", async ({ request }) => { test("init rejects unregistered server", async ({ request }) => {
const newKeys = generateKeypair() const newKeys = generateEnvKeyPair()
const res = await request.post("/discover/rotate/init", { const res = await request.post("/discover/rotate/init", {
data: { data: {
url: "https://unknown-server.com", url: "https://unknown-server.com",
@ -85,7 +85,7 @@ test("init rejects unregistered server", async ({ request }) => {
}) })
test("init rejects same keys as currently registered", async ({ request }) => { test("init rejects same keys as currently registered", async ({ request }) => {
const keys = generateKeypair() const keys = generateEnvKeyPair()
await seedServer(SERVER_URL, keys.signingPublicKey, keys.encryptionPublicKey) await seedServer(SERVER_URL, keys.signingPublicKey, keys.encryptionPublicKey)
const res = await request.post("/discover/rotate/init", { const res = await request.post("/discover/rotate/init", {
data: { data: {
@ -99,8 +99,8 @@ test("init rejects same keys as currently registered", async ({ request }) => {
}) })
test("init issues 4 challenges", async ({ request }) => { test("init issues 4 challenges", async ({ request }) => {
const oldKeys = generateKeypair() const oldKeys = generateEnvKeyPair()
const newKeys = generateKeypair() const newKeys = generateEnvKeyPair()
await seedServer(SERVER_URL, oldKeys.signingPublicKey, oldKeys.encryptionPublicKey) await seedServer(SERVER_URL, oldKeys.signingPublicKey, oldKeys.encryptionPublicKey)
const res = await request.post("/discover/rotate/init", { const res = await request.post("/discover/rotate/init", {
@ -122,9 +122,9 @@ test("init issues 4 challenges", async ({ request }) => {
}) })
test("init rejects duplicate while challenge is pending", async ({ request }) => { test("init rejects duplicate while challenge is pending", async ({ request }) => {
const oldKeys = generateKeypair() const oldKeys = generateEnvKeyPair()
const newKeys1 = generateKeypair() const newKeys1 = generateEnvKeyPair()
const newKeys2 = generateKeypair() const newKeys2 = generateEnvKeyPair()
await seedServer(SERVER_URL, oldKeys.signingPublicKey, oldKeys.encryptionPublicKey) await seedServer(SERVER_URL, oldKeys.signingPublicKey, oldKeys.encryptionPublicKey)
const res1 = await request.post("/discover/rotate/init", { const res1 = await request.post("/discover/rotate/init", {
@ -173,8 +173,8 @@ test("confirm rejects expired challenge", async ({ request }) => {
}) })
test("confirm rejects wrong proofs (init → confirm)", async ({ request }) => { test("confirm rejects wrong proofs (init → confirm)", async ({ request }) => {
const oldKeys = generateKeypair() const oldKeys = generateEnvKeyPair()
const newKeys = generateKeypair() const newKeys = generateEnvKeyPair()
await seedServer(SERVER_URL, oldKeys.signingPublicKey, oldKeys.encryptionPublicKey) await seedServer(SERVER_URL, oldKeys.signingPublicKey, oldKeys.encryptionPublicKey)
debug("test: wrong proofs calling init") debug("test: wrong proofs calling init")
@ -199,8 +199,8 @@ test("confirm rejects wrong proofs (init → confirm)", async ({ request }) => {
}) })
test("confirm blacklists after too many failed attempts", async ({ request }) => { test("confirm blacklists after too many failed attempts", async ({ request }) => {
const oldKeys = generateKeypair() const oldKeys = generateEnvKeyPair()
const newKeys = generateKeypair() const newKeys = generateEnvKeyPair()
await seedServer(SERVER_URL, oldKeys.signingPublicKey, oldKeys.encryptionPublicKey) await seedServer(SERVER_URL, oldKeys.signingPublicKey, oldKeys.encryptionPublicKey)
debug("test: blacklists calling init") debug("test: blacklists calling init")
@ -240,8 +240,8 @@ test("confirm blacklists after too many failed attempts", async ({ request }) =>
// Full init → confirm happy path // Full init → confirm happy path
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
test("full rotation flow: init → solve → confirm rotates both keys", async ({ request }) => { test("full rotation flow: init → solve → confirm rotates both keys", async ({ request }) => {
const oldKeys = generateKeypair() const oldKeys = generateEnvKeyPair()
const newKeys = generateKeypair() const newKeys = generateEnvKeyPair()
await seedServer(SERVER_URL, oldKeys.signingPublicKey, oldKeys.encryptionPublicKey) await seedServer(SERVER_URL, oldKeys.signingPublicKey, oldKeys.encryptionPublicKey)
debug("test: full flow calling init") debug("test: full flow calling init")

View file

@ -37,7 +37,7 @@ interface FedKeys {
encryptionSecretKey: string; encryptionSecretKey: string;
} }
function generateKeypair(): FedKeys { function generateEnvKeyPair(): FedKeys {
const signing = nacl.sign.keyPair(); const signing = nacl.sign.keyPair();
const encryption = nacl.box.keyPair(); const encryption = nacl.box.keyPair();
return { return {
@ -350,7 +350,7 @@ if (!isFallbackMode) {
{ {
const testName = "reject mismatched signing key"; const testName = "reject mismatched signing key";
try { try {
const fakeKeys = generateKeypair(); const fakeKeys = generateEnvKeyPair();
const innerPayload = JSON.stringify({ action: "bad-key-test" }); const innerPayload = JSON.stringify({ action: "bad-key-test" });
const targetEncKey = new Uint8Array(Buffer.from(targetInfo.encryptionPublicKey, "base64")); const targetEncKey = new Uint8Array(Buffer.from(targetInfo.encryptionPublicKey, "base64"));
@ -396,7 +396,7 @@ if (!isFallbackMode) {
{ {
const testName = "reject unknown sender"; const testName = "reject unknown sender";
try { try {
const unknownKeys = generateKeypair(); const unknownKeys = generateEnvKeyPair();
const unknownOrigin = "https://totally-unknown-federation-" + crypto.randomUUID().slice(0, 8) + ".test"; const unknownOrigin = "https://totally-unknown-federation-" + crypto.randomUUID().slice(0, 8) + ".test";
const innerPayload = JSON.stringify({ action: "unknown-sender-test" }); const innerPayload = JSON.stringify({ action: "unknown-sender-test" });