From 66ebebd105a5d324bc32617cb31f8ce2713abc77 Mon Sep 17 00:00:00 2001 From: Nixyan Date: Tue, 5 May 2026 11:40:14 -0300 Subject: [PATCH] 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 --- .cursor/mcp.json | 11 + .env.local.example | 4 +- bun.lock | 255 ++++++++------- drizzle/meta/_journal.json | 7 + next.config.ts | 3 + package.json | 48 +-- rotateKeys.ts | 4 +- src/app/PostTestForm.tsx | 28 +- src/app/api/dev/relay-discover/route.ts | 56 ++++ src/app/auth/components/SignUpForm.tsx | 15 +- src/app/auth/page.tsx | 2 +- src/app/layout.tsx | 7 +- src/app/manifest.ts | 25 ++ src/app/page.tsx | 18 +- src/components/main/CreateIdentity.tsx | 207 ++++++++++++ src/components/ui/accordion.tsx | 66 ++++ src/components/ui/switch.tsx | 35 +++ src/components/ui/tooltip.tsx | 57 ++++ src/instrumentation.ts | 2 +- src/lib/auth-client.ts | 4 +- src/lib/auth.ts | 10 +- src/lib/bull/index.ts | 1 - src/lib/db/schema/index.ts | 44 +++ src/lib/dexie/index.ts | 112 +++++++ src/lib/federation/keygen.ts | 4 +- src/lib/federation/keytools.ts | 30 +- src/lib/identity/postSignature.ts | 27 ++ src/lib/identity/sign.ts | 64 ++++ .../{ => federation}/server/federation.ts | 2 +- src/lib/plugins/oven/README.md | 3 + src/lib/plugins/oven/client/index.ts | 195 ++++++++++++ src/lib/plugins/oven/server/index.ts | 296 ++++++++++++++++++ src/lib/plugins/oven/server/schema.ts | 79 +++++ src/lib/plugins/{ => social}/client/social.ts | 108 ++++--- .../server/helpers}/endpoints/blocks.ts | 0 .../server/helpers}/endpoints/follows.ts | 0 .../server/helpers}/endpoints/index.ts | 0 .../server/helpers}/endpoints/mutes.ts | 0 .../server/helpers}/endpoints/posts.ts | 62 +++- .../server/helpers}/social.ts | 16 +- src/lib/plugins/{ => social}/server/social.ts | 4 +- .../server}/minio.client.ts | 0 src/lib/zod/methods/PostFederationSchema.ts | 2 +- tests/attacks.test.ts | 40 +-- tests/helpers/db.ts | 4 +- tests/key.test.ts | 36 +-- tests/proxies/follow.ts | 6 +- 47 files changed, 1753 insertions(+), 246 deletions(-) create mode 100644 .cursor/mcp.json create mode 100644 src/app/api/dev/relay-discover/route.ts create mode 100644 src/app/manifest.ts create mode 100644 src/components/main/CreateIdentity.tsx create mode 100644 src/components/ui/accordion.tsx create mode 100644 src/components/ui/switch.tsx create mode 100644 src/components/ui/tooltip.tsx create mode 100644 src/lib/dexie/index.ts create mode 100644 src/lib/identity/postSignature.ts create mode 100644 src/lib/identity/sign.ts rename src/lib/plugins/{ => federation}/server/federation.ts (99%) create mode 100644 src/lib/plugins/oven/README.md create mode 100644 src/lib/plugins/oven/client/index.ts create mode 100644 src/lib/plugins/oven/server/index.ts create mode 100644 src/lib/plugins/oven/server/schema.ts rename src/lib/plugins/{ => social}/client/social.ts (61%) rename src/lib/plugins/{server/helpers/social => social/server/helpers}/endpoints/blocks.ts (100%) rename src/lib/plugins/{server/helpers/social => social/server/helpers}/endpoints/follows.ts (100%) rename src/lib/plugins/{server/helpers/social => social/server/helpers}/endpoints/index.ts (100%) rename src/lib/plugins/{server/helpers/social => social/server/helpers}/endpoints/mutes.ts (100%) rename src/lib/plugins/{server/helpers/social => social/server/helpers}/endpoints/posts.ts (77%) rename src/lib/plugins/{server/helpers/social => social/server/helpers}/social.ts (90%) rename src/lib/plugins/{ => social}/server/social.ts (65%) rename src/lib/plugins/{server/storage => storage/server}/minio.client.ts (100%) diff --git a/.cursor/mcp.json b/.cursor/mcp.json new file mode 100644 index 0000000..4bafd73 --- /dev/null +++ b/.cursor/mcp.json @@ -0,0 +1,11 @@ +{ + "mcpServers": { + "next-devtools": { + "command": "npx", + "args": [ + "-y", + "next-devtools-mcp@latest" + ] + } + } +} \ No newline at end of file diff --git a/.env.local.example b/.env.local.example index 29b1c3f..4305927 100644 --- a/.env.local.example +++ b/.env.local.example @@ -24,4 +24,6 @@ MINIO_ENDPOINT= MINIO_PORT= MINIO_USE_SSL= MINIO_ACCESS_KEY= -MINIO_SECRET_KEY= \ No newline at end of file +MINIO_SECRET_KEY= + +NEXT_PUBLIC_GIT_URL= \ No newline at end of file diff --git a/bun.lock b/bun.lock index 54c5c42..ba13cbf 100644 --- a/bun.lock +++ b/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=="], diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 3f6280f..a5eb1f3 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -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 } ] } \ No newline at end of file diff --git a/next.config.ts b/next.config.ts index d0643a3..509961a 100644 --- a/next.config.ts +++ b/next.config.ts @@ -7,6 +7,9 @@ const nextConfig: NextConfig = { reactCompiler: true, allowedDevOrigins, output: "standalone", + experimental: { + webpackMemoryOptimizations: true + } }; export default nextConfig; diff --git a/package.json b/package.json index f5e3490..1c80a0d 100644 --- a/package.json +++ b/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", diff --git a/rotateKeys.ts b/rotateKeys.ts index cdb3ee6..54e3302 100644 --- a/rotateKeys.ts +++ b/rotateKeys.ts @@ -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(); } // --------------------------------------------------------------------------- diff --git a/src/app/PostTestForm.tsx b/src/app/PostTestForm.tsx index d58dbe5..17eb200 100644 --- a/src/app/PostTestForm.tsx +++ b/src/app/PostTestForm.tsx @@ -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([]); + const [password, setPassword] = useState(""); const [status, setStatus] = useState(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() { )} +
+ + setPassword(e.target.value)} + placeholder="••••••••••••" + style={{ width: "100%", padding: 8, fontSize: 14 }} + /> +
+ + + + + + )} + /> + + {password.length > 0 && ( +
+ + Requirements + +
    + {requirements.map((req) => { + const met = req.test(password); + return ( +
  • + + {req.label} +
  • + ); + })} +
+
+ )} + + + + + + + What if I lose my password? + + + +
    +
  • + + Your identity key is encrypted locally with this password. There is no recovery mechanism. +
  • +
  • + + Losing it means permanent loss of access to your encrypted messages and posts. +
  • +
  • + + Store it somewhere safe — a password manager or written offline. +
  • +
  • + + Losing your identity means that all your messages are permanently lost and your old posts won't hold a valid signature. +
  • +
+
+
+
+ + + + + + + + ) +} \ No newline at end of file diff --git a/src/components/ui/accordion.tsx b/src/components/ui/accordion.tsx new file mode 100644 index 0000000..91e3a25 --- /dev/null +++ b/src/components/ui/accordion.tsx @@ -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) { + return +} + +function AccordionItem({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AccordionTrigger({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + svg]:rotate-180", + className + )} + {...props} + > + {children} + + + + ) +} + +function AccordionContent({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + +
{children}
+
+ ) +} + +export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } diff --git a/src/components/ui/switch.tsx b/src/components/ui/switch.tsx new file mode 100644 index 0000000..8baa844 --- /dev/null +++ b/src/components/ui/switch.tsx @@ -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 & { + size?: "sm" | "default" +}) { + return ( + + + + ) +} + +export { Switch } diff --git a/src/components/ui/tooltip.tsx b/src/components/ui/tooltip.tsx new file mode 100644 index 0000000..ec65c1e --- /dev/null +++ b/src/components/ui/tooltip.tsx @@ -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) { + return ( + + ) +} + +function Tooltip({ + ...props +}: React.ComponentProps) { + return +} + +function TooltipTrigger({ + ...props +}: React.ComponentProps) { + return +} + +function TooltipContent({ + className, + sideOffset = 0, + children, + ...props +}: React.ComponentProps) { + return ( + + + {children} + + + + ) +} + +export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } diff --git a/src/instrumentation.ts b/src/instrumentation.ts index 05a09e2..55f0dd7 100644 --- a/src/instrumentation.ts +++ b/src/instrumentation.ts @@ -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(); } } diff --git a/src/lib/auth-client.ts b/src/lib/auth-client.ts index 5821fd8..24ba0cc 100644 --- a/src/lib/auth-client.ts +++ b/src/lib/auth-client.ts @@ -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(), ] }) \ No newline at end of file diff --git a/src/lib/auth.ts b/src/lib/auth.ts index dc6f439..fd0b688 100644 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -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, - } + }, } } }); diff --git a/src/lib/bull/index.ts b/src/lib/bull/index.ts index 2f0623d..c205cf1 100644 --- a/src/lib/bull/index.ts +++ b/src/lib/bull/index.ts @@ -1,4 +1,3 @@ export { getFederationQueue, getHealthCheckQueue, scheduleHealthCheck } from './queues'; export type { FederationDeliveryJob, HealthCheckJob } from './queues'; -export { startFederationWorker } from './worker'; diff --git a/src/lib/db/schema/index.ts b/src/lib/db/schema/index.ts index 68449dd..63abcc0 100644 --- a/src/lib/db/schema/index.ts +++ b/src/lib/db/schema/index.ts @@ -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], + }), +})); diff --git a/src/lib/dexie/index.ts b/src/lib/dexie/index.ts new file mode 100644 index 0000000..c923353 --- /dev/null +++ b/src/lib/dexie/index.ts @@ -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; +} + +class SipherDb extends Dexie { + decryptedEvents!: EntityTable; + rooms!: EntityTable; + syncState!: EntityTable; + identity!: EntityTable; + + // 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; +} diff --git a/src/lib/federation/keygen.ts b/src/lib/federation/keygen.ts index 9df8ac5..5fe48e1 100644 --- a/src/lib/federation/keygen.ts +++ b/src/lib/federation/keygen.ts @@ -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(); diff --git a/src/lib/federation/keytools.ts b/src/lib/federation/keytools.ts index d95df52..a32479c 100644 --- a/src/lib/federation/keytools.ts +++ b/src/lib/federation/keytools.ts @@ -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 { +// const olm = new Olm(); +// } + export function getOwnEncryptionPublicKey(): Uint8Array { return new Uint8Array(Buffer.from(process.env.FEDERATION_ENCRYPTION_PUBLIC_KEY!, "base64")) } diff --git a/src/lib/identity/postSignature.ts b/src/lib/identity/postSignature.ts new file mode 100644 index 0000000..af9cfb2 --- /dev/null +++ b/src/lib/identity/postSignature.ts @@ -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); +} diff --git a/src/lib/identity/sign.ts b/src/lib/identity/sign.ts new file mode 100644 index 0000000..e91c716 --- /dev/null +++ b/src/lib/identity/sign.ts @@ -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 { + 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); + } +} diff --git a/src/lib/plugins/server/federation.ts b/src/lib/plugins/federation/server/federation.ts similarity index 99% rename from src/lib/plugins/server/federation.ts rename to src/lib/plugins/federation/server/federation.ts index 46a2988..5005cac 100644 --- a/src/lib/plugins/server/federation.ts +++ b/src/lib/plugins/federation/server/federation.ts @@ -130,7 +130,7 @@ export const federation = () => { index: false } } - } + }, } } satisfies BetterAuthPlugin; } diff --git a/src/lib/plugins/oven/README.md b/src/lib/plugins/oven/README.md new file mode 100644 index 0000000..175d066 --- /dev/null +++ b/src/lib/plugins/oven/README.md @@ -0,0 +1,3 @@ +## Oven + +This is where the E2EE magic happens, here you'll find the Client and Server side OLM things \ No newline at end of file diff --git a/src/lib/plugins/oven/client/index.ts b/src/lib/plugins/oven/client/index.ts new file mode 100644 index 0000000..869f304 --- /dev/null +++ b/src/lib/plugins/oven/client/index.ts @@ -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, + 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((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 ( + userId: string, + password: string, + fn: (sign: (message: Uint8Array) => Uint8Array, publicKey: Uint8Array) => Promise | T, + ): Promise => { + 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; +}; diff --git a/src/lib/plugins/oven/server/index.ts b/src/lib/plugins/oven/server/index.ts new file mode 100644 index 0000000..158581d --- /dev/null +++ b/src/lib/plugins/oven/server/index.ts @@ -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["device_keys"]; + one_time_keys: Record; + fallback_keys: Record; +} + +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: { ":": SignedKey, ... }, + * fallback_keys: { ":": 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 = { ...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 = { ...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; +}; diff --git a/src/lib/plugins/oven/server/schema.ts b/src/lib/plugins/oven/server/schema.ts new file mode 100644 index 0000000..dde0c03 --- /dev/null +++ b/src/lib/plugins/oven/server/schema.ts @@ -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; +export type SignedFallbackKey = z.infer; + +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; + +/** + * 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; diff --git a/src/lib/plugins/client/social.ts b/src/lib/plugins/social/client/social.ts similarity index 61% rename from src/lib/plugins/client/social.ts rename to src/lib/plugins/social/client/social.ts index e3ab3e3..9d5b162 100644 --- a/src/lib/plugins/client/social.ts +++ b/src/lib/plugins/social/client/social.ts @@ -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, getActions($fetch, $store, options) { return { - createPost: async (content: z.infer) => { - + /** + * 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, + 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 = { - 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 = { + 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; diff --git a/src/lib/plugins/server/helpers/social/endpoints/blocks.ts b/src/lib/plugins/social/server/helpers/endpoints/blocks.ts similarity index 100% rename from src/lib/plugins/server/helpers/social/endpoints/blocks.ts rename to src/lib/plugins/social/server/helpers/endpoints/blocks.ts diff --git a/src/lib/plugins/server/helpers/social/endpoints/follows.ts b/src/lib/plugins/social/server/helpers/endpoints/follows.ts similarity index 100% rename from src/lib/plugins/server/helpers/social/endpoints/follows.ts rename to src/lib/plugins/social/server/helpers/endpoints/follows.ts diff --git a/src/lib/plugins/server/helpers/social/endpoints/index.ts b/src/lib/plugins/social/server/helpers/endpoints/index.ts similarity index 100% rename from src/lib/plugins/server/helpers/social/endpoints/index.ts rename to src/lib/plugins/social/server/helpers/endpoints/index.ts diff --git a/src/lib/plugins/server/helpers/social/endpoints/mutes.ts b/src/lib/plugins/social/server/helpers/endpoints/mutes.ts similarity index 100% rename from src/lib/plugins/server/helpers/social/endpoints/mutes.ts rename to src/lib/plugins/social/server/helpers/endpoints/mutes.ts diff --git a/src/lib/plugins/server/helpers/social/endpoints/posts.ts b/src/lib/plugins/social/server/helpers/endpoints/posts.ts similarity index 77% rename from src/lib/plugins/server/helpers/social/endpoints/posts.ts rename to src/lib/plugins/social/server/helpers/endpoints/posts.ts index 2935047..2b6d705 100644 --- a/src/lib/plugins/server/helpers/social/endpoints/posts.ts +++ b/src/lib/plugins/social/server/helpers/endpoints/posts.ts @@ -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 }); diff --git a/src/lib/plugins/server/helpers/social/social.ts b/src/lib/plugins/social/server/helpers/social.ts similarity index 90% rename from src/lib/plugins/server/helpers/social/social.ts rename to src/lib/plugins/social/server/helpers/social.ts index 572f2ab..edf139f 100644 --- a/src/lib/plugins/server/helpers/social/social.ts +++ b/src/lib/plugins/social/server/helpers/social.ts @@ -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: { diff --git a/src/lib/plugins/server/social.ts b/src/lib/plugins/social/server/social.ts similarity index 65% rename from src/lib/plugins/server/social.ts rename to src/lib/plugins/social/server/social.ts index d10c7bc..51e993b 100644 --- a/src/lib/plugins/server/social.ts +++ b/src/lib/plugins/social/server/social.ts @@ -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 { diff --git a/src/lib/plugins/server/storage/minio.client.ts b/src/lib/plugins/storage/server/minio.client.ts similarity index 100% rename from src/lib/plugins/server/storage/minio.client.ts rename to src/lib/plugins/storage/server/minio.client.ts diff --git a/src/lib/zod/methods/PostFederationSchema.ts b/src/lib/zod/methods/PostFederationSchema.ts index 808be44..a7943f3 100644 --- a/src/lib/zod/methods/PostFederationSchema.ts +++ b/src/lib/zod/methods/PostFederationSchema.ts @@ -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"; diff --git a/tests/attacks.test.ts b/tests/attacks.test.ts index 3c5ebaa..0477153 100644 --- a/tests/attacks.test.ts +++ b/tests/attacks.test.ts @@ -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) diff --git a/tests/helpers/db.ts b/tests/helpers/db.ts index a0f4d38..9fff33d 100644 --- a/tests/helpers/db.ts +++ b/tests/helpers/db.ts @@ -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) { - const keys = generateKeypair() + const keys = generateEnvKeyPair() const defaults = { id: crypto.randomUUID(), serverUrl: "https://test-server.com", diff --git a/tests/key.test.ts b/tests/key.test.ts index 7c5fc7a..9ba9776 100644 --- a/tests/key.test.ts +++ b/tests/key.test.ts @@ -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, - newKeys: ReturnType, + oldKeys: ReturnType, + newKeys: ReturnType, ) { 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") diff --git a/tests/proxies/follow.ts b/tests/proxies/follow.ts index 2545508..c8d30d9 100644 --- a/tests/proxies/follow.ts +++ b/tests/proxies/follow.ts @@ -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" });