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:
parent
7049a40870
commit
66ebebd105
47 changed files with 1753 additions and 246 deletions
11
.cursor/mcp.json
Normal file
11
.cursor/mcp.json
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"mcpServers": {
|
||||
"next-devtools": {
|
||||
"command": "npx",
|
||||
"args": [
|
||||
"-y",
|
||||
"next-devtools-mcp@latest"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -24,4 +24,6 @@ MINIO_ENDPOINT=
|
|||
MINIO_PORT=
|
||||
MINIO_USE_SSL=
|
||||
MINIO_ACCESS_KEY=
|
||||
MINIO_SECRET_KEY=
|
||||
MINIO_SECRET_KEY=
|
||||
|
||||
NEXT_PUBLIC_GIT_URL=
|
||||
255
bun.lock
255
bun.lock
|
|
@ -5,59 +5,64 @@
|
|||
"": {
|
||||
"name": "sipher",
|
||||
"dependencies": {
|
||||
"@better-auth/drizzle-adapter": "latest",
|
||||
"@hookform/resolvers": "latest",
|
||||
"@nanostores/react": "latest",
|
||||
"@react-email/components": "latest",
|
||||
"better-auth": "latest",
|
||||
"bullmq": "latest",
|
||||
"class-variance-authority": "latest",
|
||||
"clsx": "latest",
|
||||
"debug": "latest",
|
||||
"dexie": "latest",
|
||||
"dexie-react-hooks": "latest",
|
||||
"dotenv": "latest",
|
||||
"drizzle-orm": "latest",
|
||||
"framer-motion": "latest",
|
||||
"ioredis": "latest",
|
||||
"lucide-react": "latest",
|
||||
"minio": "latest",
|
||||
"nanostores": "latest",
|
||||
"next": "latest",
|
||||
"next-themes": "latest",
|
||||
"nodemailer": "latest",
|
||||
"pg": "latest",
|
||||
"radix-ui": "latest",
|
||||
"react": "latest",
|
||||
"react-dom": "latest",
|
||||
"react-hook-form": "latest",
|
||||
"socket.io": "latest",
|
||||
"socket.io-client": "latest",
|
||||
"sonner": "latest",
|
||||
"tailwind-merge": "latest",
|
||||
"tweetnacl": "latest",
|
||||
"tweetnacl-util": "latest",
|
||||
"zod": "latest",
|
||||
"@better-auth/drizzle-adapter": "^1.6.9",
|
||||
"@hookform/resolvers": "^5.2.2",
|
||||
"@matrix-org/matrix-sdk-crypto-wasm": "^18.2.0",
|
||||
"@nanostores/react": "^1.1.0",
|
||||
"@react-email/components": "1.0.12",
|
||||
"@scure/bip39": "^2.2.0",
|
||||
"@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",
|
||||
"clsx": "^2.1.1",
|
||||
"debug": "^4.4.3",
|
||||
"dexie": "^4.4.2",
|
||||
"dexie-react-hooks": "^4.4.0",
|
||||
"dotenv": "^17.4.2",
|
||||
"drizzle-orm": "^0.45.2",
|
||||
"framer-motion": "^12.38.0",
|
||||
"ioredis": "^5.10.1",
|
||||
"lucide-react": "^1.14.0",
|
||||
"minio": "^8.0.7",
|
||||
"nanostores": "^1.3.0",
|
||||
"next": "16.2.3",
|
||||
"next-themes": "^0.4.6",
|
||||
"nodemailer": "^8.0.7",
|
||||
"pg": "^8.20.0",
|
||||
"radix-ui": "^1.4.3",
|
||||
"react": "19.2.5",
|
||||
"react-dom": "19.2.5",
|
||||
"react-hook-form": "^7.75.0",
|
||||
"socket.io": "^4.8.3",
|
||||
"socket.io-client": "^4.8.3",
|
||||
"sonner": "^2.0.7",
|
||||
"tailwind-merge": "^3.5.0",
|
||||
"tweetnacl": "^1.0.3",
|
||||
"tweetnacl-util": "^0.15.1",
|
||||
"uuid": "^14.0.0",
|
||||
"zod": "^4.4.3",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "latest",
|
||||
"@types/bun": "latest",
|
||||
"@types/debug": "latest",
|
||||
"@types/node": "latest",
|
||||
"@types/nodemailer": "latest",
|
||||
"@types/pg": "latest",
|
||||
"@types/react": "latest",
|
||||
"@types/react-dom": "latest",
|
||||
"auth": "latest",
|
||||
"babel-plugin-react-compiler": "latest",
|
||||
"cross-env": "latest",
|
||||
"drizzle-kit": "latest",
|
||||
"react-email": "latest",
|
||||
"shadcn": "latest",
|
||||
"tailwindcss": "latest",
|
||||
"tsx": "latest",
|
||||
"tw-animate-css": "latest",
|
||||
"typescript": "latest",
|
||||
"@tailwindcss/postcss": "^4.2.4",
|
||||
"@types/bun": "^1.3.13",
|
||||
"@types/debug": "^4.1.13",
|
||||
"@types/node": "^25.6.0",
|
||||
"@types/nodemailer": "^8.0.0",
|
||||
"@types/pg": "^8.20.0",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"auth": "^1.6.9",
|
||||
"babel-plugin-react-compiler": "1.0.0",
|
||||
"cross-env": "^10.1.0",
|
||||
"drizzle-kit": "^0.31.10",
|
||||
"react-email": "5.2.10",
|
||||
"shadcn": "^4.6.0",
|
||||
"tailwindcss": "^4.2.4",
|
||||
"tsx": "^4.21.0",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"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=="],
|
||||
|
||||
"@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=="],
|
||||
|
||||
|
|
@ -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=="],
|
||||
|
||||
"@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=="],
|
||||
|
||||
"@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=="],
|
||||
|
||||
"@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/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=="],
|
||||
|
||||
|
|
@ -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/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=="],
|
||||
|
||||
|
|
@ -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/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=="],
|
||||
|
||||
|
|
@ -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=="],
|
||||
|
||||
"@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=="],
|
||||
|
||||
"@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=="],
|
||||
|
||||
"@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=="],
|
||||
|
||||
"@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=="],
|
||||
|
||||
"@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=="],
|
||||
|
||||
|
|
@ -617,7 +630,7 @@
|
|||
|
||||
"@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=="],
|
||||
|
||||
|
|
@ -653,7 +666,7 @@
|
|||
|
||||
"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=="],
|
||||
|
||||
|
|
@ -661,11 +674,13 @@
|
|||
|
||||
"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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
|
|
@ -685,9 +700,9 @@
|
|||
|
||||
"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=="],
|
||||
|
||||
|
|
@ -815,7 +830,7 @@
|
|||
|
||||
"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=="],
|
||||
|
||||
|
|
@ -1105,7 +1120,7 @@
|
|||
|
||||
"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=="],
|
||||
|
||||
|
|
@ -1147,7 +1162,7 @@
|
|||
|
||||
"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=="],
|
||||
|
||||
|
|
@ -1161,11 +1176,11 @@
|
|||
|
||||
"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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
|
|
@ -1177,11 +1192,13 @@
|
|||
|
||||
"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-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=="],
|
||||
|
||||
|
|
@ -1305,13 +1322,13 @@
|
|||
|
||||
"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-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=="],
|
||||
|
||||
|
|
@ -1379,7 +1396,7 @@
|
|||
|
||||
"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=="],
|
||||
|
||||
|
|
@ -1461,7 +1478,7 @@
|
|||
|
||||
"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=="],
|
||||
|
||||
|
|
@ -1495,15 +1512,15 @@
|
|||
|
||||
"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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
|
|
@ -1523,7 +1540,7 @@
|
|||
|
||||
"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=="],
|
||||
|
||||
|
|
@ -1569,7 +1586,7 @@
|
|||
|
||||
"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=="],
|
||||
|
||||
|
|
@ -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=="],
|
||||
|
||||
"@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/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=="],
|
||||
|
||||
"@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=="],
|
||||
|
||||
"@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=="],
|
||||
|
||||
"@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/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/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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
|
|
@ -1659,6 +1690,8 @@
|
|||
|
||||
"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/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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"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/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=="],
|
||||
|
||||
"c12/chokidar/readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="],
|
||||
|
|
|
|||
|
|
@ -8,6 +8,13 @@
|
|||
"when": 1772741645429,
|
||||
"tag": "0000_lazy_slyde",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 1,
|
||||
"version": "7",
|
||||
"when": 1746392520000,
|
||||
"tag": "0001_otk_schema_fix",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -7,6 +7,9 @@ const nextConfig: NextConfig = {
|
|||
reactCompiler: true,
|
||||
allowedDevOrigins,
|
||||
output: "standalone",
|
||||
experimental: {
|
||||
webpackMemoryOptimizations: true
|
||||
}
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
|
|
|
|||
48
package.json
48
package.json
|
|
@ -10,7 +10,7 @@
|
|||
}
|
||||
],
|
||||
"license": "AGPL-3.0",
|
||||
"version": "0.1.0",
|
||||
"version": "0.1.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"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:attacks": "cross-env NODE_ENV=test playwright test tests/attacks.test.ts",
|
||||
"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",
|
||||
"db:push": "drizzle-kit push",
|
||||
"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"
|
||||
},
|
||||
"dependencies": {
|
||||
"@better-auth/drizzle-adapter": "^1.6.0",
|
||||
"@better-auth/drizzle-adapter": "^1.6.9",
|
||||
"@hookform/resolvers": "^5.2.2",
|
||||
"@matrix-org/matrix-sdk-crypto-wasm": "^18.2.0",
|
||||
"@nanostores/react": "^1.1.0",
|
||||
"@react-email/components": "1.0.11",
|
||||
"better-auth": "^1.6.0",
|
||||
"bullmq": "^5.73.1",
|
||||
"@react-email/components": "1.0.12",
|
||||
"@scure/bip39": "^2.2.0",
|
||||
"@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",
|
||||
"clsx": "^2.1.1",
|
||||
"debug": "^4.4.3",
|
||||
"dexie": "^4.4.2",
|
||||
"dexie-react-hooks": "^4.4.0",
|
||||
"dotenv": "^17.4.1",
|
||||
"dotenv": "^17.4.2",
|
||||
"drizzle-orm": "^0.45.2",
|
||||
"framer-motion": "^12.38.0",
|
||||
"ioredis": "^5.10.1",
|
||||
"lucide-react": "^1.7.0",
|
||||
"lucide-react": "^1.14.0",
|
||||
"minio": "^8.0.7",
|
||||
"nanostores": "^1.2.0",
|
||||
"next": "16.2.2",
|
||||
"nanostores": "^1.3.0",
|
||||
"next": "16.2.3",
|
||||
"next-themes": "^0.4.6",
|
||||
"nodemailer": "^8.0.5",
|
||||
"nodemailer": "^8.0.7",
|
||||
"pg": "^8.20.0",
|
||||
"radix-ui": "^1.4.3",
|
||||
"react": "19.2.4",
|
||||
"react-dom": "19.2.4",
|
||||
"react-hook-form": "^7.72.1",
|
||||
"react": "19.2.5",
|
||||
"react-dom": "19.2.5",
|
||||
"react-hook-form": "^7.75.0",
|
||||
"socket.io": "^4.8.3",
|
||||
"socket.io-client": "^4.8.3",
|
||||
"sonner": "^2.0.7",
|
||||
"tailwind-merge": "^3.5.0",
|
||||
"tweetnacl": "^1.0.3",
|
||||
"tweetnacl-util": "^0.15.1",
|
||||
"zod": "^4.3.6"
|
||||
"uuid": "^14.0.0",
|
||||
"zod": "^4.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4.2.2",
|
||||
"@types/bun": "^1.3.11",
|
||||
"@tailwindcss/postcss": "^4.2.4",
|
||||
"@types/bun": "^1.3.13",
|
||||
"@types/debug": "^4.1.13",
|
||||
"@types/node": "^25.5.2",
|
||||
"@types/node": "^25.6.0",
|
||||
"@types/nodemailer": "^8.0.0",
|
||||
"@types/pg": "^8.20.0",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"auth": "^1.6.0",
|
||||
"auth": "^1.6.9",
|
||||
"babel-plugin-react-compiler": "1.0.0",
|
||||
"cross-env": "^10.1.0",
|
||||
"drizzle-kit": "^0.31.10",
|
||||
"react-email": "5.2.10",
|
||||
"shadcn": "^4.2.0",
|
||||
"tailwindcss": "^4.2.2",
|
||||
"shadcn": "^4.7.0",
|
||||
"tailwindcss": "^4.2.4",
|
||||
"tsx": "^4.21.0",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"typescript": "^6.0.2"
|
||||
"typescript": "^6.0.3"
|
||||
},
|
||||
"ignoreScripts": [
|
||||
"sharp",
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ interface FedKeys {
|
|||
encryptionSecretKey: string;
|
||||
}
|
||||
|
||||
function generateKeypair(): FedKeys {
|
||||
function generateEnvKeyPair(): FedKeys {
|
||||
const signing = nacl.sign.keyPair();
|
||||
const encryption = nacl.box.keyPair();
|
||||
return {
|
||||
|
|
@ -131,7 +131,7 @@ if (resumeIdx !== -1) {
|
|||
process.exit(1);
|
||||
}
|
||||
} else {
|
||||
newFedKeys = generateKeypair();
|
||||
newFedKeys = generateEnvKeyPair();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -5,12 +5,23 @@ import { authClient } from "@/lib/auth-client";
|
|||
import { useState } from "react";
|
||||
|
||||
export function PostTestForm() {
|
||||
const { data: session } = authClient.useSession();
|
||||
const [text, setText] = useState("");
|
||||
const [files, setFiles] = useState<File[]>([]);
|
||||
const [password, setPassword] = useState("");
|
||||
const [status, setStatus] = useState<string | null>(null);
|
||||
|
||||
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 {
|
||||
const content: { type: "text" | "image"; value: string | File }[] = [];
|
||||
|
||||
|
|
@ -27,7 +38,7 @@ export function PostTestForm() {
|
|||
return;
|
||||
}
|
||||
|
||||
const result = await authClient.createPost(content);
|
||||
const result = await authClient.createPost(content, session.user.id, password);
|
||||
setStatus(`Done: ${JSON.stringify(result)}`);
|
||||
} catch (err) {
|
||||
setStatus(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
||||
|
|
@ -90,6 +101,19 @@ export function PostTestForm() {
|
|||
)}
|
||||
</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
|
||||
onClick={handleSubmit}
|
||||
style={{
|
||||
|
|
|
|||
56
src/app/api/dev/relay-discover/route.ts
Normal file
56
src/app/api/dev/relay-discover/route.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -46,12 +46,23 @@ export function SignUpForm({
|
|||
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({
|
||||
email,
|
||||
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}`);
|
||||
toast.success("Registration successful, please check your email for the verification link!");
|
||||
onSuccess(email);
|
||||
|
|
|
|||
|
|
@ -110,7 +110,7 @@ function AuthPageContent() {
|
|||
<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">
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||
import type { Metadata } from "next";
|
||||
import { ThemeProvider } from "next-themes";
|
||||
import { Bebas_Neue, DM_Sans, Space_Mono } from "next/font/google";
|
||||
|
|
@ -46,8 +47,10 @@ export default function RootLayout({
|
|||
enableSystem
|
||||
disableTransitionOnChange
|
||||
>
|
||||
<Toaster />
|
||||
{children}
|
||||
<TooltipProvider>
|
||||
<Toaster />
|
||||
{children}
|
||||
</TooltipProvider>
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
25
src/app/manifest.ts
Normal file
25
src/app/manifest.ts
Normal 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',
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
|
@ -1,23 +1,31 @@
|
|||
"use server";
|
||||
|
||||
import CreateIdentity from "@/components/main/CreateIdentity";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { headers } from "next/headers";
|
||||
import { redirect } from "next/navigation";
|
||||
import { PostTestForm } from "./PostTestForm";
|
||||
|
||||
export default async function Home() {
|
||||
const reqHeaders = await headers();
|
||||
|
||||
const session = await auth.api.getSession({
|
||||
headers: await headers(),
|
||||
});
|
||||
const session = await auth.api.getSession({ headers: reqHeaders });
|
||||
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 (
|
||||
<>
|
||||
<PostTestForm />
|
||||
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
207
src/components/main/CreateIdentity.tsx
Normal file
207
src/components/main/CreateIdentity.tsx
Normal 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: "8–64 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>
|
||||
)
|
||||
}
|
||||
66
src/components/ui/accordion.tsx
Normal file
66
src/components/ui/accordion.tsx
Normal 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 }
|
||||
35
src/components/ui/switch.tsx
Normal file
35
src/components/ui/switch.tsx
Normal 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 }
|
||||
57
src/components/ui/tooltip.tsx
Normal file
57
src/components/ui/tooltip.tsx
Normal 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 }
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
export async function register() {
|
||||
if (process.env.NEXT_RUNTIME === 'nodejs') {
|
||||
const { startFederationWorker } = await import('./lib/bull');
|
||||
const { startFederationWorker } = await import('./lib/bull/worker');
|
||||
startFederationWorker();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { twoFactorClient, usernameClient } from "better-auth/client/plugins";
|
||||
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({
|
||||
fetchOptions: {},
|
||||
|
|
@ -8,5 +9,6 @@ export const authClient = createAuthClient({
|
|||
usernameClient(),
|
||||
twoFactorClient(),
|
||||
sipherSocialClientPlugin(),
|
||||
sipherOvenClientPlugin(),
|
||||
]
|
||||
})
|
||||
|
|
@ -1,12 +1,13 @@
|
|||
import { federation } from "@/plugins/server/federation";
|
||||
import { sipherSocial } from '@/plugins/server/social';
|
||||
import { federation } from "@/lib/plugins/federation/server/federation";
|
||||
import { sipherOven } from "@/lib/plugins/oven/server/index";
|
||||
import { sipherSocial } from '@/lib/plugins/social/server/social';
|
||||
import { drizzleAdapter } from "@better-auth/drizzle-adapter";
|
||||
import { betterAuth } from "better-auth";
|
||||
import { bearer, haveIBeenPwned, openAPI, testUtils, twoFactor, username } from "better-auth/plugins";
|
||||
import db from "./db";
|
||||
import * as schema from "./db/schema";
|
||||
import EmailService from "./mail";
|
||||
import minioClient from "./plugins/server/storage/minio.client";
|
||||
import minioClient from "./plugins/storage/server/minio.client";
|
||||
import getRedisClient from "./redis";
|
||||
|
||||
const isTest = process.env.NODE_ENV === "test";
|
||||
|
|
@ -73,6 +74,7 @@ const bAuth = betterAuth({
|
|||
federation(),
|
||||
openAPI(),
|
||||
testUtils(), // TODO: Add a conditional plugin for test utils in development
|
||||
sipherOven(),
|
||||
bearer()
|
||||
],
|
||||
// This is disabled by default, but I'll keep this here for ease of mind.
|
||||
|
|
@ -94,7 +96,7 @@ const bAuth = betterAuth({
|
|||
required: false,
|
||||
index: false,
|
||||
enum: ["all", "followers", "none"] as const,
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
export { getFederationQueue, getHealthCheckQueue, scheduleHealthCheck } from './queues';
|
||||
export type { FederationDeliveryJob, HealthCheckJob } from './queues';
|
||||
export { startFederationWorker } from './worker';
|
||||
|
||||
|
|
|
|||
|
|
@ -61,6 +61,7 @@ export const twoFactor = pgTable(
|
|||
userId: text("user_id")
|
||||
.notNull()
|
||||
.references(() => user.id, { onDelete: "cascade" }),
|
||||
verified: boolean("verified").default(true),
|
||||
},
|
||||
(table) => [
|
||||
index("twoFactor_secret_idx").on(table.secret),
|
||||
|
|
@ -83,6 +84,7 @@ export const posts = pgTable(
|
|||
createdAt: timestamp("created_at").notNull(),
|
||||
federationUrl: text("federation_url"),
|
||||
federationPostId: text("federation_post_id"),
|
||||
authorSignature: text("author_signature"),
|
||||
},
|
||||
(table) => [
|
||||
index("posts_federationUrl_idx").on(table.federationUrl),
|
||||
|
|
@ -198,12 +200,37 @@ export const blacklistedServers = pgTable(
|
|||
(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 }) => ({
|
||||
accounts: many(account),
|
||||
twoFactors: many(twoFactor),
|
||||
postss: many(posts),
|
||||
mutess: many(mutes),
|
||||
blockss: many(blocks),
|
||||
userIdentityKeys: many(userIdentityKeys),
|
||||
olmDeviceKeyss: many(olmDeviceKeys),
|
||||
}));
|
||||
|
||||
export const accountRelations = relations(account, ({ one }) => ({
|
||||
|
|
@ -281,3 +308,20 @@ export const serverRegistryRelations = relations(
|
|||
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
112
src/lib/dexie/index.ts
Normal 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;
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import Bun from "bun";
|
||||
import nacl from "tweetnacl";
|
||||
|
||||
export async function generateKeyPair() {
|
||||
export async function generateEnvKeyPair() {
|
||||
const envFile = Bun.file(".env.local");
|
||||
if (!await envFile.exists()) {
|
||||
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");
|
||||
}
|
||||
|
||||
generateKeyPair();
|
||||
generateEnvKeyPair();
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
||||
export interface EncryptedEnvelope {
|
||||
|
|
@ -79,12 +80,37 @@ export function decryptPayload(envelope: EncryptedEnvelope, ownX25519SecretKey:
|
|||
}
|
||||
}
|
||||
|
||||
import { createHash } from "node:crypto";
|
||||
export function fingerprintKey(keyBase64: string): string {
|
||||
const hash = createHash("sha256").update(fromBase64(keyBase64)).digest("hex");
|
||||
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 {
|
||||
return new Uint8Array(Buffer.from(process.env.FEDERATION_ENCRYPTION_PUBLIC_KEY!, "base64"))
|
||||
}
|
||||
|
|
|
|||
27
src/lib/identity/postSignature.ts
Normal file
27
src/lib/identity/postSignature.ts
Normal 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
64
src/lib/identity/sign.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -130,7 +130,7 @@ export const federation = () => {
|
|||
index: false
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
} satisfies BetterAuthPlugin;
|
||||
}
|
||||
3
src/lib/plugins/oven/README.md
Normal file
3
src/lib/plugins/oven/README.md
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
## Oven
|
||||
|
||||
This is where the E2EE magic happens, here you'll find the Client and Server side OLM things
|
||||
195
src/lib/plugins/oven/client/index.ts
Normal file
195
src/lib/plugins/oven/client/index.ts
Normal 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;
|
||||
};
|
||||
296
src/lib/plugins/oven/server/index.ts
Normal file
296
src/lib/plugins/oven/server/index.ts
Normal 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;
|
||||
};
|
||||
79
src/lib/plugins/oven/server/schema.ts
Normal file
79
src/lib/plugins/oven/server/schema.ts
Normal 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>;
|
||||
|
|
@ -1,4 +1,7 @@
|
|||
import { canonicalPostBytes } from "@/lib/identity/postSignature";
|
||||
import { signWithLocalIdentity } from "@/lib/identity/sign";
|
||||
import type { BetterAuthClientPlugin } from "better-auth/client";
|
||||
import { v4 } from "uuid";
|
||||
import { z } from "zod";
|
||||
import type { sipherSocial } from "../server/social";
|
||||
|
||||
|
|
@ -27,15 +30,31 @@ export const sipherSocialClientPlugin = () => {
|
|||
$InferServerPlugin: {} as ReturnType<SipherSocialPlugin>,
|
||||
getActions($fetch, $store, options) {
|
||||
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:
|
||||
// 1. Text only
|
||||
// 2. Text and images
|
||||
// 3. Text, images and videos
|
||||
// 4. Text and audio
|
||||
// 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);
|
||||
if (contentTypes.length > 1) {
|
||||
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.")
|
||||
}
|
||||
}
|
||||
// Check if the content amount per type is under the allowed limits
|
||||
const imageCount = content.filter((block) => block.type === "image").length;
|
||||
const videoCount = content.filter((block) => block.type === "video").length;
|
||||
const audioCount = content.filter((block) => block.type === "audio").length;
|
||||
|
|
@ -98,47 +116,65 @@ export const sipherSocialClientPlugin = () => {
|
|||
});
|
||||
}
|
||||
|
||||
const { data, error } = await $fetch<{
|
||||
postId: string;
|
||||
}>("/social/posts", {
|
||||
method: "POST",
|
||||
body: {
|
||||
content: resolvedContent,
|
||||
const postId = v4()
|
||||
const publishedAt = new Date().toISOString();
|
||||
|
||||
const signed = await signWithLocalIdentity(
|
||||
userId,
|
||||
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) {
|
||||
throw new Error("Failed to create post");
|
||||
}
|
||||
|
||||
return data.postId;
|
||||
return { id: data.id, federationDeliveriesQueued: data.federationDeliveriesQueued };
|
||||
},
|
||||
followUser: async (userId: string, federationUrl?: string) => {
|
||||
const body: Record<string, string> = {
|
||||
method: "INSERT",
|
||||
userId,
|
||||
};
|
||||
if (federationUrl) {
|
||||
body.federationUrl = federationUrl;
|
||||
}
|
||||
|
||||
const { data, error } = await $fetch<{
|
||||
following: {
|
||||
id: string;
|
||||
createdAt: Date;
|
||||
followerId: string;
|
||||
followingId: string;
|
||||
accepted: boolean;
|
||||
followUser: async (userId: string, federationUrl?: string) => {
|
||||
const body: Record<string, string> = {
|
||||
method: "INSERT",
|
||||
userId,
|
||||
};
|
||||
}>("/social/follows", {
|
||||
method: "POST",
|
||||
body,
|
||||
});
|
||||
if (error || !data) {
|
||||
throw new Error("Failed to follow user");
|
||||
if (federationUrl) {
|
||||
body.federationUrl = federationUrl;
|
||||
}
|
||||
|
||||
const { data, error } = await $fetch<{
|
||||
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;
|
||||
|
|
@ -1,14 +1,17 @@
|
|||
import { getFederationQueue, type FederationDeliveryJob } from "@/lib/bull";
|
||||
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 { 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 { 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 createDebug from "debug";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import nacl from "tweetnacl";
|
||||
import { z } from "zod";
|
||||
import { postContentSchema } from "../social";
|
||||
|
||||
|
|
@ -20,7 +23,14 @@ const federatedPostRequestSchema = z.object({
|
|||
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", {
|
||||
method: "POST",
|
||||
|
|
@ -28,7 +38,7 @@ export const createPost = createAuthEndpoint("/social/posts", {
|
|||
}, async (context) => {
|
||||
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 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);
|
||||
|
||||
if (!user) {
|
||||
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 shouldPropagate = {
|
||||
all: true,
|
||||
|
|
@ -94,8 +142,7 @@ export const createPost = createAuthEndpoint("/social/posts", {
|
|||
none: false,
|
||||
}[user.user.postPropagationPolicy as "all" | "followers" | "none"] ?? true;
|
||||
|
||||
const postId = crypto.randomUUID();
|
||||
const published = new Date();
|
||||
const published = new Date(publishedAt);
|
||||
const inserted = await db
|
||||
.insert(posts)
|
||||
.values({
|
||||
|
|
@ -108,6 +155,7 @@ export const createPost = createAuthEndpoint("/social/posts", {
|
|||
federationUrl: process.env.BETTER_AUTH_URL!,
|
||||
federationPostId: postId,
|
||||
createdAt: new Date(),
|
||||
authorSignature: signature,
|
||||
})
|
||||
.returning({ id: posts.id });
|
||||
|
||||
|
|
@ -111,7 +111,21 @@ export default {
|
|||
type: "string",
|
||||
required: false,
|
||||
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: {
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import type { BetterAuthPlugin } from "better-auth";
|
||||
|
||||
import * as socialEndpoints from "./helpers/social/endpoints";
|
||||
import socialSchema from "./helpers/social/social";
|
||||
import * as socialEndpoints from "./helpers/endpoints";
|
||||
import socialSchema from "./helpers/social";
|
||||
|
||||
export const sipherSocial = () => {
|
||||
return {
|
||||
|
|
@ -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 { createEncryptedEnvelopeSchema } from "../EncryptedEnvelope";
|
||||
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ import { expect, test } from "@playwright/test"
|
|||
import http from "node:http"
|
||||
import {
|
||||
clearTables,
|
||||
generateKeypair,
|
||||
generateEnvKeyPair,
|
||||
getBlacklistedServer,
|
||||
getChallengesByServerUrl,
|
||||
seedChallenge,
|
||||
|
|
@ -70,7 +70,7 @@ test.afterEach(async () => { await clearTables() })
|
|||
// ---------------------------------------------------------------------------
|
||||
test.describe("SSRF protection", () => {
|
||||
test("REGISTER rejects loopback URLs", async ({ request }) => {
|
||||
const keys = generateKeypair()
|
||||
const keys = generateEnvKeyPair()
|
||||
const trap = createTrapServer(keys.signingPublicKey, keys.encryptionPublicKey)
|
||||
const port = await trap.start()
|
||||
|
||||
|
|
@ -102,7 +102,7 @@ test.describe("SSRF protection", () => {
|
|||
]
|
||||
|
||||
for (const url of internalUrls) {
|
||||
const keys = generateKeypair()
|
||||
const keys = generateEnvKeyPair()
|
||||
const res = await request.post(`${BASE}/discover`, {
|
||||
data: {
|
||||
method: "REGISTER",
|
||||
|
|
@ -119,7 +119,7 @@ test.describe("SSRF protection", () => {
|
|||
})
|
||||
|
||||
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)
|
||||
|
||||
const envelopePayload = JSON.stringify({
|
||||
|
|
@ -176,12 +176,12 @@ test.describe("Blacklist enforcement (fixed)", () => {
|
|||
}
|
||||
|
||||
test("blacklisted server is rejected by rotate/init", async ({ request }) => {
|
||||
const oldKeys = generateKeypair()
|
||||
const oldKeys = generateEnvKeyPair()
|
||||
const serverUrl = "https://blacklisted-server.example"
|
||||
await seedServer(serverUrl, oldKeys.signingPublicKey, oldKeys.encryptionPublicKey)
|
||||
await blacklistServer(serverUrl, request as any)
|
||||
|
||||
const newKeys = generateKeypair()
|
||||
const newKeys = generateEnvKeyPair()
|
||||
const initRes = await request.post(`${BASE}/discover/rotate/init`, {
|
||||
data: {
|
||||
url: serverUrl,
|
||||
|
|
@ -196,7 +196,7 @@ test.describe("Blacklist enforcement (fixed)", () => {
|
|||
|
||||
test("blacklisted server is rejected by rotate/confirm", async ({ request }) => {
|
||||
const serverUrl = "https://blacklisted-confirm.example"
|
||||
const keys = generateKeypair()
|
||||
const keys = generateEnvKeyPair()
|
||||
await seedServer(serverUrl, keys.signingPublicKey, keys.encryptionPublicKey)
|
||||
await blacklistServer(serverUrl, request as any)
|
||||
|
||||
|
|
@ -218,7 +218,7 @@ test.describe("Blacklist enforcement (fixed)", () => {
|
|||
test.describe("Race condition fixed on rotate/confirm", () => {
|
||||
test("concurrent requests are serialised by the row lock", async () => {
|
||||
const serverUrl = "https://race-target.example"
|
||||
const keys = generateKeypair()
|
||||
const keys = generateEnvKeyPair()
|
||||
await seedServer(serverUrl, keys.signingPublicKey, keys.encryptionPublicKey)
|
||||
|
||||
await seedChallenge({
|
||||
|
|
@ -257,11 +257,11 @@ test.describe("Race condition fixed on rotate/confirm", () => {
|
|||
test.describe("Challenge deduplication (fixed)", () => {
|
||||
test("second init is rejected while a challenge is pending", async ({ request }) => {
|
||||
const serverUrl = "https://dedup-target.example"
|
||||
const keys = generateKeypair()
|
||||
const keys = generateEnvKeyPair()
|
||||
await seedServer(serverUrl, keys.signingPublicKey, keys.encryptionPublicKey)
|
||||
|
||||
const newKeys1 = generateKeypair()
|
||||
const newKeys2 = generateKeypair()
|
||||
const newKeys1 = generateEnvKeyPair()
|
||||
const newKeys2 = generateEnvKeyPair()
|
||||
|
||||
const res1 = await request.post(`${BASE}/discover/rotate/init`, {
|
||||
data: {
|
||||
|
|
@ -289,7 +289,7 @@ test.describe("Challenge deduplication (fixed)", () => {
|
|||
|
||||
test("init succeeds after the previous challenge expires", async ({ request }) => {
|
||||
const serverUrl = "https://dedup-expire.example"
|
||||
const keys = generateKeypair()
|
||||
const keys = generateEnvKeyPair()
|
||||
await seedServer(serverUrl, keys.signingPublicKey, keys.encryptionPublicKey)
|
||||
|
||||
await seedChallenge({
|
||||
|
|
@ -297,7 +297,7 @@ test.describe("Challenge deduplication (fixed)", () => {
|
|||
expiresAt: new Date(Date.now() - 1000),
|
||||
})
|
||||
|
||||
const newKeys = generateKeypair()
|
||||
const newKeys = generateEnvKeyPair()
|
||||
const res = await request.post(`${BASE}/discover/rotate/init`, {
|
||||
data: {
|
||||
url: serverUrl,
|
||||
|
|
@ -314,7 +314,7 @@ test.describe("Challenge deduplication (fixed)", () => {
|
|||
|
||||
test("blacklisted server cannot reset attempts via new init", async ({ request }) => {
|
||||
const serverUrl = "https://reset-blocked.example"
|
||||
const keys = generateKeypair()
|
||||
const keys = generateEnvKeyPair()
|
||||
await seedServer(serverUrl, keys.signingPublicKey, keys.encryptionPublicKey)
|
||||
|
||||
await seedChallenge({
|
||||
|
|
@ -338,7 +338,7 @@ test.describe("Challenge deduplication (fixed)", () => {
|
|||
const bl = await getBlacklistedServer(serverUrl)
|
||||
expect(bl).toBeDefined()
|
||||
|
||||
const freshKeys = generateKeypair()
|
||||
const freshKeys = generateEnvKeyPair()
|
||||
const initRes = await request.post(`${BASE}/discover/rotate/init`, {
|
||||
data: {
|
||||
url: serverUrl,
|
||||
|
|
@ -355,7 +355,7 @@ test.describe("Challenge deduplication (fixed)", () => {
|
|||
// ---------------------------------------------------------------------------
|
||||
test.describe("Envelope validation (fixed)", () => {
|
||||
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)
|
||||
|
||||
const badEnvelope = encryptPayload(
|
||||
|
|
@ -380,7 +380,7 @@ test.describe("Envelope validation (fixed)", () => {
|
|||
})
|
||||
|
||||
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)
|
||||
|
||||
const forgeryEnvelope = encryptPayload(
|
||||
|
|
@ -401,7 +401,7 @@ test.describe("Envelope validation (fixed)", () => {
|
|||
})
|
||||
|
||||
test("envelope with correct fingerprints passes validation", async ({ request }) => {
|
||||
const keys = generateKeypair()
|
||||
const keys = generateEnvKeyPair()
|
||||
const trap = createTrapServer(keys.signingPublicKey, keys.encryptionPublicKey)
|
||||
const port = await trap.start()
|
||||
const peerUrl = `http://127.0.0.1:${port}`
|
||||
|
|
@ -442,8 +442,8 @@ test.describe("Envelope validation (fixed)", () => {
|
|||
// ---------------------------------------------------------------------------
|
||||
test.describe("Information disclosure", () => {
|
||||
test("GET /discover only returns url and isHealthy for peers", async ({ request }) => {
|
||||
const keys1 = generateKeypair()
|
||||
const keys2 = generateKeypair()
|
||||
const keys1 = generateEnvKeyPair()
|
||||
const keys2 = generateEnvKeyPair()
|
||||
await seedServer("https://peer-one.example", keys1.signingPublicKey, keys1.encryptionPublicKey)
|
||||
await seedServer("https://peer-two.example", keys2.signingPublicKey, keys2.encryptionPublicKey)
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { blacklistedServers, rotateChallengeTokens, serverRegistry } from "@/lib
|
|||
import { eq } from "drizzle-orm";
|
||||
import nacl from "tweetnacl";
|
||||
|
||||
export function generateKeypair() {
|
||||
export function generateEnvKeyPair() {
|
||||
const signing = nacl.sign.keyPair();
|
||||
const encryption = nacl.box.keyPair();
|
||||
return {
|
||||
|
|
@ -29,7 +29,7 @@ export async function seedServer(url: string, publicKey: string, encryptionPubli
|
|||
}
|
||||
|
||||
export async function seedChallenge(overrides?: Partial<typeof rotateChallengeTokens.$inferInsert>) {
|
||||
const keys = generateKeypair()
|
||||
const keys = generateEnvKeyPair()
|
||||
const defaults = {
|
||||
id: crypto.randomUUID(),
|
||||
serverUrl: "https://test-server.com",
|
||||
|
|
|
|||
|
|
@ -9,11 +9,11 @@
|
|||
* - Blacklists server after too many failed attempts
|
||||
* - 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 { 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")
|
||||
|
||||
|
|
@ -53,8 +53,8 @@ interface InitChallenges {
|
|||
|
||||
function solveInitChallenges(
|
||||
challenges: InitChallenges,
|
||||
oldKeys: ReturnType<typeof generateKeypair>,
|
||||
newKeys: ReturnType<typeof generateKeypair>,
|
||||
oldKeys: ReturnType<typeof generateEnvKeyPair>,
|
||||
newKeys: ReturnType<typeof generateEnvKeyPair>,
|
||||
) {
|
||||
const oldSigningSecret = new Uint8Array(Buffer.from(oldKeys.signingSecretKey, "base64"))
|
||||
const newSigningSecret = new Uint8Array(Buffer.from(newKeys.signingSecretKey, "base64"))
|
||||
|
|
@ -73,7 +73,7 @@ function solveInitChallenges(
|
|||
// rotate/init tests
|
||||
// ---------------------------------------------------------------------------
|
||||
test("init rejects unregistered server", async ({ request }) => {
|
||||
const newKeys = generateKeypair()
|
||||
const newKeys = generateEnvKeyPair()
|
||||
const res = await request.post("/discover/rotate/init", {
|
||||
data: {
|
||||
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 }) => {
|
||||
const keys = generateKeypair()
|
||||
const keys = generateEnvKeyPair()
|
||||
await seedServer(SERVER_URL, keys.signingPublicKey, keys.encryptionPublicKey)
|
||||
const res = await request.post("/discover/rotate/init", {
|
||||
data: {
|
||||
|
|
@ -99,8 +99,8 @@ test("init rejects same keys as currently registered", async ({ request }) => {
|
|||
})
|
||||
|
||||
test("init issues 4 challenges", async ({ request }) => {
|
||||
const oldKeys = generateKeypair()
|
||||
const newKeys = generateKeypair()
|
||||
const oldKeys = generateEnvKeyPair()
|
||||
const newKeys = generateEnvKeyPair()
|
||||
await seedServer(SERVER_URL, oldKeys.signingPublicKey, oldKeys.encryptionPublicKey)
|
||||
|
||||
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 }) => {
|
||||
const oldKeys = generateKeypair()
|
||||
const newKeys1 = generateKeypair()
|
||||
const newKeys2 = generateKeypair()
|
||||
const oldKeys = generateEnvKeyPair()
|
||||
const newKeys1 = generateEnvKeyPair()
|
||||
const newKeys2 = generateEnvKeyPair()
|
||||
await seedServer(SERVER_URL, oldKeys.signingPublicKey, oldKeys.encryptionPublicKey)
|
||||
|
||||
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 }) => {
|
||||
const oldKeys = generateKeypair()
|
||||
const newKeys = generateKeypair()
|
||||
const oldKeys = generateEnvKeyPair()
|
||||
const newKeys = generateEnvKeyPair()
|
||||
await seedServer(SERVER_URL, oldKeys.signingPublicKey, oldKeys.encryptionPublicKey)
|
||||
|
||||
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 }) => {
|
||||
const oldKeys = generateKeypair()
|
||||
const newKeys = generateKeypair()
|
||||
const oldKeys = generateEnvKeyPair()
|
||||
const newKeys = generateEnvKeyPair()
|
||||
await seedServer(SERVER_URL, oldKeys.signingPublicKey, oldKeys.encryptionPublicKey)
|
||||
|
||||
debug("test: blacklists – calling init")
|
||||
|
|
@ -240,8 +240,8 @@ test("confirm blacklists after too many failed attempts", async ({ request }) =>
|
|||
// Full init → confirm happy path
|
||||
// ---------------------------------------------------------------------------
|
||||
test("full rotation flow: init → solve → confirm rotates both keys", async ({ request }) => {
|
||||
const oldKeys = generateKeypair()
|
||||
const newKeys = generateKeypair()
|
||||
const oldKeys = generateEnvKeyPair()
|
||||
const newKeys = generateEnvKeyPair()
|
||||
await seedServer(SERVER_URL, oldKeys.signingPublicKey, oldKeys.encryptionPublicKey)
|
||||
|
||||
debug("test: full flow – calling init")
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ interface FedKeys {
|
|||
encryptionSecretKey: string;
|
||||
}
|
||||
|
||||
function generateKeypair(): FedKeys {
|
||||
function generateEnvKeyPair(): FedKeys {
|
||||
const signing = nacl.sign.keyPair();
|
||||
const encryption = nacl.box.keyPair();
|
||||
return {
|
||||
|
|
@ -350,7 +350,7 @@ if (!isFallbackMode) {
|
|||
{
|
||||
const testName = "reject mismatched signing key";
|
||||
try {
|
||||
const fakeKeys = generateKeypair();
|
||||
const fakeKeys = generateEnvKeyPair();
|
||||
|
||||
const innerPayload = JSON.stringify({ action: "bad-key-test" });
|
||||
const targetEncKey = new Uint8Array(Buffer.from(targetInfo.encryptionPublicKey, "base64"));
|
||||
|
|
@ -396,7 +396,7 @@ if (!isFallbackMode) {
|
|||
{
|
||||
const testName = "reject unknown sender";
|
||||
try {
|
||||
const unknownKeys = generateKeypair();
|
||||
const unknownKeys = generateEnvKeyPair();
|
||||
const unknownOrigin = "https://totally-unknown-federation-" + crypto.randomUUID().slice(0, 8) + ".test";
|
||||
|
||||
const innerPayload = JSON.stringify({ action: "unknown-sender-test" });
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue