From 9ba20c8a9eceb601523f57dfb0e62a0cfa8319ba Mon Sep 17 00:00:00 2001 From: Richard R Date: Sat, 24 Jan 2026 17:36:11 -0700 Subject: [PATCH] feat(auth): add user authentication and rate limiting - Implement user sign-in, sign-up, and account management using better-auth - Add rate limiting for TTS API with daily character limits for authenticated and anonymous users - Integrate SQLite and PostgreSQL database support for user sessions and data persistence - Update UI components to include authentication flows, user menu, and privacy popup - Modify Dockerfile and package.json for new dependencies and entrypoint script - Bump version to v1.3.0 --- Dockerfile | 5 + README.md | 7 +- .../2026-01-24T20-06-28.748Z.sql | 13 + docker-entrypoint.sh | 11 + next.config.ts | 1 + package.json | 11 +- pnpm-lock.yaml | 1456 ++++++++++++++++- src/app/api/account/delete/route.ts | 36 + src/app/api/auth/[...all]/route.ts | 11 + src/app/api/rate-limit/status/route.ts | 73 + src/app/api/tts/route.ts | 69 +- src/app/layout.tsx | 11 +- src/app/page.tsx | 41 +- src/app/providers.tsx | 25 +- src/app/signin/page.tsx | 286 ++++ src/app/signup/page.tsx | 240 +++ src/components/SettingsModal.tsx | 316 ++-- src/components/Spinner.tsx | 8 +- src/components/auth/AuthLoader.tsx | 76 + src/components/auth/SessionManager.tsx | 48 + src/components/auth/UserMenu.tsx | 66 + src/components/privacy-popup.tsx | 211 +++ src/components/rate-limit-banner.tsx | 85 + src/components/rate-limit-provider.tsx | 206 +++ src/contexts/AuthConfigContext.tsx | 33 + src/hooks/useAuth.ts | 36 + src/lib/auth-client.ts | 35 + src/lib/server/auth-config.ts | 17 + src/lib/server/auth.ts | 106 ++ src/lib/server/db-adapter.ts | 125 ++ src/lib/server/db.ts | 45 + src/lib/server/rate-limiter.ts | 230 +++ src/lib/session-utils.ts | 26 + src/types/config.ts | 4 + tests/helpers.ts | 10 +- 35 files changed, 3780 insertions(+), 199 deletions(-) create mode 100644 better-auth_migrations/2026-01-24T20-06-28.748Z.sql create mode 100755 docker-entrypoint.sh create mode 100644 src/app/api/account/delete/route.ts create mode 100644 src/app/api/auth/[...all]/route.ts create mode 100644 src/app/api/rate-limit/status/route.ts create mode 100644 src/app/signin/page.tsx create mode 100644 src/app/signup/page.tsx create mode 100644 src/components/auth/AuthLoader.tsx create mode 100644 src/components/auth/SessionManager.tsx create mode 100644 src/components/auth/UserMenu.tsx create mode 100644 src/components/privacy-popup.tsx create mode 100644 src/components/rate-limit-banner.tsx create mode 100644 src/components/rate-limit-provider.tsx create mode 100644 src/contexts/AuthConfigContext.tsx create mode 100644 src/hooks/useAuth.ts create mode 100644 src/lib/auth-client.ts create mode 100644 src/lib/server/auth-config.ts create mode 100644 src/lib/server/auth.ts create mode 100644 src/lib/server/db-adapter.ts create mode 100644 src/lib/server/db.ts create mode 100644 src/lib/server/rate-limiter.ts create mode 100644 src/lib/session-utils.ts diff --git a/Dockerfile b/Dockerfile index 255275d..94aef56 100644 --- a/Dockerfile +++ b/Dockerfile @@ -65,4 +65,9 @@ ENV LD_LIBRARY_PATH=/opt/whisper.cpp/build EXPOSE 3003 # Start the application +# Copy entrypoint script +COPY docker-entrypoint.sh /usr/local/bin/ +RUN chmod +x /usr/local/bin/docker-entrypoint.sh + +ENTRYPOINT ["docker-entrypoint.sh"] CMD ["pnpm", "start"] diff --git a/README.md b/README.md index 44354eb..fa51d1f 100644 --- a/README.md +++ b/README.md @@ -241,7 +241,12 @@ Optionally required for different features: ``` > Note: The base URL for the TTS API should be accessible and relative to the Next.js server -4. Start the development server: +4. Run SQLite creation: + ```bash + npx @better-auth/cli migrate -y + ``` + +5. Start the development server: With pnpm (recommended): ```bash diff --git a/better-auth_migrations/2026-01-24T20-06-28.748Z.sql b/better-auth_migrations/2026-01-24T20-06-28.748Z.sql new file mode 100644 index 0000000..e57bdc3 --- /dev/null +++ b/better-auth_migrations/2026-01-24T20-06-28.748Z.sql @@ -0,0 +1,13 @@ +create table "user" ("id" text not null primary key, "name" text not null, "email" text not null unique, "emailVerified" boolean not null, "image" text, "createdAt" timestamptz default CURRENT_TIMESTAMP not null, "updatedAt" timestamptz default CURRENT_TIMESTAMP not null, "isAnonymous" boolean); + +create table "session" ("id" text not null primary key, "expiresAt" timestamptz not null, "token" text not null unique, "createdAt" timestamptz default CURRENT_TIMESTAMP not null, "updatedAt" timestamptz not null, "ipAddress" text, "userAgent" text, "userId" text not null references "user" ("id") on delete cascade); + +create table "account" ("id" text not null primary key, "accountId" text not null, "providerId" text not null, "userId" text not null references "user" ("id") on delete cascade, "accessToken" text, "refreshToken" text, "idToken" text, "accessTokenExpiresAt" timestamptz, "refreshTokenExpiresAt" timestamptz, "scope" text, "password" text, "createdAt" timestamptz default CURRENT_TIMESTAMP not null, "updatedAt" timestamptz not null); + +create table "verification" ("id" text not null primary key, "identifier" text not null, "value" text not null, "expiresAt" timestamptz not null, "createdAt" timestamptz default CURRENT_TIMESTAMP not null, "updatedAt" timestamptz default CURRENT_TIMESTAMP not null); + +create index "session_userId_idx" on "session" ("userId"); + +create index "account_userId_idx" on "account" ("userId"); + +create index "verification_identifier_idx" on "verification" ("identifier"); \ No newline at end of file diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh new file mode 100755 index 0000000..c913a3f --- /dev/null +++ b/docker-entrypoint.sh @@ -0,0 +1,11 @@ +#!/bin/sh +set -e + +# Run migrations if we have the SQLite file path set or default +if [ -z "$POSTGRES_URL" ]; then + echo "Running SQLite migrations..." + npx @better-auth/cli migrate -y +fi + +# Start the application +exec "$@" diff --git a/next.config.ts b/next.config.ts index e7f0bee..c6b910e 100644 --- a/next.config.ts +++ b/next.config.ts @@ -6,6 +6,7 @@ const nextConfig: NextConfig = { canvas: './empty-module.ts', }, }, + serverExternalPackages: ["better-sqlite3"], }; export default nextConfig; diff --git a/package.json b/package.json index 23dd947..fa3a5f2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "openreader-webui", - "version": "v1.2.1", + "version": "v1.3.0", "private": true, "scripts": { "dev": "next dev --turbopack -p 3003", @@ -10,10 +10,13 @@ "test": "playwright test" }, "dependencies": { + "@better-auth/cli": "1.4.17", "@headlessui/react": "^2.2.9", "@types/howler": "^2.2.12", "@types/uuid": "^10.0.0", "@vercel/analytics": "^1.6.1", + "better-auth": "^1.4.17", + "better-sqlite3": "^12.6.2", "cmpstr": "^3.2.0", "compromise": "^14.14.5", "core-js": "^3.48.0", @@ -25,6 +28,7 @@ "next": "^15.5.9", "openai": "^6.16.0", "pdfjs-dist": "4.8.69", + "pg": "^8.17.2", "react": "^19.2.3", "react-dnd": "^16.0.1", "react-dnd-html5-backend": "^16.0.1", @@ -41,7 +45,9 @@ "@eslint/eslintrc": "^3.3.3", "@playwright/test": "^1.58.0", "@tailwindcss/typography": "^0.5.19", + "@types/better-sqlite3": "^7.6.13", "@types/node": "^20.19.30", + "@types/pg": "^8.16.0", "@types/react": "^19.2.9", "@types/react-dom": "^19.2.3", "eslint": "^9.39.2", @@ -51,6 +57,9 @@ "typescript": "^5.9.3" }, "pnpm": { + "onlyBuiltDependencies": [ + "better-sqlite3" + ], "overrides": { "lodash": "^4.17.23", "@xmldom/xmldom": "^0.9.8", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e9212d5..32800bb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -13,6 +13,9 @@ importers: .: dependencies: + '@better-auth/cli': + specifier: 1.4.17 + version: 1.4.17(@better-fetch/fetch@1.1.21)(@types/better-sqlite3@7.6.13)(better-call@1.1.8(zod@4.3.6))(jose@6.1.3)(kysely@0.28.10)(nanostores@1.1.0)(next@15.5.9(@babel/core@7.28.6)(@playwright/test@1.58.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@headlessui/react': specifier: ^2.2.9 version: 2.2.9(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -24,7 +27,13 @@ importers: version: 10.0.0 '@vercel/analytics': specifier: ^1.6.1 - version: 1.6.1(next@15.5.9(@playwright/test@1.58.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3) + version: 1.6.1(next@15.5.9(@babel/core@7.28.6)(@playwright/test@1.58.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3) + better-auth: + specifier: ^1.4.17 + version: 1.4.17(@prisma/client@5.22.0)(better-sqlite3@12.6.2)(drizzle-orm@0.41.0(@prisma/client@5.22.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.16.0)(better-sqlite3@12.6.2)(kysely@0.28.10)(pg@8.17.2))(next@15.5.9(@babel/core@7.28.6)(@playwright/test@1.58.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(pg@8.17.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + better-sqlite3: + specifier: ^12.6.2 + version: 12.6.2 cmpstr: specifier: ^3.2.0 version: 3.2.0 @@ -51,13 +60,16 @@ importers: version: 11.2.4 next: specifier: ^15.5.9 - version: 15.5.9(@playwright/test@1.58.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + version: 15.5.9(@babel/core@7.28.6)(@playwright/test@1.58.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) openai: specifier: ^6.16.0 version: 6.16.0(zod@4.3.6) pdfjs-dist: specifier: 4.8.69 version: 4.8.69 + pg: + specifier: ^8.17.2 + version: 8.17.2 react: specifier: ^19.2.3 version: 19.2.3 @@ -101,9 +113,15 @@ importers: '@tailwindcss/typography': specifier: ^0.5.19 version: 0.5.19(tailwindcss@3.4.19) + '@types/better-sqlite3': + specifier: ^7.6.13 + version: 7.6.13 '@types/node': specifier: ^20.19.30 version: 20.19.30 + '@types/pg': + specifier: ^8.16.0 + version: 8.16.0 '@types/react': specifier: ^19.2.9 version: 19.2.9 @@ -132,10 +150,212 @@ packages: resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} engines: {node: '>=10'} + '@babel/code-frame@7.28.6': + resolution: {integrity: sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.28.6': + resolution: {integrity: sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.28.6': + resolution: {integrity: sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.28.6': + resolution: {integrity: sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-annotate-as-pure@7.27.3': + resolution: {integrity: sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.28.6': + resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-create-class-features-plugin@7.28.6': + resolution: {integrity: sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-member-expression-to-functions@7.28.5': + resolution: {integrity: sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.28.6': + resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.28.6': + resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-optimise-call-expression@7.27.1': + resolution: {integrity: sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-plugin-utils@7.28.6': + resolution: {integrity: sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==} + engines: {node: '>=6.9.0'} + + '@babel/helper-replace-supers@7.28.6': + resolution: {integrity: sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-skip-transparent-expression-wrappers@7.27.1': + resolution: {integrity: sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.28.6': + resolution: {integrity: sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.28.6': + resolution: {integrity: sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-syntax-jsx@7.28.6': + resolution: {integrity: sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-typescript@7.28.6': + resolution: {integrity: sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-modules-commonjs@7.28.6': + resolution: {integrity: sha512-jppVbf8IV9iWWwWTQIxJMAJCWBuuKx71475wHwYytrRGQ2CWiDvYlADQno3tcYpS/T2UUWFQp3nVtYfK/YBQrA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-display-name@7.28.0': + resolution: {integrity: sha512-D6Eujc2zMxKjfa4Zxl4GHMsmhKKZ9VpcqIchJLvwTxad9zWIYulwYItBovpDOoNLISpcZSXoDJ5gaGbQUDqViA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-development@7.27.1': + resolution: {integrity: sha512-ykDdF5yI4f1WrAolLqeF3hmYU12j9ntLQl/AOG1HAS21jxyg1Q0/J/tpREuYLfatGdGmXp/3yS0ZA76kOlVq9Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx@7.28.6': + resolution: {integrity: sha512-61bxqhiRfAACulXSLd/GxqmAedUSrRZIu/cbaT18T1CetkTmtDN15it7i80ru4DVqRK1WMxQhXs+Lf9kajm5Ow==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-pure-annotations@7.27.1': + resolution: {integrity: sha512-JfuinvDOsD9FVMTHpzA/pBLisxpv1aSf+OIV8lgH3MuWrks19R27e6a6DipIg4aX1Zm9Wpb04p8wljfKrVSnPA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-typescript@7.28.6': + resolution: {integrity: sha512-0YWL2RFxOqEm9Efk5PvreamxPME8OyY0wM5wh5lHjF+VtVhdneCWGzZeSqzOfiobVqQaNCd2z0tQvnI9DaPWPw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/preset-react@7.28.5': + resolution: {integrity: sha512-Z3J8vhRq7CeLjdC58jLv4lnZ5RKFUJWqH5emvxmv9Hv3BD1T9R/Im713R4MTKwvFaV74ejZ3sM01LyEKk4ugNQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/preset-typescript@7.28.5': + resolution: {integrity: sha512-+bQy5WOI2V6LJZpPVxY+yp66XdZ2yifu0Mc1aP5CQKgjn4QM5IN2i5fAZ4xKop47pr8rpVhiAeu+nDQa12C8+g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/runtime@7.28.6': resolution: {integrity: sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==} engines: {node: '>=6.9.0'} + '@babel/template@7.28.6': + resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.28.6': + resolution: {integrity: sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.28.6': + resolution: {integrity: sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==} + engines: {node: '>=6.9.0'} + + '@better-auth/cli@1.4.17': + resolution: {integrity: sha512-GT0epRZCRAxwDnDNVsAVXx2W4PNjBfm8NwrtjxfYKRHsnrCHkT0N/cZyKCYG1sReN/wlsyYk8bJ6f1N1P4VT8w==} + hasBin: true + + '@better-auth/core@1.4.17': + resolution: {integrity: sha512-WSaEQDdUO6B1CzAmissN6j0lx9fM9lcslEYzlApB5UzFaBeAOHNUONTdglSyUs6/idiZBoRvt0t/qMXCgIU8ug==} + peerDependencies: + '@better-auth/utils': 0.3.0 + '@better-fetch/fetch': 1.1.21 + better-call: 1.1.8 + jose: ^6.1.0 + kysely: ^0.28.5 + nanostores: ^1.0.1 + + '@better-auth/telemetry@1.4.17': + resolution: {integrity: sha512-R1BC4e/bNjQbXu7lG6ubpgmsPj7IMqky5DvMlzAtnAJWJhh99pMh/n6w5gOHa0cqDZgEAuj75IPTxv+q3YiInA==} + peerDependencies: + '@better-auth/core': 1.4.17 + + '@better-auth/utils@0.3.0': + resolution: {integrity: sha512-W+Adw6ZA6mgvnSnhOki270rwJ42t4XzSK6YWGF//BbVXL6SwCLWfyzBc1lN2m/4RM28KubdBKQ4X5VMoLRNPQw==} + + '@better-fetch/fetch@1.1.21': + resolution: {integrity: sha512-/ImESw0sskqlVR94jB+5+Pxjf+xBwDZF/N5+y2/q4EqD7IARUTSpPfIo8uf39SYpCxyOCtbyYpUrZ3F/k0zT4A==} + + '@chevrotain/cst-dts-gen@10.5.0': + resolution: {integrity: sha512-lhmC/FyqQ2o7pGK4Om+hzuDrm9rhFYIJ/AXoQBeongmn870Xeb0L6oGEiuR8nohFNL5sMaQEJWCxr1oIVIVXrw==} + + '@chevrotain/gast@10.5.0': + resolution: {integrity: sha512-pXdMJ9XeDAbgOWKuD1Fldz4ieCs6+nLNmyVhe2gZVqoO7v8HXuHYs5OV2EzUtbuai37TlOAQHrTDvxMnvMJz3A==} + + '@chevrotain/types@10.5.0': + resolution: {integrity: sha512-f1MAia0x/pAVPWH/T73BJVyO2XU5tI4/iE7cnxb7tqdNTNhQI3Uq3XkqcoteTmD4t1aM0LbHCJOhgIDn07kl2A==} + + '@chevrotain/utils@10.5.0': + resolution: {integrity: sha512-hBzuU5+JjB2cqNZyszkDHZgOSrUUT8V3dhgRl8Q9Gp6dAj/H5+KILGjbhDpc3Iy9qmqlm/akuOI2ut9VUtzJxQ==} + + '@clack/core@0.5.0': + resolution: {integrity: sha512-p3y0FIOwaYRUPRcMO7+dlmLh8PSRcrjuTndsiA0WAFbWES0mLZlrjVoBRZ9DzkPFJZG6KGkJmoEAY0ZcVWTkow==} + + '@clack/prompts@0.11.0': + resolution: {integrity: sha512-pMN5FcrEw9hUkZA4f+zLlzivQSeQf5dRGJjSUbvVYDLvpKCdQx5OaknvKzgbtXOizhP+SJJJjqEbOe55uKKfAw==} + '@emnapi/core@1.8.1': resolution: {integrity: sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==} @@ -367,6 +587,9 @@ packages: '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + '@jridgewell/resolve-uri@3.1.2': resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} @@ -377,6 +600,10 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@mrleebo/prisma-ast@0.13.1': + resolution: {integrity: sha512-XyroGQXcHrZdvmrGJvsA9KNeOOgGMg1Vg9OlheUsBOSKznLMDl+YChxbkboRHvtFYJEMRYmlV3uoo/njCw05iw==} + engines: {node: '>=16'} + '@napi-rs/wasm-runtime@0.2.12': resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} @@ -434,6 +661,14 @@ packages: cpu: [x64] os: [win32] + '@noble/ciphers@2.1.1': + resolution: {integrity: sha512-bysYuiVfhxNJuldNXlFEitTVdNnYUc+XNJZd7Qm2a5j1vZHgY+fazadNFWFaMK/2vye0JVlxV3gHmC0WDfAOQw==} + engines: {node: '>= 20.19.0'} + + '@noble/hashes@2.0.1': + resolution: {integrity: sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==} + engines: {node: '>= 20.19.0'} + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -455,6 +690,15 @@ packages: engines: {node: '>=18'} hasBin: true + '@prisma/client@5.22.0': + resolution: {integrity: sha512-M0SVXfyHnQREBKxCgyo7sffrKttwE6R8PMq330MIUF0pTwjUhLbW84pFDlf06B27XyCR++VtjugEnIHdr07SVA==} + engines: {node: '>=16.13'} + peerDependencies: + prisma: '*' + peerDependenciesMeta: + prisma: + optional: true + '@react-aria/focus@3.21.3': resolution: {integrity: sha512-FsquWvjSCwC2/sBk4b+OqJyONETUIXQ2vM0YdPAuC+QFQh2DT6TIBo6dOZVSezlhudDla69xFBd6JvCFq1AbUw==} peerDependencies: @@ -507,6 +751,9 @@ packages: '@rushstack/eslint-patch@1.15.0': resolution: {integrity: sha512-ojSshQPKwVvSMR8yT2L/QtUkV5SXi/IfDiJ4/8d6UbTPjiHVmxZzUAzGD8Tzks1b9+qQkZa0isUOvYObedITaw==} + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + '@swc/helpers@0.5.15': resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} @@ -530,6 +777,9 @@ packages: '@tybys/wasm-util@0.10.1': resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} + '@types/better-sqlite3@7.6.13': + resolution: {integrity: sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==} + '@types/debug@4.1.12': resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} @@ -560,6 +810,9 @@ packages: '@types/node@20.19.30': resolution: {integrity: sha512-WJtwWJu7UdlvzEAUm484QNg5eAoq5QR08KDNx7g45Usrs2NtOPiX8ugDqmKdXkyL03rBqU5dYNYVQetEpBHq2g==} + '@types/pg@8.16.0': + resolution: {integrity: sha512-RmhMd/wD+CF8Dfo+cVIy3RR5cl8CyfXQ0tGgW6XBL8L4LM/UTEbNXYRbLwU6w+CgrKBNbrQWt4FUtTfaU5jSYQ==} + '@types/react-dom@19.2.3': resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} peerDependencies: @@ -862,10 +1115,91 @@ packages: base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + baseline-browser-mapping@2.9.18: + resolution: {integrity: sha512-e23vBV1ZLfjb9apvfPk4rHVu2ry6RIr2Wfs+O324okSidrX7pTAnEJPCh/O5BtRlr7QtZI7ktOP3vsqr7Z5XoA==} + hasBin: true + + better-auth@1.4.17: + resolution: {integrity: sha512-VmHGQyKsEahkEs37qguROKg/6ypYpNF13D7v/lkbO7w7Aivz0Bv2h+VyUkH4NzrGY0QBKXi1577mGhDCVwp0ew==} + 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 + peerDependenciesMeta: + '@lynx-js/react': + optional: true + '@prisma/client': + optional: true + '@sveltejs/kit': + optional: true + '@tanstack/react-start': + optional: true + '@tanstack/solid-start': + optional: true + better-sqlite3: + optional: true + drizzle-kit: + optional: true + drizzle-orm: + optional: true + mongodb: + optional: true + mysql2: + optional: true + next: + optional: true + pg: + optional: true + prisma: + optional: true + react: + optional: true + react-dom: + optional: true + solid-js: + optional: true + svelte: + optional: true + vitest: + optional: true + vue: + optional: true + + better-call@1.1.8: + resolution: {integrity: sha512-XMQ2rs6FNXasGNfMjzbyroSwKwYbZ/T3IxruSS6U2MJRsSYh3wYtG3o6H00ZlKZ/C/UPOAD97tqgQJNsxyeTXw==} + peerDependencies: + zod: ^4.0.0 + peerDependenciesMeta: + zod: + optional: true + + better-sqlite3@12.6.2: + resolution: {integrity: sha512-8VYKM3MjCa9WcaSAI3hzwhmyHVlH8tiGFwf0RlTsZPWJ1I5MkzjiudCo4KC4DxOaL/53A5B1sI/IbldNFDbsKA==} + engines: {node: 20.x || 22.x || 23.x || 24.x || 25.x} + binary-extensions@2.3.0: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} + bindings@1.5.0: + resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} + bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} @@ -879,9 +1213,26 @@ packages: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} + browserslist@4.28.1: + resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + buffer@5.7.1: resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + bundle-name@4.1.0: + resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} + engines: {node: '>=18'} + + c12@3.3.3: + resolution: {integrity: sha512-750hTRvgBy5kcMNPdh95Qo+XUBeGo8C7nsKSmedDmaQI+E0r82DwHeM6vBewDe4rGFbnxoa4V9pw+sPh5+Iz8Q==} + peerDependencies: + magicast: '*' + peerDependenciesMeta: + magicast: + optional: true + call-bind-apply-helpers@1.0.2: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} @@ -916,6 +1267,10 @@ packages: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} + chalk@5.6.2: + resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + character-entities-html4@2.1.0: resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} @@ -928,13 +1283,26 @@ packages: character-reference-invalid@2.0.1: resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==} + chevrotain@10.5.0: + resolution: {integrity: sha512-Pkv5rBY3+CsHOYfV5g/Vs5JY9WTHHDEKOlohI2XeygaZhUeqhAlldZ8Hz9cRmxu709bvS08YzxHdTPHhffc13A==} + chokidar@3.6.0: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} + chokidar@5.0.0: + resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} + engines: {node: '>= 20.19.0'} + chownr@1.1.4: resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} + citty@0.1.6: + resolution: {integrity: sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==} + + citty@0.2.0: + resolution: {integrity: sha512-8csy5IBFI2ex2hTVpaHN2j+LNE199AgiI7y4dMintrr8i0lQiFn+0AWMZrWdHKIgMOer65f8IThysYhoReqjWA==} + client-only@0.0.1: resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} @@ -956,6 +1324,10 @@ packages: comma-separated-tokens@2.0.3: resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + commander@12.1.0: + resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} + engines: {node: '>=18'} + commander@4.1.1: resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} engines: {node: '>= 6'} @@ -967,6 +1339,16 @@ packages: concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + confbox@0.2.2: + resolution: {integrity: sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==} + + consola@3.4.2: + resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} + engines: {node: ^14.18.0 || >=16.10.0} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + core-js@3.48.0: resolution: {integrity: sha512-zpEHTy1fjTMZCKLHUZoVeylt9XrzaIN2rbPXEt0k+q7JE5CkCZdo6bNq55bn24a69CH7ErAVLKijxJja4fw+UQ==} @@ -1035,18 +1417,36 @@ packages: deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + default-browser-id@5.0.1: + resolution: {integrity: sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==} + engines: {node: '>=18'} + + default-browser@5.4.0: + resolution: {integrity: sha512-XDuvSq38Hr1MdN47EDvYtx3U0MTqpCEn+F6ft8z2vYDzMrvQhVp0ui9oQdqW3MvK3vqUETglt1tVGgjLuJ5izg==} + engines: {node: '>=18'} + define-data-property@1.1.4: resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} engines: {node: '>= 0.4'} + define-lazy-prop@3.0.0: + resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==} + engines: {node: '>=12'} + define-properties@1.2.1: resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} engines: {node: '>= 0.4'} + defu@6.1.4: + resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} + dequal@2.0.3: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} + destr@2.0.5: + resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==} + detect-libc@2.1.2: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} @@ -1077,6 +1477,99 @@ packages: resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} engines: {node: '>=0.10.0'} + dotenv@17.2.3: + resolution: {integrity: sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==} + engines: {node: '>=12'} + + drizzle-orm@0.41.0: + resolution: {integrity: sha512-7A4ZxhHk9gdlXmTdPj/lREtP+3u8KvZ4yEN6MYVxBzZGex5Wtdc+CWSbu7btgF6TB0N+MNPrvW7RKBbxJchs/Q==} + peerDependencies: + '@aws-sdk/client-rds-data': '>=3' + '@cloudflare/workers-types': '>=4' + '@electric-sql/pglite': '>=0.2.0' + '@libsql/client': '>=0.10.0' + '@libsql/client-wasm': '>=0.10.0' + '@neondatabase/serverless': '>=0.10.0' + '@op-engineering/op-sqlite': '>=2' + '@opentelemetry/api': ^1.4.1 + '@planetscale/database': '>=1' + '@prisma/client': '*' + '@tidbcloud/serverless': '*' + '@types/better-sqlite3': '*' + '@types/pg': '*' + '@types/sql.js': '*' + '@vercel/postgres': '>=0.8.0' + '@xata.io/client': '*' + better-sqlite3: '>=7' + bun-types: '*' + expo-sqlite: '>=14.0.0' + gel: '>=2' + knex: '*' + kysely: '*' + mysql2: '>=2' + pg: '>=8' + postgres: '>=3' + prisma: '*' + sql.js: '>=1' + sqlite3: '>=5' + peerDependenciesMeta: + '@aws-sdk/client-rds-data': + optional: true + '@cloudflare/workers-types': + optional: true + '@electric-sql/pglite': + optional: true + '@libsql/client': + optional: true + '@libsql/client-wasm': + optional: true + '@neondatabase/serverless': + optional: true + '@op-engineering/op-sqlite': + optional: true + '@opentelemetry/api': + optional: true + '@planetscale/database': + optional: true + '@prisma/client': + optional: true + '@tidbcloud/serverless': + optional: true + '@types/better-sqlite3': + optional: true + '@types/pg': + optional: true + '@types/sql.js': + optional: true + '@vercel/postgres': + optional: true + '@xata.io/client': + optional: true + better-sqlite3: + optional: true + bun-types: + optional: true + expo-sqlite: + optional: true + gel: + optional: true + knex: + optional: true + kysely: + optional: true + mysql2: + optional: true + pg: + optional: true + postgres: + optional: true + prisma: + optional: true + sql.js: + optional: true + sqlite3: + optional: true + dunder-proto@1.0.1: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} @@ -1085,6 +1578,9 @@ packages: resolution: {integrity: sha512-/RInbCy1d4P6Zdfa+TMVsf/ufZVotat5hCw3QXmWtjU+3pFEOvOQ7ibo3aIxyCJw2leIeAMjmPj+1SLJiCpdrQ==} engines: {node: '>=12.0.0'} + electron-to-chromium@1.5.278: + resolution: {integrity: sha512-dQ0tM1svDRQOwxnXxm+twlGTjr9Upvt8UFWAgmLsxEzFQxhbti4VwxmMjsDxVC51Zo84swW7FVCXEV+VAkhuPw==} + emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} @@ -1140,6 +1636,10 @@ packages: resolution: {integrity: sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==} engines: {node: '>=0.12'} + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + escape-string-regexp@4.0.0: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} @@ -1278,6 +1778,9 @@ packages: resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} engines: {node: '>=6'} + exsolve@1.0.8: + resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==} + ext@1.7.0: resolution: {integrity: sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==} @@ -1321,6 +1824,9 @@ packages: resolution: {integrity: sha512-QgXo+mXTe8ljeqUFaX3QVHc5osSItJ/Km+xpocx0aSqWGMSCf6qYs/VnzZgS864Pjn5iceMRFigeAV7AfTlaig==} engines: {node: '>= 12'} + file-uri-to-path@1.0.0: + resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} + fill-range@7.1.1: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} @@ -1367,6 +1873,10 @@ packages: resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==} engines: {node: '>= 0.4'} + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + get-intrinsic@1.3.0: resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} engines: {node: '>= 0.4'} @@ -1382,6 +1892,10 @@ packages: get-tsconfig@4.13.0: resolution: {integrity: sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==} + giget@2.0.0: + resolution: {integrity: sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==} + hasBin: true + github-from-package@0.0.0: resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} @@ -1539,6 +2053,11 @@ packages: is-decimal@2.0.1: resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==} + is-docker@3.0.0: + resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + hasBin: true + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -1558,6 +2077,11 @@ packages: is-hexadecimal@2.0.1: resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==} + is-inside-container@1.0.0: + resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} + engines: {node: '>=14.16'} + hasBin: true + is-map@2.0.3: resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} engines: {node: '>= 0.4'} @@ -1614,6 +2138,10 @@ packages: resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==} engines: {node: '>= 0.4'} + is-wsl@3.1.0: + resolution: {integrity: sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==} + engines: {node: '>=16'} + isarray@1.0.0: resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} @@ -1631,6 +2159,13 @@ packages: resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==} hasBin: true + jiti@2.6.1: + resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} + hasBin: true + + jose@6.1.3: + resolution: {integrity: sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -1638,6 +2173,11 @@ packages: resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} hasBin: true + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + json-buffer@3.0.1: resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} @@ -1651,6 +2191,11 @@ packages: resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} hasBin: true + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + jsx-ast-utils@3.3.5: resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} engines: {node: '>=4.0'} @@ -1661,6 +2206,14 @@ packages: keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + kleur@3.0.3: + resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} + engines: {node: '>=6'} + + kysely@0.28.10: + resolution: {integrity: sha512-ksNxfzIW77OcZ+QWSAPC7yDqUSaIVwkTWnTPNiIy//vifNbwsSgQ57OkkncHxxpcBHM3LRfLAZVEh7kjq5twVA==} + engines: {node: '>=20.0.0'} + language-subtag-registry@0.3.23: resolution: {integrity: sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==} @@ -1678,6 +2231,10 @@ packages: lie@3.3.0: resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==} + lilconfig@2.1.0: + resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} + engines: {node: '>=10'} + lilconfig@3.1.3: resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} engines: {node: '>=14'} @@ -1709,6 +2266,9 @@ packages: resolution: {integrity: sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==} engines: {node: 20 || >=22} + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + make-cancellable-promise@1.3.2: resolution: {integrity: sha512-GCXh3bq/WuMbS+Ky4JBPW1hYTOU+znU+Q5m9Pu+pI8EoUqIHk9+tviOKC6/qhHh8C4/As3tzJ69IF32kdz85ww==} @@ -1898,6 +2458,10 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + nanostores@1.1.0: + resolution: {integrity: sha512-yJBmDJr18xy47dbNVlHcgdPrulSn1nhSE6Ns9vTG+Nx9VPT6iV1MD6aQFp/t52zpf82FhLLTXAXr30NuCnxvwA==} + engines: {node: ^20.0.0 || >=22.0.0} + napi-build-utils@2.0.0: resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==} @@ -1940,10 +2504,21 @@ packages: node-addon-api@7.1.1: resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} + node-fetch-native@1.6.7: + resolution: {integrity: sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==} + + node-releases@2.0.27: + resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} + normalize-path@3.0.0: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} + nypm@0.6.4: + resolution: {integrity: sha512-1TvCKjZyyklN+JJj2TS3P4uSQEInrM/HkkuSXsEzm1ApPgBffOn8gFguNnZf07r/1X6vlryfIqMUkJKQMzlZiw==} + engines: {node: '>=18'} + hasBin: true + object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -1980,9 +2555,16 @@ packages: resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} engines: {node: '>= 0.4'} + ohash@2.0.11: + resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==} + once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + open@10.2.0: + resolution: {integrity: sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==} + engines: {node: '>=18'} + openai@6.16.0: resolution: {integrity: sha512-fZ1uBqjFUjXzbGc35fFtYKEOxd20kd9fDpFeqWtsOZWiubY8CZ1NAlXHW3iathaFvqmNtCWMIsosCuyeI7Joxg==} hasBin: true @@ -2039,10 +2621,50 @@ packages: resolution: {integrity: sha512-+vnG6S4dYcYxZd+CZxzXCNKdELYZSKfohrk98yajCo1PtRoDgCTrrwOvK1GT0UoAdVszagDVllQc0U1vaX4NUQ==} engines: {node: '>=6'} + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + pdfjs-dist@4.8.69: resolution: {integrity: sha512-IHZsA4T7YElCKNNXtiLgqScw4zPd3pG9do8UrznC757gMd7UPeHSL2qwNNMJo4r79fl8oj1Xx+1nh2YkzdMpLQ==} engines: {node: '>=18'} + perfect-debounce@2.1.0: + resolution: {integrity: sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==} + + pg-cloudflare@1.3.0: + resolution: {integrity: sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==} + + pg-connection-string@2.10.1: + resolution: {integrity: sha512-iNzslsoeSH2/gmDDKiyMqF64DATUCWj3YJ0wP14kqcsf2TUklwimd+66yYojKwZCA7h2yRNLGug71hCBA2a4sw==} + + pg-int8@1.0.1: + resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} + engines: {node: '>=4.0.0'} + + pg-pool@3.11.0: + resolution: {integrity: sha512-MJYfvHwtGp870aeusDh+hg9apvOe2zmpZJpyt+BMtzUWlVqbhFmMK6bOBXLBUPd7iRtIF9fZplDc7KrPN3PN7w==} + peerDependencies: + pg: '>=8.0' + + pg-protocol@1.11.0: + resolution: {integrity: sha512-pfsxk2M9M3BuGgDOfuy37VNRRX3jmKgMjcvAcWqNDpZSf4cUmv8HSOl5ViRQFsfARFn0KuUQTgLxVMbNq5NW3g==} + + pg-types@2.2.0: + resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} + engines: {node: '>=4'} + + pg@8.17.2: + resolution: {integrity: sha512-vjbKdiBJRqzcYw1fNU5KuHyYvdJ1qpcQg1CeBrHFqV1pWgHeVR6j/+kX0E1AAXfyuLUGY1ICrN2ELKA/z2HWzw==} + engines: {node: '>= 16.0.0'} + peerDependencies: + pg-native: '>=3.0.1' + peerDependenciesMeta: + pg-native: + optional: true + + pgpass@1.0.5: + resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -2062,6 +2684,9 @@ packages: resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} engines: {node: '>= 6'} + pkg-types@2.3.0: + resolution: {integrity: sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==} + playwright-core@1.58.0: resolution: {integrity: sha512-aaoB1RWrdNi3//rOeKuMiS65UCcgOVljU46At6eFcOFPFHWtd2weHRRow6z/n+Lec0Lvu0k9ZPKJSjPugikirw==} engines: {node: '>=18'} @@ -2131,6 +2756,22 @@ packages: resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} + postgres-array@2.0.0: + resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} + engines: {node: '>=4'} + + postgres-bytea@1.0.1: + resolution: {integrity: sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==} + engines: {node: '>=0.10.0'} + + postgres-date@1.0.7: + resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==} + engines: {node: '>=0.10.0'} + + postgres-interval@1.2.0: + resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} + engines: {node: '>=0.10.0'} + prebuild-install@7.1.3: resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==} engines: {node: '>=10'} @@ -2140,9 +2781,18 @@ packages: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} + prettier@3.8.1: + resolution: {integrity: sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==} + engines: {node: '>=14'} + hasBin: true + process-nextick-args@2.0.1: resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + prompts@2.4.2: + resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} + engines: {node: '>= 6'} + prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} @@ -2159,6 +2809,9 @@ packages: queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + rc9@2.1.2: + resolution: {integrity: sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==} + rc@1.2.8: resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} hasBin: true @@ -2244,6 +2897,10 @@ packages: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} + readdirp@5.0.0: + resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==} + engines: {node: '>= 20.19.0'} + redux@4.2.1: resolution: {integrity: sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==} @@ -2251,6 +2908,9 @@ packages: resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} engines: {node: '>= 0.4'} + regexp-to-ast@0.5.0: + resolution: {integrity: sha512-tlbJqcMHnPKI9zSrystikWKwHkBqu2a/Sgw01h3zFjvYrMxEDYHzzoMZnUrbIfpTFEsoRnnviOXNCzFiSc54Qw==} + regexp.prototype.flags@1.5.4: resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} engines: {node: '>= 0.4'} @@ -2287,6 +2947,13 @@ packages: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + rou3@0.7.12: + resolution: {integrity: sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg==} + + run-applescript@7.1.0: + resolution: {integrity: sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==} + engines: {node: '>=18'} + run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} @@ -2320,6 +2987,9 @@ packages: engines: {node: '>=10'} hasBin: true + set-cookie-parser@2.7.2: + resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} + set-function-length@1.2.2: resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} engines: {node: '>= 0.4'} @@ -2369,6 +3039,9 @@ packages: simple-get@4.0.1: resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} + sisteransi@1.0.5: + resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -2376,6 +3049,10 @@ packages: space-separated-tokens@2.0.2: resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + stable-hash@0.0.5: resolution: {integrity: sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==} @@ -2487,6 +3164,10 @@ packages: tiny-invariant@1.3.3: resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + tinyexec@1.0.2: + resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==} + engines: {node: '>=18'} + tinyglobby@0.2.15: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} @@ -2575,6 +3256,12 @@ packages: unrs-resolver@1.11.1: resolution: {integrity: sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==} + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} @@ -2627,10 +3314,29 @@ packages: wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + wsl-utils@0.1.0: + resolution: {integrity: sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==} + engines: {node: '>=18'} + + xtend@4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} + yocto-spinner@0.2.3: + resolution: {integrity: sha512-sqBChb33loEnkoXte1bLg45bEBsOP9N1kzQh5JZNKj/0rik4zAPTNSAVPj3uQAdc6slYJ0Ksc403G2XgxsJQFQ==} + engines: {node: '>=18.19'} + + yoctocolors@2.1.2: + resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==} + engines: {node: '>=18'} + zod@4.3.6: resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} @@ -2641,8 +3347,354 @@ snapshots: '@alloc/quick-lru@5.2.0': {} + '@babel/code-frame@7.28.6': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.28.6': {} + + '@babel/core@7.28.6': + dependencies: + '@babel/code-frame': 7.28.6 + '@babel/generator': 7.28.6 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.28.6) + '@babel/helpers': 7.28.6 + '@babel/parser': 7.28.6 + '@babel/template': 7.28.6 + '@babel/traverse': 7.28.6 + '@babel/types': 7.28.6 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.28.6': + dependencies: + '@babel/parser': 7.28.6 + '@babel/types': 7.28.6 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-annotate-as-pure@7.27.3': + dependencies: + '@babel/types': 7.28.6 + + '@babel/helper-compilation-targets@7.28.6': + dependencies: + '@babel/compat-data': 7.28.6 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.28.1 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-create-class-features-plugin@7.28.6(@babel/core@7.28.6)': + dependencies: + '@babel/core': 7.28.6 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-member-expression-to-functions': 7.28.5 + '@babel/helper-optimise-call-expression': 7.27.1 + '@babel/helper-replace-supers': 7.28.6(@babel/core@7.28.6) + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/traverse': 7.28.6 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/helper-globals@7.28.0': {} + + '@babel/helper-member-expression-to-functions@7.28.5': + dependencies: + '@babel/traverse': 7.28.6 + '@babel/types': 7.28.6 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-imports@7.28.6': + dependencies: + '@babel/traverse': 7.28.6 + '@babel/types': 7.28.6 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.28.6(@babel/core@7.28.6)': + dependencies: + '@babel/core': 7.28.6 + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.28.6 + transitivePeerDependencies: + - supports-color + + '@babel/helper-optimise-call-expression@7.27.1': + dependencies: + '@babel/types': 7.28.6 + + '@babel/helper-plugin-utils@7.28.6': {} + + '@babel/helper-replace-supers@7.28.6(@babel/core@7.28.6)': + dependencies: + '@babel/core': 7.28.6 + '@babel/helper-member-expression-to-functions': 7.28.5 + '@babel/helper-optimise-call-expression': 7.27.1 + '@babel/traverse': 7.28.6 + transitivePeerDependencies: + - supports-color + + '@babel/helper-skip-transparent-expression-wrappers@7.27.1': + dependencies: + '@babel/traverse': 7.28.6 + '@babel/types': 7.28.6 + transitivePeerDependencies: + - supports-color + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/helper-validator-option@7.27.1': {} + + '@babel/helpers@7.28.6': + dependencies: + '@babel/template': 7.28.6 + '@babel/types': 7.28.6 + + '@babel/parser@7.28.6': + dependencies: + '@babel/types': 7.28.6 + + '@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.28.6)': + dependencies: + '@babel/core': 7.28.6 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-typescript@7.28.6(@babel/core@7.28.6)': + dependencies: + '@babel/core': 7.28.6 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-modules-commonjs@7.28.6(@babel/core@7.28.6)': + dependencies: + '@babel/core': 7.28.6 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.28.6) + '@babel/helper-plugin-utils': 7.28.6 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-react-display-name@7.28.0(@babel/core@7.28.6)': + dependencies: + '@babel/core': 7.28.6 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-react-jsx-development@7.27.1(@babel/core@7.28.6)': + dependencies: + '@babel/core': 7.28.6 + '@babel/plugin-transform-react-jsx': 7.28.6(@babel/core@7.28.6) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-react-jsx@7.28.6(@babel/core@7.28.6)': + dependencies: + '@babel/core': 7.28.6 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.28.6) + '@babel/types': 7.28.6 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-react-pure-annotations@7.27.1(@babel/core@7.28.6)': + dependencies: + '@babel/core': 7.28.6 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-typescript@7.28.6(@babel/core@7.28.6)': + dependencies: + '@babel/core': 7.28.6 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-create-class-features-plugin': 7.28.6(@babel/core@7.28.6) + '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/plugin-syntax-typescript': 7.28.6(@babel/core@7.28.6) + transitivePeerDependencies: + - supports-color + + '@babel/preset-react@7.28.5(@babel/core@7.28.6)': + dependencies: + '@babel/core': 7.28.6 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-validator-option': 7.27.1 + '@babel/plugin-transform-react-display-name': 7.28.0(@babel/core@7.28.6) + '@babel/plugin-transform-react-jsx': 7.28.6(@babel/core@7.28.6) + '@babel/plugin-transform-react-jsx-development': 7.27.1(@babel/core@7.28.6) + '@babel/plugin-transform-react-pure-annotations': 7.27.1(@babel/core@7.28.6) + transitivePeerDependencies: + - supports-color + + '@babel/preset-typescript@7.28.5(@babel/core@7.28.6)': + dependencies: + '@babel/core': 7.28.6 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-validator-option': 7.27.1 + '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.28.6) + '@babel/plugin-transform-modules-commonjs': 7.28.6(@babel/core@7.28.6) + '@babel/plugin-transform-typescript': 7.28.6(@babel/core@7.28.6) + transitivePeerDependencies: + - supports-color + '@babel/runtime@7.28.6': {} + '@babel/template@7.28.6': + dependencies: + '@babel/code-frame': 7.28.6 + '@babel/parser': 7.28.6 + '@babel/types': 7.28.6 + + '@babel/traverse@7.28.6': + dependencies: + '@babel/code-frame': 7.28.6 + '@babel/generator': 7.28.6 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.28.6 + '@babel/template': 7.28.6 + '@babel/types': 7.28.6 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.28.6': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + + '@better-auth/cli@1.4.17(@better-fetch/fetch@1.1.21)(@types/better-sqlite3@7.6.13)(better-call@1.1.8(zod@4.3.6))(jose@6.1.3)(kysely@0.28.10)(nanostores@1.1.0)(next@15.5.9(@babel/core@7.28.6)(@playwright/test@1.58.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@babel/core': 7.28.6 + '@babel/preset-react': 7.28.5(@babel/core@7.28.6) + '@babel/preset-typescript': 7.28.5(@babel/core@7.28.6) + '@better-auth/core': 1.4.17(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@4.3.6))(jose@6.1.3)(kysely@0.28.10)(nanostores@1.1.0) + '@better-auth/telemetry': 1.4.17(@better-auth/core@1.4.17(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@4.3.6))(jose@6.1.3)(kysely@0.28.10)(nanostores@1.1.0)) + '@better-auth/utils': 0.3.0 + '@clack/prompts': 0.11.0 + '@mrleebo/prisma-ast': 0.13.1 + '@prisma/client': 5.22.0 + '@types/pg': 8.16.0 + better-auth: 1.4.17(@prisma/client@5.22.0)(better-sqlite3@12.6.2)(drizzle-orm@0.41.0(@prisma/client@5.22.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.16.0)(better-sqlite3@12.6.2)(kysely@0.28.10)(pg@8.17.2))(next@15.5.9(@babel/core@7.28.6)(@playwright/test@1.58.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(pg@8.17.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + better-sqlite3: 12.6.2 + c12: 3.3.3 + chalk: 5.6.2 + commander: 12.1.0 + dotenv: 17.2.3 + drizzle-orm: 0.41.0(@prisma/client@5.22.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.16.0)(better-sqlite3@12.6.2)(kysely@0.28.10)(pg@8.17.2) + open: 10.2.0 + pg: 8.17.2 + prettier: 3.8.1 + prompts: 2.4.2 + semver: 7.7.3 + yocto-spinner: 0.2.3 + zod: 4.3.6 + transitivePeerDependencies: + - '@aws-sdk/client-rds-data' + - '@better-fetch/fetch' + - '@cloudflare/workers-types' + - '@electric-sql/pglite' + - '@libsql/client' + - '@libsql/client-wasm' + - '@lynx-js/react' + - '@neondatabase/serverless' + - '@op-engineering/op-sqlite' + - '@opentelemetry/api' + - '@planetscale/database' + - '@sveltejs/kit' + - '@tanstack/react-start' + - '@tanstack/solid-start' + - '@tidbcloud/serverless' + - '@types/better-sqlite3' + - '@types/sql.js' + - '@vercel/postgres' + - '@xata.io/client' + - better-call + - bun-types + - drizzle-kit + - expo-sqlite + - gel + - jose + - knex + - kysely + - magicast + - mongodb + - mysql2 + - nanostores + - next + - pg-native + - postgres + - prisma + - react + - react-dom + - solid-js + - sql.js + - sqlite3 + - supports-color + - svelte + - vitest + - vue + + '@better-auth/core@1.4.17(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@4.3.6))(jose@6.1.3)(kysely@0.28.10)(nanostores@1.1.0)': + dependencies: + '@better-auth/utils': 0.3.0 + '@better-fetch/fetch': 1.1.21 + '@standard-schema/spec': 1.1.0 + better-call: 1.1.8(zod@4.3.6) + jose: 6.1.3 + kysely: 0.28.10 + nanostores: 1.1.0 + zod: 4.3.6 + + '@better-auth/telemetry@1.4.17(@better-auth/core@1.4.17(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@4.3.6))(jose@6.1.3)(kysely@0.28.10)(nanostores@1.1.0))': + dependencies: + '@better-auth/core': 1.4.17(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@4.3.6))(jose@6.1.3)(kysely@0.28.10)(nanostores@1.1.0) + '@better-auth/utils': 0.3.0 + '@better-fetch/fetch': 1.1.21 + + '@better-auth/utils@0.3.0': {} + + '@better-fetch/fetch@1.1.21': {} + + '@chevrotain/cst-dts-gen@10.5.0': + dependencies: + '@chevrotain/gast': 10.5.0 + '@chevrotain/types': 10.5.0 + lodash: 4.17.23 + + '@chevrotain/gast@10.5.0': + dependencies: + '@chevrotain/types': 10.5.0 + lodash: 4.17.23 + + '@chevrotain/types@10.5.0': {} + + '@chevrotain/utils@10.5.0': {} + + '@clack/core@0.5.0': + dependencies: + picocolors: 1.1.1 + sisteransi: 1.0.5 + + '@clack/prompts@0.11.0': + dependencies: + '@clack/core': 0.5.0 + picocolors: 1.1.1 + sisteransi: 1.0.5 + '@emnapi/core@1.8.1': dependencies: '@emnapi/wasi-threads': 1.1.0 @@ -2853,6 +3905,11 @@ snapshots: '@jridgewell/sourcemap-codec': 1.5.5 '@jridgewell/trace-mapping': 0.3.31 + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + '@jridgewell/resolve-uri@3.1.2': {} '@jridgewell/sourcemap-codec@1.5.5': {} @@ -2862,6 +3919,11 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@mrleebo/prisma-ast@0.13.1': + dependencies: + chevrotain: 10.5.0 + lilconfig: 2.1.0 + '@napi-rs/wasm-runtime@0.2.12': dependencies: '@emnapi/core': 1.8.1 @@ -2899,6 +3961,10 @@ snapshots: '@next/swc-win32-x64-msvc@15.5.7': optional: true + '@noble/ciphers@2.1.1': {} + + '@noble/hashes@2.0.1': {} + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -2917,6 +3983,8 @@ snapshots: dependencies: playwright: 1.58.0 + '@prisma/client@5.22.0': {} + '@react-aria/focus@3.21.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: '@react-aria/interactions': 3.26.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -2976,6 +4044,8 @@ snapshots: '@rushstack/eslint-patch@1.15.0': {} + '@standard-schema/spec@1.1.0': {} + '@swc/helpers@0.5.15': dependencies: tslib: 2.8.1 @@ -3002,6 +4072,10 @@ snapshots: tslib: 2.8.1 optional: true + '@types/better-sqlite3@7.6.13': + dependencies: + '@types/node': 20.19.30 + '@types/debug@4.1.12': dependencies: '@types/ms': 2.1.0 @@ -3032,6 +4106,12 @@ snapshots: dependencies: undici-types: 6.21.0 + '@types/pg@8.16.0': + dependencies: + '@types/node': 20.19.30 + pg-protocol: 1.11.0 + pg-types: 2.2.0 + '@types/react-dom@19.2.3(@types/react@19.2.9)': dependencies: '@types/react': 19.2.9 @@ -3198,9 +4278,9 @@ snapshots: '@unrs/resolver-binding-win32-x64-msvc@1.11.1': optional: true - '@vercel/analytics@1.6.1(next@15.5.9(@playwright/test@1.58.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)': + '@vercel/analytics@1.6.1(next@15.5.9(@babel/core@7.28.6)(@playwright/test@1.58.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)': optionalDependencies: - next: 15.5.9(@playwright/test@1.58.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + next: 15.5.9(@babel/core@7.28.6)(@playwright/test@1.58.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) react: 19.2.3 '@xmldom/xmldom@0.9.8': {} @@ -3320,17 +4400,58 @@ snapshots: balanced-match@1.0.2: {} - base64-js@1.5.1: - optional: true + base64-js@1.5.1: {} + + baseline-browser-mapping@2.9.18: {} + + better-auth@1.4.17(@prisma/client@5.22.0)(better-sqlite3@12.6.2)(drizzle-orm@0.41.0(@prisma/client@5.22.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.16.0)(better-sqlite3@12.6.2)(kysely@0.28.10)(pg@8.17.2))(next@15.5.9(@babel/core@7.28.6)(@playwright/test@1.58.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(pg@8.17.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + dependencies: + '@better-auth/core': 1.4.17(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@4.3.6))(jose@6.1.3)(kysely@0.28.10)(nanostores@1.1.0) + '@better-auth/telemetry': 1.4.17(@better-auth/core@1.4.17(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@4.3.6))(jose@6.1.3)(kysely@0.28.10)(nanostores@1.1.0)) + '@better-auth/utils': 0.3.0 + '@better-fetch/fetch': 1.1.21 + '@noble/ciphers': 2.1.1 + '@noble/hashes': 2.0.1 + better-call: 1.1.8(zod@4.3.6) + defu: 6.1.4 + jose: 6.1.3 + kysely: 0.28.10 + nanostores: 1.1.0 + zod: 4.3.6 + optionalDependencies: + '@prisma/client': 5.22.0 + better-sqlite3: 12.6.2 + drizzle-orm: 0.41.0(@prisma/client@5.22.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.16.0)(better-sqlite3@12.6.2)(kysely@0.28.10)(pg@8.17.2) + next: 15.5.9(@babel/core@7.28.6)(@playwright/test@1.58.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + pg: 8.17.2 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + + better-call@1.1.8(zod@4.3.6): + dependencies: + '@better-auth/utils': 0.3.0 + '@better-fetch/fetch': 1.1.21 + rou3: 0.7.12 + set-cookie-parser: 2.7.2 + optionalDependencies: + zod: 4.3.6 + + better-sqlite3@12.6.2: + dependencies: + bindings: 1.5.0 + prebuild-install: 7.1.3 binary-extensions@2.3.0: {} + bindings@1.5.0: + dependencies: + file-uri-to-path: 1.0.0 + bl@4.1.0: dependencies: buffer: 5.7.1 inherits: 2.0.4 readable-stream: 3.6.2 - optional: true brace-expansion@1.1.12: dependencies: @@ -3345,11 +4466,37 @@ snapshots: dependencies: fill-range: 7.1.1 + browserslist@4.28.1: + dependencies: + baseline-browser-mapping: 2.9.18 + caniuse-lite: 1.0.30001766 + electron-to-chromium: 1.5.278 + node-releases: 2.0.27 + update-browserslist-db: 1.2.3(browserslist@4.28.1) + buffer@5.7.1: dependencies: base64-js: 1.5.1 ieee754: 1.2.1 - optional: true + + bundle-name@4.1.0: + dependencies: + run-applescript: 7.1.0 + + c12@3.3.3: + dependencies: + chokidar: 5.0.0 + confbox: 0.2.2 + defu: 6.1.4 + dotenv: 17.2.3 + exsolve: 1.0.8 + giget: 2.0.0 + jiti: 2.6.1 + ohash: 2.0.11 + pathe: 2.0.3 + perfect-debounce: 2.1.0 + pkg-types: 2.3.0 + rc9: 2.1.2 call-bind-apply-helpers@1.0.2: dependencies: @@ -3387,6 +4534,8 @@ snapshots: ansi-styles: 4.3.0 supports-color: 7.2.0 + chalk@5.6.2: {} + character-entities-html4@2.1.0: {} character-entities-legacy@3.0.0: {} @@ -3395,6 +4544,15 @@ snapshots: character-reference-invalid@2.0.1: {} + chevrotain@10.5.0: + dependencies: + '@chevrotain/cst-dts-gen': 10.5.0 + '@chevrotain/gast': 10.5.0 + '@chevrotain/types': 10.5.0 + '@chevrotain/utils': 10.5.0 + lodash: 4.17.23 + regexp-to-ast: 0.5.0 + chokidar@3.6.0: dependencies: anymatch: 3.1.3 @@ -3407,8 +4565,17 @@ snapshots: optionalDependencies: fsevents: 2.3.3 - chownr@1.1.4: - optional: true + chokidar@5.0.0: + dependencies: + readdirp: 5.0.0 + + chownr@1.1.4: {} + + citty@0.1.6: + dependencies: + consola: 3.4.2 + + citty@0.2.0: {} client-only@0.0.1: {} @@ -3424,6 +4591,8 @@ snapshots: comma-separated-tokens@2.0.3: {} + commander@12.1.0: {} + commander@4.1.1: {} compromise@14.14.5: @@ -3434,6 +4603,12 @@ snapshots: concat-map@0.0.1: {} + confbox@0.2.2: {} + + consola@3.4.2: {} + + convert-source-map@2.0.0: {} + core-js@3.48.0: {} core-util-is@1.0.3: {} @@ -3488,29 +4663,39 @@ snapshots: decompress-response@6.0.0: dependencies: mimic-response: 3.1.0 - optional: true - deep-extend@0.6.0: - optional: true + deep-extend@0.6.0: {} deep-is@0.1.4: {} + default-browser-id@5.0.1: {} + + default-browser@5.4.0: + dependencies: + bundle-name: 4.1.0 + default-browser-id: 5.0.1 + define-data-property@1.1.4: dependencies: es-define-property: 1.0.1 es-errors: 1.3.0 gopd: 1.2.0 + define-lazy-prop@3.0.0: {} + define-properties@1.2.1: dependencies: define-data-property: 1.1.4 has-property-descriptors: 1.0.2 object-keys: 1.1.1 + defu@6.1.4: {} + dequal@2.0.3: {} - detect-libc@2.1.2: - optional: true + destr@2.0.5: {} + + detect-libc@2.1.2: {} devlop@1.1.0: dependencies: @@ -3538,6 +4723,17 @@ snapshots: dependencies: esutils: 2.0.3 + dotenv@17.2.3: {} + + drizzle-orm@0.41.0(@prisma/client@5.22.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.16.0)(better-sqlite3@12.6.2)(kysely@0.28.10)(pg@8.17.2): + optionalDependencies: + '@prisma/client': 5.22.0 + '@types/better-sqlite3': 7.6.13 + '@types/pg': 8.16.0 + better-sqlite3: 12.6.2 + kysely: 0.28.10 + pg: 8.17.2 + dunder-proto@1.0.1: dependencies: call-bind-apply-helpers: 1.0.2 @@ -3546,6 +4742,8 @@ snapshots: efrt@2.7.0: {} + electron-to-chromium@1.5.278: {} + emoji-regex@9.2.2: {} empty-module@0.0.2: {} @@ -3553,7 +4751,6 @@ snapshots: end-of-stream@1.4.5: dependencies: once: 1.4.0 - optional: true epubjs@0.3.93: dependencies: @@ -3686,6 +4883,8 @@ snapshots: d: 1.0.2 ext: 1.7.0 + escalade@3.2.0: {} + escape-string-regexp@4.0.0: {} escape-string-regexp@5.0.0: {} @@ -3900,8 +5099,9 @@ snapshots: d: 1.0.2 es5-ext: 0.10.64 - expand-template@2.0.3: - optional: true + expand-template@2.0.3: {} + + exsolve@1.0.8: {} ext@1.7.0: dependencies: @@ -3947,6 +5147,8 @@ snapshots: dependencies: tslib: 2.8.1 + file-uri-to-path@1.0.0: {} + fill-range@7.1.1: dependencies: to-regex-range: 5.0.1 @@ -3967,8 +5169,7 @@ snapshots: dependencies: is-callable: 1.2.7 - fs-constants@1.0.0: - optional: true + fs-constants@1.0.0: {} fsevents@2.3.2: optional: true @@ -3991,6 +5192,8 @@ snapshots: generator-function@2.0.1: {} + gensync@1.0.0-beta.2: {} + get-intrinsic@1.3.0: dependencies: call-bind-apply-helpers: 1.0.2 @@ -4019,8 +5222,16 @@ snapshots: dependencies: resolve-pkg-maps: 1.0.0 - github-from-package@0.0.0: - optional: true + giget@2.0.0: + dependencies: + citty: 0.1.6 + consola: 3.4.2 + defu: 6.1.4 + node-fetch-native: 1.6.7 + nypm: 0.6.4 + pathe: 2.0.3 + + github-from-package@0.0.0: {} glob-parent@5.1.2: dependencies: @@ -4099,8 +5310,7 @@ snapshots: html-url-attributes@3.0.1: {} - ieee754@1.2.1: - optional: true + ieee754@1.2.1: {} ignore@5.3.2: {} @@ -4117,8 +5327,7 @@ snapshots: inherits@2.0.4: {} - ini@1.3.8: - optional: true + ini@1.3.8: {} inline-style-parser@0.2.7: {} @@ -4185,6 +5394,8 @@ snapshots: is-decimal@2.0.1: {} + is-docker@3.0.0: {} + is-extglob@2.1.1: {} is-finalizationregistry@1.1.1: @@ -4205,6 +5416,10 @@ snapshots: is-hexadecimal@2.0.1: {} + is-inside-container@1.0.0: + dependencies: + is-docker: 3.0.0 + is-map@2.0.3: {} is-negative-zero@2.0.3: {} @@ -4257,6 +5472,10 @@ snapshots: call-bound: 1.0.4 get-intrinsic: 1.3.0 + is-wsl@3.1.0: + dependencies: + is-inside-container: 1.0.0 + isarray@1.0.0: {} isarray@2.0.5: {} @@ -4274,12 +5493,18 @@ snapshots: jiti@1.21.7: {} + jiti@2.6.1: {} + + jose@6.1.3: {} + js-tokens@4.0.0: {} js-yaml@4.1.1: dependencies: argparse: 2.0.1 + jsesc@3.1.0: {} + json-buffer@3.0.1: {} json-schema-traverse@0.4.1: {} @@ -4290,6 +5515,8 @@ snapshots: dependencies: minimist: 1.2.8 + json5@2.2.3: {} + jsx-ast-utils@3.3.5: dependencies: array-includes: 3.1.9 @@ -4308,6 +5535,10 @@ snapshots: dependencies: json-buffer: 3.0.1 + kleur@3.0.3: {} + + kysely@0.28.10: {} + language-subtag-registry@0.3.23: {} language-tags@1.0.9: @@ -4327,6 +5558,8 @@ snapshots: dependencies: immediate: 3.0.6 + lilconfig@2.1.0: {} + lilconfig@3.1.3: {} lines-and-columns@1.2.4: {} @@ -4351,6 +5584,10 @@ snapshots: lru-cache@11.2.4: {} + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + make-cancellable-promise@1.3.2: {} make-event-props@1.6.2: {} @@ -4716,8 +5953,7 @@ snapshots: braces: 3.0.3 picomatch: 2.3.1 - mimic-response@3.1.0: - optional: true + mimic-response@3.1.0: {} minimatch@3.1.2: dependencies: @@ -4729,8 +5965,7 @@ snapshots: minimist@1.2.8: {} - mkdirp-classic@0.5.3: - optional: true + mkdirp-classic@0.5.3: {} ms@2.1.3: {} @@ -4742,8 +5977,9 @@ snapshots: nanoid@3.3.11: {} - napi-build-utils@2.0.0: - optional: true + nanostores@1.1.0: {} + + napi-build-utils@2.0.0: {} napi-postinstall@0.3.4: {} @@ -4751,7 +5987,7 @@ snapshots: next-tick@1.1.0: {} - next@15.5.9(@playwright/test@1.58.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + next@15.5.9(@babel/core@7.28.6)(@playwright/test@1.58.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: '@next/env': 15.5.9 '@swc/helpers': 0.5.15 @@ -4759,7 +5995,7 @@ snapshots: postcss: 8.4.31 react: 19.2.3 react-dom: 19.2.3(react@19.2.3) - styled-jsx: 5.1.6(react@19.2.3) + styled-jsx: 5.1.6(@babel/core@7.28.6)(react@19.2.3) optionalDependencies: '@next/swc-darwin-arm64': 15.5.7 '@next/swc-darwin-x64': 15.5.7 @@ -4778,13 +6014,22 @@ snapshots: node-abi@3.87.0: dependencies: semver: 7.7.3 - optional: true node-addon-api@7.1.1: optional: true + node-fetch-native@1.6.7: {} + + node-releases@2.0.27: {} + normalize-path@3.0.0: {} + nypm@0.6.4: + dependencies: + citty: 0.2.0 + pathe: 2.0.3 + tinyexec: 1.0.2 + object-assign@4.1.1: {} object-hash@3.0.0: {} @@ -4829,10 +6074,18 @@ snapshots: define-properties: 1.2.1 es-object-atoms: 1.1.1 + ohash@2.0.11: {} + once@1.4.0: dependencies: wrappy: 1.0.2 - optional: true + + open@10.2.0: + dependencies: + default-browser: 5.4.0 + define-lazy-prop: 3.0.0 + is-inside-container: 1.0.0 + wsl-utils: 0.1.0 openai@6.16.0(zod@4.3.6): optionalDependencies: @@ -4888,11 +6141,50 @@ snapshots: path2d@0.2.2: optional: true + pathe@2.0.3: {} + pdfjs-dist@4.8.69: optionalDependencies: canvas: 3.2.1 path2d: 0.2.2 + perfect-debounce@2.1.0: {} + + pg-cloudflare@1.3.0: + optional: true + + pg-connection-string@2.10.1: {} + + pg-int8@1.0.1: {} + + pg-pool@3.11.0(pg@8.17.2): + dependencies: + pg: 8.17.2 + + pg-protocol@1.11.0: {} + + pg-types@2.2.0: + dependencies: + pg-int8: 1.0.1 + postgres-array: 2.0.0 + postgres-bytea: 1.0.1 + postgres-date: 1.0.7 + postgres-interval: 1.2.0 + + pg@8.17.2: + dependencies: + pg-connection-string: 2.10.1 + pg-pool: 3.11.0(pg@8.17.2) + pg-protocol: 1.11.0 + pg-types: 2.2.0 + pgpass: 1.0.5 + optionalDependencies: + pg-cloudflare: 1.3.0 + + pgpass@1.0.5: + dependencies: + split2: 4.2.0 + picocolors@1.1.1: {} picomatch@2.3.1: {} @@ -4903,6 +6195,12 @@ snapshots: pirates@4.0.7: {} + pkg-types@2.3.0: + dependencies: + confbox: 0.2.2 + exsolve: 1.0.8 + pathe: 2.0.3 + playwright-core@1.58.0: {} playwright@1.58.0: @@ -4961,6 +6259,16 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + postgres-array@2.0.0: {} + + postgres-bytea@1.0.1: {} + + postgres-date@1.0.7: {} + + postgres-interval@1.2.0: + dependencies: + xtend: 4.0.2 + prebuild-install@7.1.3: dependencies: detect-libc: 2.1.2 @@ -4975,12 +6283,18 @@ snapshots: simple-get: 4.0.1 tar-fs: 2.1.4 tunnel-agent: 0.6.0 - optional: true prelude-ls@1.2.1: {} + prettier@3.8.1: {} + process-nextick-args@2.0.1: {} + prompts@2.4.2: + dependencies: + kleur: 3.0.3 + sisteransi: 1.0.5 + prop-types@15.8.1: dependencies: loose-envify: 1.4.0 @@ -4993,19 +6307,22 @@ snapshots: dependencies: end-of-stream: 1.4.5 once: 1.4.0 - optional: true punycode@2.3.1: {} queue-microtask@1.2.3: {} + rc9@2.1.2: + dependencies: + defu: 6.1.4 + destr: 2.0.5 + rc@1.2.8: dependencies: deep-extend: 0.6.0 ini: 1.3.8 minimist: 1.2.8 strip-json-comments: 2.0.1 - optional: true react-dnd-html5-backend@16.0.1: dependencies: @@ -5109,12 +6426,13 @@ snapshots: inherits: 2.0.4 string_decoder: 1.3.0 util-deprecate: 1.0.2 - optional: true readdirp@3.6.0: dependencies: picomatch: 2.3.1 + readdirp@5.0.0: {} + redux@4.2.1: dependencies: '@babel/runtime': 7.28.6 @@ -5130,6 +6448,8 @@ snapshots: get-proto: 1.0.1 which-builtin-type: 1.2.1 + regexp-to-ast@0.5.0: {} + regexp.prototype.flags@1.5.4: dependencies: call-bind: 1.0.8 @@ -5191,6 +6511,10 @@ snapshots: reusify@1.1.0: {} + rou3@0.7.12: {} + + run-applescript@7.1.0: {} + run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 @@ -5205,8 +6529,7 @@ snapshots: safe-buffer@5.1.2: {} - safe-buffer@5.2.1: - optional: true + safe-buffer@5.2.1: {} safe-push-apply@1.0.0: dependencies: @@ -5225,6 +6548,8 @@ snapshots: semver@7.7.3: {} + set-cookie-parser@2.7.2: {} + set-function-length@1.2.2: dependencies: define-data-property: 1.1.4 @@ -5315,20 +6640,22 @@ snapshots: side-channel-map: 1.0.1 side-channel-weakmap: 1.0.2 - simple-concat@1.0.1: - optional: true + simple-concat@1.0.1: {} simple-get@4.0.1: dependencies: decompress-response: 6.0.0 once: 1.4.0 simple-concat: 1.0.1 - optional: true + + sisteransi@1.0.5: {} source-map-js@1.2.1: {} space-separated-tokens@2.0.2: {} + split2@4.2.0: {} + stable-hash@0.0.5: {} stop-iteration-iterator@1.1.0: @@ -5393,7 +6720,6 @@ snapshots: string_decoder@1.3.0: dependencies: safe-buffer: 5.2.1 - optional: true stringify-entities@4.0.4: dependencies: @@ -5402,8 +6728,7 @@ snapshots: strip-bom@3.0.0: {} - strip-json-comments@2.0.1: - optional: true + strip-json-comments@2.0.1: {} strip-json-comments@3.1.1: {} @@ -5415,10 +6740,12 @@ snapshots: dependencies: inline-style-parser: 0.2.7 - styled-jsx@5.1.6(react@19.2.3): + styled-jsx@5.1.6(@babel/core@7.28.6)(react@19.2.3): dependencies: client-only: 0.0.1 react: 19.2.3 + optionalDependencies: + '@babel/core': 7.28.6 sucrase@3.35.1: dependencies: @@ -5474,7 +6801,6 @@ snapshots: mkdirp-classic: 0.5.3 pump: 3.0.3 tar-stream: 2.2.0 - optional: true tar-stream@2.2.0: dependencies: @@ -5483,7 +6809,6 @@ snapshots: fs-constants: 1.0.0 inherits: 2.0.4 readable-stream: 3.6.2 - optional: true thenify-all@1.6.0: dependencies: @@ -5495,6 +6820,8 @@ snapshots: tiny-invariant@1.3.3: {} + tinyexec@1.0.2: {} + tinyglobby@0.2.15: dependencies: fdir: 6.5.0(picomatch@4.0.3) @@ -5526,7 +6853,6 @@ snapshots: tunnel-agent@0.6.0: dependencies: safe-buffer: 5.2.1 - optional: true type-check@0.4.0: dependencies: @@ -5635,6 +6961,12 @@ snapshots: '@unrs/resolver-binding-win32-ia32-msvc': 1.11.1 '@unrs/resolver-binding-win32-x64-msvc': 1.11.1 + update-browserslist-db@1.2.3(browserslist@4.28.1): + dependencies: + browserslist: 4.28.1 + escalade: 3.2.0 + picocolors: 1.1.1 + uri-js@4.4.1: dependencies: punycode: 2.3.1 @@ -5708,12 +7040,24 @@ snapshots: word-wrap@1.2.5: {} - wrappy@1.0.2: - optional: true + wrappy@1.0.2: {} + + wsl-utils@0.1.0: + dependencies: + is-wsl: 3.1.0 + + xtend@4.0.2: {} + + yallist@3.1.1: {} yocto-queue@0.1.0: {} - zod@4.3.6: - optional: true + yocto-spinner@0.2.3: + dependencies: + yoctocolors: 2.1.2 + + yoctocolors@2.1.2: {} + + zod@4.3.6: {} zwitch@2.0.4: {} diff --git a/src/app/api/account/delete/route.ts b/src/app/api/account/delete/route.ts new file mode 100644 index 0000000..cc7d01d --- /dev/null +++ b/src/app/api/account/delete/route.ts @@ -0,0 +1,36 @@ +import { headers } from 'next/headers'; +import { NextResponse } from 'next/server'; +import { auth } from '@/lib/server/auth'; +import { db } from '@/lib/server/db'; +import { isAuthEnabled } from '@/lib/server/auth-config'; + +export async function DELETE() { + if (!isAuthEnabled() || !auth) { + return NextResponse.json({ error: 'Authentication disabled' }, { status: 403 }); + } + + const session = await auth.api.getSession({ + headers: await headers() + }); + + if (!session?.user?.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + + try { + // Directly delete user from database + await db.transaction(async (client) => { + // Deleting user usually cascades to sessions, accounts, etc if FKs are set up correctly + // But better-auth schemas do cascade. + await client.query('DELETE FROM "user" WHERE id = $1', [session.user.id]); + }); + + return NextResponse.json({ success: true }); + } catch (error) { + console.error('Failed to delete account:', error); + return NextResponse.json( + { error: 'Failed to delete account' }, + { status: 500 } + ); + } +} diff --git a/src/app/api/auth/[...all]/route.ts b/src/app/api/auth/[...all]/route.ts new file mode 100644 index 0000000..b49da69 --- /dev/null +++ b/src/app/api/auth/[...all]/route.ts @@ -0,0 +1,11 @@ +import { auth } from "@/lib/server/auth"; // path to your auth file +import { toNextJsHandler } from "better-auth/next-js"; + +const handlers = auth + ? toNextJsHandler(auth) + : { + POST: async () => new Response("Auth disabled", { status: 404 }), + GET: async () => new Response("Auth disabled", { status: 404 }) + }; + +export const { POST, GET } = handlers; \ No newline at end of file diff --git a/src/app/api/rate-limit/status/route.ts b/src/app/api/rate-limit/status/route.ts new file mode 100644 index 0000000..12e1a07 --- /dev/null +++ b/src/app/api/rate-limit/status/route.ts @@ -0,0 +1,73 @@ +import { NextResponse } from 'next/server'; +import { auth } from '@/lib/server/auth'; +import { rateLimiter, RATE_LIMITS } from '@/lib/server/rate-limiter'; +import { headers } from 'next/headers'; +import { isAuthEnabled } from '@/lib/server/auth-config'; + +export const dynamic = 'force-dynamic'; + +export async function GET() { + try { + // If auth is not enabled, return unlimited status + if (!isAuthEnabled() || !auth) { + const tomorrow = new Date(); + tomorrow.setDate(tomorrow.getDate() + 1); + tomorrow.setHours(0, 0, 0, 0); + + return NextResponse.json({ + allowed: true, + currentCount: 0, + limit: Infinity, + remainingChars: Infinity, + resetTime: tomorrow.toISOString(), + userType: 'unauthenticated', + authEnabled: false + }); + } + + // Get session from auth + const session = await auth.api.getSession({ + headers: await headers() + }); + + // No session means unauthenticated + if (!session?.user) { + const tomorrow = new Date(); + tomorrow.setDate(tomorrow.getDate() + 1); + tomorrow.setHours(0, 0, 0, 0); + + return NextResponse.json({ + allowed: true, + currentCount: 0, + limit: RATE_LIMITS.ANONYMOUS, + remainingChars: RATE_LIMITS.ANONYMOUS, + resetTime: tomorrow.toISOString(), + userType: 'unauthenticated', + authEnabled: true + }); + } + + const isAnonymous = !session.user.email || session.user.email === ''; + + const result = await rateLimiter.getCurrentUsage({ + id: session.user.id, + isAnonymous + }); + + return NextResponse.json({ + allowed: result.allowed, + currentCount: result.currentCount, + limit: result.limit, + remainingChars: result.remainingChars, + resetTime: result.resetTime.toISOString(), + userType: isAnonymous ? 'anonymous' : 'authenticated', + authEnabled: true + }); + } catch (error) { + console.error('Error getting rate limit status:', error); + return NextResponse.json( + { error: 'Failed to get rate limit status' }, + { status: 500 } + ); + } +} diff --git a/src/app/api/tts/route.ts b/src/app/api/tts/route.ts index 6195d32..33a0bdd 100644 --- a/src/app/api/tts/route.ts +++ b/src/app/api/tts/route.ts @@ -6,6 +6,10 @@ import { LRUCache } from 'lru-cache'; import { createHash } from 'crypto'; import type { TTSRequestPayload } from '@/types/client'; import type { TTSError, TTSAudioBuffer } from '@/types/tts'; +import { headers } from 'next/headers'; +import { auth } from '@/lib/server/auth'; +import { rateLimiter, RATE_LIMITS } from '@/lib/server/rate-limiter'; +import { isAuthEnabled } from '@/lib/server/auth-config'; type CustomVoice = string; type ExtendedSpeechParams = Omit & { @@ -47,7 +51,7 @@ async function fetchTTSBufferWithRetry( const backoff = Number(process.env.TTS_RETRY_BACKOFF ?? 2); // Retry on 429 and 5xx only; never retry aborts - for (;;) { + for (; ;) { try { const response = await openai.audio.speech.create(createParams as SpeechCreateParams, { signal }); return await response.arrayBuffer(); @@ -98,13 +102,9 @@ function makeCacheKey(input: { export async function POST(req: NextRequest) { try { - // Get API credentials from headers or fall back to environment variables - const openApiKey = req.headers.get('x-openai-key') || process.env.API_KEY || 'none'; - const openApiBaseUrl = req.headers.get('x-openai-base-url') || process.env.API_BASE; - const provider = req.headers.get('x-tts-provider') || 'openai'; + // Parse body first to get text for rate limiting const body = (await req.json()) as TTSRequestPayload; const { text, voice, speed, format, model: req_model, instructions } = body; - console.log('Received TTS request:', { provider, req_model, voice, speed, format, hasInstructions: Boolean(instructions) }); if (!text || !voice || !speed) { const errorBody: TTSError = { @@ -113,6 +113,53 @@ export async function POST(req: NextRequest) { }; return NextResponse.json(errorBody, { status: 400 }); } + + // Auth and rate limiting check (only when auth is enabled) + if (isAuthEnabled() && auth) { + const session = await auth.api.getSession({ + headers: await headers() + }); + + if (!session?.user) { + return NextResponse.json( + { code: 'UNAUTHORIZED', message: 'Authentication required' }, + { status: 401 } + ); + } + + const isAnonymous = !session.user.email || session.user.email === ''; + const charCount = text.length; + + // Check rate limit + const rateLimitResult = await rateLimiter.checkAndIncrementLimit( + { id: session.user.id, isAnonymous }, + charCount + ); + + if (!rateLimitResult.allowed) { + return NextResponse.json( + { + code: 'RATE_LIMIT_EXCEEDED', + message: 'Daily character limit exceeded', + currentCount: rateLimitResult.currentCount, + limit: rateLimitResult.limit, + remainingChars: rateLimitResult.remainingChars, + resetTime: rateLimitResult.resetTime.toISOString(), + userType: isAnonymous ? 'anonymous' : 'authenticated', + upgradeHint: isAnonymous + ? `Sign up to increase your limit from ${(RATE_LIMITS.ANONYMOUS / 1000).toFixed(0)}K to ${(RATE_LIMITS.AUTHENTICATED / 1_000_000).toFixed(0)}M characters per day` + : undefined + }, + { status: 429 } + ); + } + } + + // Get API credentials from headers or fall back to environment variables + const openApiKey = req.headers.get('x-openai-key') || process.env.API_KEY || 'none'; + const openApiBaseUrl = req.headers.get('x-openai-base-url') || process.env.API_BASE; + const provider = req.headers.get('x-tts-provider') || 'openai'; + console.log('Received TTS request:', { provider, req_model, voice, speed, format, hasInstructions: Boolean(instructions) }); // Use default Kokoro model for Deepinfra if none specified, then fall back to a safe default const rawModel = provider === 'deepinfra' && !req_model ? 'hexgrad/Kokoro-82M' : req_model; const model: SpeechCreateParams['model'] = (rawModel ?? 'gpt-4o-mini-tts') as SpeechCreateParams['model']; @@ -125,10 +172,10 @@ export async function POST(req: NextRequest) { const normalizedVoice = ( !isKokoroModel(model as string) && voice.includes('+') - ? (voice.split('+')[0].trim()) - : voice + ? (voice.split('+')[0].trim()) + : voice ) as SpeechCreateParams['voice']; - + const createParams: ExtendedSpeechParams = { model: model, voice: normalizedVoice, @@ -214,7 +261,7 @@ export async function POST(req: NextRequest) { } }); } finally { - try { req.signal.removeEventListener('abort', onAbort); } catch {} + try { req.signal.removeEventListener('abort', onAbort); } catch { } } } @@ -248,7 +295,7 @@ export async function POST(req: NextRequest) { try { buffer = await entry.promise; } finally { - try { req.signal.removeEventListener('abort', onAbort); } catch {} + try { req.signal.removeEventListener('abort', onAbort); } catch { } } return new NextResponse(buffer, { diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 164a6f8..1eed6f7 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -5,6 +5,7 @@ import { Metadata } from "next"; import { Footer } from "@/components/Footer"; import { Toaster } from 'react-hot-toast'; import { Analytics } from "@vercel/analytics/next"; +import { isAuthEnabled, getAuthBaseUrl } from '@/lib/server/auth-config'; export const metadata: Metadata = { title: "OpenReader WebUI", @@ -45,17 +46,23 @@ export const metadata: Metadata = { }, }; +// Force dynamic rendering so env vars are read at runtime, not build time +export const dynamic = 'force-dynamic'; + const isDev = process.env.NEXT_PUBLIC_NODE_ENV !== 'production' || process.env.NODE_ENV == null; -//const isDev = false; export default function RootLayout({ children }: { children: ReactNode }) { + // Read auth config inside the component to ensure runtime evaluation + const authEnabled = isAuthEnabled(); + const authBaseUrl = getAuthBaseUrl(); + return ( - +
{children} diff --git a/src/app/page.tsx b/src/app/page.tsx index fa48f73..9d83fd5 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,27 +1,32 @@ import { HomeContent } from '@/components/HomeContent'; import { SettingsModal } from '@/components/SettingsModal'; +import { UserMenu } from '@/components/auth/UserMenu'; +import { AuthLoader } from '@/components/auth/AuthLoader'; const isDev = process.env.NEXT_PUBLIC_NODE_ENV !== 'production' || process.env.NODE_ENV == null; export default function Home() { return ( -
- -
-
-

OpenReader WebUI

-

- Open source document reader {isDev ? 'self-hosted server' : 'demo app'}. - Read & listen to PDF, EPUB, MD, and TXT documents with high quality text to speech voices. -

-
-
-
-
- -
-
+ +
+ + +
+
+

OpenReader WebUI

+

+ Open source document reader {isDev ? 'self-hosted server' : 'demo app'}. + Read & listen to PDF, EPUB, MD, and TXT documents with high quality text to speech voices. +

+
+
+
+
+ +
+
+
); } diff --git a/src/app/providers.tsx b/src/app/providers.tsx index 3eb1602..5d9016d 100644 --- a/src/app/providers.tsx +++ b/src/app/providers.tsx @@ -9,9 +9,18 @@ import { TTSProvider } from '@/contexts/TTSContext'; import { ThemeProvider } from '@/contexts/ThemeContext'; import { ConfigProvider } from '@/contexts/ConfigContext'; import { HTMLProvider } from '@/contexts/HTMLContext'; +import { RateLimitProvider } from '@/components/rate-limit-provider'; +import { PrivacyPopup } from '@/components/privacy-popup'; +import { AuthConfigProvider } from '@/contexts/AuthConfigContext'; -export function Providers({ children }: { children: ReactNode }) { - return ( +interface ProvidersProps { + children: ReactNode; + authEnabled: boolean; + authBaseUrl: string | null; +} + +export function Providers({ children, authEnabled, authBaseUrl }: ProvidersProps) { + const content = ( @@ -20,6 +29,7 @@ export function Providers({ children }: { children: ReactNode }) { {children} + @@ -28,4 +38,15 @@ export function Providers({ children }: { children: ReactNode }) { ); + + // Wrap with RateLimitProvider only when auth is enabled + const wrappedContent = authEnabled ? ( + {content} + ) : content; + + return ( + + {wrappedContent} + + ); } diff --git a/src/app/signin/page.tsx b/src/app/signin/page.tsx new file mode 100644 index 0000000..4b9637d --- /dev/null +++ b/src/app/signin/page.tsx @@ -0,0 +1,286 @@ +'use client'; + +import { useState, useEffect, Suspense } from 'react'; +import { Button, Input } from '@headlessui/react'; +import { useRouter, useSearchParams } from 'next/navigation'; +import Link from 'next/link'; +import { getAuthClient } from '@/lib/auth-client'; +import { useAuthConfig } from '@/contexts/AuthConfigContext'; +import { showPrivacyPopup } from '@/components/privacy-popup'; +import { wasSignedOut, clearSignedOut } from '@/lib/session-utils'; +import { GithubIcon } from '@/components/icons/Icons'; +import { LoadingSpinner } from '@/components/Spinner'; + +function SessionExpiredLoader({ setSessionExpired }: { setSessionExpired: (v: boolean) => void }) { + const searchParams = useSearchParams(); + useEffect(() => { + const reason = searchParams.get('reason'); + setSessionExpired(reason === 'expired'); + }, [searchParams, setSessionExpired]); + return null; +} + +function SignInContent() { + const router = useRouter(); + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [loadingEmail, setLoadingEmail] = useState(false); + const [loadingGithub, setLoadingGithub] = useState(false); + const [loadingGuest, setLoadingGuest] = useState(false); + const [rememberMe, setRememberMe] = useState(true); + const [sessionExpired, setSessionExpired] = useState(false); + const [justSignedOut, setJustSignedOut] = useState(false); + const [error, setError] = useState(null); + const { authEnabled, baseUrl } = useAuthConfig(); + + const isAnyLoading = loadingEmail || loadingGithub || loadingGuest; + + // Check if auth is enabled, redirect home if not + useEffect(() => { + if (!authEnabled) { + router.push('/'); + } + }, [router, authEnabled]); + + // Detect explicit sign-out + useEffect(() => { + wasSignedOut().then(signedOut => { + if (signedOut) { + setJustSignedOut(true); + clearSignedOut(); + } + }); + }, []); + + const validateEmail = (email: string): boolean => { + return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email); + }; + + const handleSignIn = async () => { + setError(null); + + if (!email.trim() || !validateEmail(email)) { + setError('Please enter a valid email address'); + return; + } + if (!password.trim()) { + setError('Password is required'); + return; + } + + setLoadingEmail(true); + + try { + const client = getAuthClient(baseUrl); + const result = await client.signIn.email({ + email: email.trim(), + password, + rememberMe + }); + + if (result.error) { + const errorMessage = result.error.message || 'An unknown error occurred'; + if (errorMessage.toLowerCase().includes('invalid') || + errorMessage.toLowerCase().includes('credentials')) { + setError('Invalid email or password'); + } else { + setError(errorMessage); + } + } else { + router.push('/'); + } + } catch (err) { + console.error('Sign in error:', err); + setError('Unable to connect. Please try again.'); + } finally { + setLoadingEmail(false); + } + }; + + const handleGithubSignIn = async () => { + setLoadingGithub(true); + try { + const client = getAuthClient(baseUrl); + await client.signIn.social({ + provider: 'github', + callbackURL: '/' + }); + } finally { + setLoadingGithub(false); + } + }; + + const handleGuestSignIn = async () => { + setLoadingGuest(true); + setError(null); + try { + const client = getAuthClient(baseUrl); + await client.signIn.anonymous(); + router.push('/'); + } catch (e) { + console.error('Anonymous sign-in failed:', e); + setError('Unable to continue as guest. Please try again.'); + } finally { + setLoadingGuest(false); + } + }; + + if (!authEnabled) { + return null; + } + + return ( +
+ + + + +
+

+ {sessionExpired ? 'Session Expired' : 'Sign In'} +

+

+ {sessionExpired + ? 'Please sign in again to continue' + : justSignedOut + ? 'Sign in to continue' + : 'Enter your email below to login'} +

+ + {/* Alerts */} + {(sessionExpired || justSignedOut) && ( +
+

+ {sessionExpired + ? 'Your session has expired. Please sign in again.' + : "You've been signed out."} +

+
+ )} + + {error && ( +
+

{error}

+
+ )} + +
+ {/* Email */} +
+ + { setEmail(e.target.value); setError(null); }} + placeholder="me@example.com" + className="w-full rounded-lg bg-background py-2 px-3 text-foreground shadow-sm + focus:outline-none focus:ring-2 focus:ring-accent" + /> +
+ + {/* Password */} +
+ + { setPassword(e.target.value); setError(null); }} + placeholder="Password" + className="w-full rounded-lg bg-background py-2 px-3 text-foreground shadow-sm + focus:outline-none focus:ring-2 focus:ring-accent" + /> +
+ + {/* Remember Me */} + + + {/* Sign In Button */} + + + {/* GitHub */} + + + {/* Guest */} + +
+ + {/* Footer */} +
+

+ Don't have an account?{' '} + + Sign up + +

+

+ By signing in, you agree to our{' '} + +

+
+
+
+ ); +} + +export default function SignInPage() { + return ( + + +
+ }> + + + ); +} diff --git a/src/app/signup/page.tsx b/src/app/signup/page.tsx new file mode 100644 index 0000000..ea3d974 --- /dev/null +++ b/src/app/signup/page.tsx @@ -0,0 +1,240 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { Button, Input } from '@headlessui/react'; +import { useRouter } from 'next/navigation'; +import Link from 'next/link'; +import { getAuthClient } from '@/lib/auth-client'; +import { useAuthConfig } from '@/contexts/AuthConfigContext'; +import { showPrivacyPopup } from '@/components/privacy-popup'; +import { LoadingSpinner } from '@/components/Spinner'; +import toast from 'react-hot-toast'; + +export default function SignUpPage() { + const router = useRouter(); + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [passwordConfirmation, setPasswordConfirmation] = useState(''); + const [showPassword, setShowPassword] = useState(false); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const { authEnabled, baseUrl } = useAuthConfig(); + + // Check if auth is enabled, redirect home if not + useEffect(() => { + if (!authEnabled) { + router.push('/'); + } + }, [router, authEnabled]); + + const validateEmail = (email: string): boolean => { + return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email); + }; + + const validatePassword = (password: string) => { + const checks = { + length: password.length >= 8, + uppercase: /[A-Z]/.test(password), + lowercase: /[a-z]/.test(password), + number: /\d/.test(password), + special: /[!@#$%^&*(),.?":{}|<>]/.test(password) + }; + const strength = Object.values(checks).filter(Boolean).length; + return { checks, strength }; + }; + + const handleSignUp = async () => { + setError(null); + + if (!email.trim() || !validateEmail(email)) { + setError('Please enter a valid email address'); + return; + } + const { strength } = validatePassword(password); + if (strength < 3) { + setError('Password is too weak'); + return; + } + if (password !== passwordConfirmation) { + setError('Passwords do not match'); + return; + } + + setLoading(true); + + try { + const client = getAuthClient(baseUrl); + const result = await client.signUp.email({ + email: email.trim(), + password, + name: email.trim().split('@')[0], // Use part of email as name + }); + + if (result.error) { + const errorMessage = result.error.message || 'An unknown error occurred'; + if (errorMessage.toLowerCase().includes('already exists')) { + setError('An account with this email already exists'); + } else { + setError(errorMessage); + } + } else { + // Auto sign in + const signInResult = await client.signIn.email({ email: email.trim(), password }); + if (signInResult.error) { + toast.success('Account created! Please sign in.'); + router.push('/signin'); + } else { + toast.success('Account created successfully!'); + router.push('/'); + } + } + } catch (err) { + console.error('Signup error:', err); + setError('Unable to connect. Please try again.'); + } finally { + setLoading(false); + } + }; + + if (!authEnabled) { + return null; + } + + const { checks, strength } = validatePassword(password); + const strengthLabels = ['Very Weak', 'Weak', 'Fair', 'Good', 'Strong']; + const strengthColors = ['bg-red-500', 'bg-orange-500', 'bg-yellow-500', 'bg-blue-500', 'bg-green-500']; + + return ( +
+
+

Sign Up

+

Create your account to get started

+ + {error && ( +
+

{error}

+
+ )} + +
+ {/* Email */} +
+ + setEmail(e.target.value)} + placeholder="me@example.com" + className="w-full rounded-lg bg-background py-2 px-3 text-foreground shadow-sm + focus:outline-none focus:ring-2 focus:ring-accent" + /> +
+ + {/* Password */} +
+ +
+ setPassword(e.target.value)} + placeholder="Password" + className="w-full rounded-lg bg-background py-2 px-3 pr-10 text-foreground shadow-sm + focus:outline-none focus:ring-2 focus:ring-accent" + /> + +
+ + {/* Password Strength */} + {password && ( +
+
+ {[0, 1, 2, 3, 4].map((i) => ( +
+ ))} +
+

= 3 ? 'text-green-600' : 'text-red-600'}`}> + {strengthLabels[strength - 1] || 'Very Weak'} +

+
+ {Object.entries(checks).map(([key, passed]) => ( +
+ {passed ? '✓' : '○'} + + {key === 'length' && 'At least 8 characters'} + {key === 'uppercase' && 'Uppercase letter'} + {key === 'lowercase' && 'Lowercase letter'} + {key === 'number' && 'Number'} + {key === 'special' && 'Special character'} + +
+ ))} +
+
+ )} +
+ + {/* Confirm Password */} +
+ + setPasswordConfirmation(e.target.value)} + placeholder="Confirm Password" + className="w-full rounded-lg bg-background py-2 px-3 text-foreground shadow-sm + focus:outline-none focus:ring-2 focus:ring-accent" + /> + {passwordConfirmation && password && ( +

+ {password === passwordConfirmation ? '✓ Passwords match' : '✗ Passwords do not match'} +

+ )} +
+ + {/* Sign Up Button */} + +
+ + {/* Footer */} +
+

+ Already have an account?{' '} + + Sign in + +

+

+ By creating an account, you agree to our{' '} + +

+
+
+
+ ); +} diff --git a/src/components/SettingsModal.tsx b/src/components/SettingsModal.tsx index dc25e1f..da8a8a9 100644 --- a/src/components/SettingsModal.tsx +++ b/src/components/SettingsModal.tsx @@ -19,6 +19,7 @@ import { TabPanels, TabPanel, } from '@headlessui/react'; +import Link from 'next/link'; import { useTheme } from '@/contexts/ThemeContext'; import { useConfig } from '@/contexts/ConfigContext'; import { ChevronUpDownIcon, CheckIcon, SettingsIcon } from '@/components/icons/Icons'; @@ -31,6 +32,10 @@ import { THEMES } from '@/contexts/ThemeContext'; import { deleteServerDocuments } from '@/lib/client'; import { DocumentSelectionModal } from '@/components/DocumentSelectionModal'; import { BaseDocument } from '@/types/documents'; +import { getAuthClient } from '@/lib/auth-client'; +import { useAuthSession } from '@/hooks/useAuth'; +import { markSignedOut, clearSignedOut } from '@/lib/session-utils'; +import { useAuthConfig } from '@/contexts/AuthConfigContext'; const isDev = process.env.NEXT_PUBLIC_NODE_ENV !== 'production' || process.env.NODE_ENV == null; @@ -56,17 +61,17 @@ export function SettingsModal() { const [isImportingLibrary, setIsImportingLibrary] = useState(false); const [isSelectionModalOpen, setIsSelectionModalOpen] = useState(false); const [selectionModalProps, setSelectionModalProps] = useState<{ - title: string; - confirmLabel: string; - mode: 'library' | 'load' | 'save'; - defaultSelected: boolean; - initialFiles?: BaseDocument[]; - fetcher?: () => Promise; + title: string; + confirmLabel: string; + mode: 'library' | 'load' | 'save'; + defaultSelected: boolean; + initialFiles?: BaseDocument[]; + fetcher?: () => Promise; }>({ - title: '', - confirmLabel: '', - mode: 'library', - defaultSelected: false + title: '', + confirmLabel: '', + mode: 'library', + defaultSelected: false }); const [showProgress, setShowProgress] = useState(false); @@ -76,7 +81,10 @@ export function SettingsModal() { const selectedTheme = themes.find(t => t.id === theme) || themes[0]; const [showClearLocalConfirm, setShowClearLocalConfirm] = useState(false); const [showClearServerConfirm, setShowClearServerConfirm] = useState(false); + const [showDeleteAccountConfirm, setShowDeleteAccountConfirm] = useState(false); const { progress, setProgress, estimatedTimeRemaining } = useTimeEstimation(); + const { authEnabled, baseUrl: authBaseUrl } = useAuthConfig(); + const { data: session } = useAuthSession(); const ttsProviders = useMemo(() => [ { id: 'custom-openai', name: 'Custom OpenAI-Like' }, @@ -171,53 +179,53 @@ export function SettingsModal() { const pdfs = await getAllPdfDocuments(); const epubs = await getAllEpubDocuments(); const htmls = await getAllHtmlDocuments(); - + const allDocs: BaseDocument[] = [ - ...pdfs.map(d => ({ ...d, type: 'pdf' as const })), - ...epubs.map(d => ({ ...d, type: 'epub' as const })), - ...htmls.map(d => ({ ...d, type: 'html' as const })) + ...pdfs.map(d => ({ ...d, type: 'pdf' as const })), + ...epubs.map(d => ({ ...d, type: 'epub' as const })), + ...htmls.map(d => ({ ...d, type: 'html' as const })) ]; setSelectionModalProps({ - title: 'Save to Server', - confirmLabel: 'Save', - mode: 'save', - defaultSelected: true, - initialFiles: allDocs + title: 'Save to Server', + confirmLabel: 'Save', + mode: 'save', + defaultSelected: true, + initialFiles: allDocs }); setIsSelectionModalOpen(true); }; const handleLoad = async () => { setSelectionModalProps({ - title: 'Load from Server', - confirmLabel: 'Load', - mode: 'load', - defaultSelected: true, - fetcher: async () => { - const res = await fetch('/api/documents?list=true'); - if (!res.ok) throw new Error('Failed to list server documents'); - const data = await res.json(); - // Handle case where API might return error object - if (data.error) throw new Error(data.error); - return data.documents || []; - } + title: 'Load from Server', + confirmLabel: 'Load', + mode: 'load', + defaultSelected: true, + fetcher: async () => { + const res = await fetch('/api/documents?list=true'); + if (!res.ok) throw new Error('Failed to list server documents'); + const data = await res.json(); + // Handle case where API might return error object + if (data.error) throw new Error(data.error); + return data.documents || []; + } }); setIsSelectionModalOpen(true); }; const handleImportLibrary = async () => { setSelectionModalProps({ - title: 'Import from Library', - confirmLabel: 'Import', - mode: 'library', - defaultSelected: false, - fetcher: async () => { - const res = await fetch('/api/documents/library?limit=10000'); - if (!res.ok) throw new Error('Failed to list library documents'); - const data = await res.json(); - return data.documents || []; - } + title: 'Import from Library', + confirmLabel: 'Import', + mode: 'library', + defaultSelected: false, + fetcher: async () => { + const res = await fetch('/api/documents/library?limit=10000'); + if (!res.ok) throw new Error('Failed to list library documents'); + const data = await res.json(); + return data.documents || []; + } }); setIsSelectionModalOpen(true); }; @@ -225,9 +233,9 @@ export function SettingsModal() { const handleModalConfirm = async (selectedFiles: BaseDocument[]) => { const controller = new AbortController(); setAbortController(controller); - + const mode = selectionModalProps.mode; - + // Close modal? Maybe keep open until started? // Let's close it here, process starts. // Actually we keep it open if we want to show loading state INSIDE modal? @@ -236,57 +244,57 @@ export function SettingsModal() { setIsSelectionModalOpen(false); try { - setShowProgress(true); - setProgress(0); - - if (mode === 'save') { - setIsSyncing(true); - setOperationType('sync'); - setStatusMessage('Preparing documents...'); - await syncSelectedDocumentsToServer(selectedFiles, (progress, status) => { - if (controller.signal.aborted) return; - setProgress(progress); - if (status) setStatusMessage(status); - }, controller.signal); - } else if (mode === 'load') { - setIsLoading(true); - setOperationType('load'); - setStatusMessage('Downloading documents...'); - // Need ids - const ids = selectedFiles.map(f => f.id); - await loadSelectedDocumentsFromServer(ids, (progress, status) => { - if (controller.signal.aborted) return; - setProgress(progress); - if (status) setStatusMessage(status); - }, controller.signal); - if (!controller.signal.aborted) setStatusMessage('Documents loaded'); - } else if (mode === 'library') { - setIsImportingLibrary(true); - setOperationType('library'); - setStatusMessage('Importing selected documents...'); - await importSelectedDocuments(selectedFiles, (progress, status) => { - if (controller.signal.aborted) return; - setProgress(progress); - if (status) setStatusMessage(status); - }, controller.signal); - } + setShowProgress(true); + setProgress(0); + + if (mode === 'save') { + setIsSyncing(true); + setOperationType('sync'); + setStatusMessage('Preparing documents...'); + await syncSelectedDocumentsToServer(selectedFiles, (progress, status) => { + if (controller.signal.aborted) return; + setProgress(progress); + if (status) setStatusMessage(status); + }, controller.signal); + } else if (mode === 'load') { + setIsLoading(true); + setOperationType('load'); + setStatusMessage('Downloading documents...'); + // Need ids + const ids = selectedFiles.map(f => f.id); + await loadSelectedDocumentsFromServer(ids, (progress, status) => { + if (controller.signal.aborted) return; + setProgress(progress); + if (status) setStatusMessage(status); + }, controller.signal); + if (!controller.signal.aborted) setStatusMessage('Documents loaded'); + } else if (mode === 'library') { + setIsImportingLibrary(true); + setOperationType('library'); + setStatusMessage('Importing selected documents...'); + await importSelectedDocuments(selectedFiles, (progress, status) => { + if (controller.signal.aborted) return; + setProgress(progress); + if (status) setStatusMessage(status); + }, controller.signal); + } } catch (error) { - if (controller.signal.aborted) { - console.log(`${mode} operation cancelled`); - setStatusMessage('Operation cancelled'); - } else { - console.error(`${mode} failed:`, error); - setStatusMessage(`${mode} failed. Please try again.`); - } + if (controller.signal.aborted) { + console.log(`${mode} operation cancelled`); + setStatusMessage('Operation cancelled'); + } else { + console.error(`${mode} failed:`, error); + setStatusMessage(`${mode} failed. Please try again.`); + } } finally { - setIsSyncing(false); - setIsLoading(false); - setIsImportingLibrary(false); - setShowProgress(false); - setProgress(0); - setStatusMessage(''); - setAbortController(null); + setIsSyncing(false); + setIsLoading(false); + setIsImportingLibrary(false); + setShowProgress(false); + setProgress(0); + setStatusMessage(''); + setAbortController(null); } }; @@ -306,6 +314,32 @@ export function SettingsModal() { setShowClearServerConfirm(false); }; + + + const handleSignOut = async () => { + await markSignedOut(); + const client = getAuthClient(authBaseUrl); + await client.signOut(); + window.location.reload(); + }; + + const handleDeleteAccount = async () => { + try { + const res = await fetch('/api/account/delete', { method: 'DELETE' }); + if (!res.ok) throw new Error('Failed to delete account'); + + // Sign out locally + const client = getAuthClient(authBaseUrl); + await client.signOut(); + // Clear the "signed out" flag so AuthLoader triggers auto-anon-login + clearSignedOut(); + window.location.href = '/'; + } catch (error) { + console.error('Failed to delete account:', error); + } + setShowDeleteAccountConfirm(false); + }; + const handleInputChange = (type: 'apiKey' | 'baseUrl', value: string) => { if (type === 'apiKey') { setLocalApiKey(value === '' ? '' : value); @@ -330,8 +364,9 @@ export function SettingsModal() { const tabs = [ { name: 'API', icon: '🔑' }, - { name: 'Appearance', icon: '✨' }, - { name: 'Documents', icon: '📄' } + { name: 'Theme', icon: '✨' }, + { name: 'Docs', icon: '📄' }, + ...(authEnabled ? [{ name: 'User', icon: '👤' }] : []) ]; return ( @@ -441,8 +476,7 @@ export function SettingsModal() { - `relative cursor-pointer select-none py-1.5 pl-10 pr-4 ${ - active ? 'bg-offbase text-accent' : 'text-foreground' + `relative cursor-pointer select-none py-1.5 pl-10 pr-4 ${active ? 'bg-offbase text-accent' : 'text-foreground' }` } value={provider} @@ -516,8 +550,7 @@ export function SettingsModal() { - `relative cursor-pointer select-none py-1.5 pl-10 pr-4 ${ - active ? 'bg-offbase text-accent' : 'text-foreground' + `relative cursor-pointer select-none py-1.5 pl-10 pr-4 ${active ? 'bg-offbase text-accent' : 'text-foreground' }` } value={model} @@ -599,13 +632,14 @@ export function SettingsModal() { setModelValue('kokoro'); setCustomModelInput(''); setLocalTTSInstructions(''); + setLocalTTSInstructions(''); }} > Reset
+ + {authEnabled && ( + +
+
+

Current Session

+
+

Logged in as:

+

{session?.user?.name || 'Guest'}

+

{session?.user?.email}

+ {session?.user?.isAnonymous && ( +

Anonymous / Guest Account

+ )} +
+
+ +
+ {!session?.user?.isAnonymous ? ( + <> + + +
+

Danger Zone

+ +

+ This will permanently delete your account and data. You will be returned to a fresh guest session. +

+
+ + ) : ( +
+

+ You are using a temporary guest account. Sign up to save your progress permanently. +

+
+ + + + + + +
+
+ )} +
+
+
+ )} @@ -752,7 +854,7 @@ export function SettingsModal() { - + - - setShowDeleteAccountConfirm(false)} + onConfirm={handleDeleteAccount} + title="Delete Account" + message="Are you sure you want to delete your account? This action cannot be undone and all your data will be lost." + confirmText="Delete Account" + isDangerous={true} + /> + + - !isImportingLibrary && !isSyncing && !isLoading && setIsSelectionModalOpen(false)} onConfirm={handleModalConfirm} diff --git a/src/components/Spinner.tsx b/src/components/Spinner.tsx index 1c4e0b2..3344e67 100644 --- a/src/components/Spinner.tsx +++ b/src/components/Spinner.tsx @@ -1,7 +1,11 @@ // Loading spinner component -export function LoadingSpinner() { +interface SpinnerProps { + className?: string; +} + +export function LoadingSpinner({ className }: SpinnerProps = {}) { return ( -
+
); diff --git a/src/components/auth/AuthLoader.tsx b/src/components/auth/AuthLoader.tsx new file mode 100644 index 0000000..80877db --- /dev/null +++ b/src/components/auth/AuthLoader.tsx @@ -0,0 +1,76 @@ +'use client'; + +import { useEffect, useState, ReactNode } from 'react'; +import { useAuthConfig } from '@/contexts/AuthConfigContext'; +import { useAuthSession } from '@/hooks/useAuth'; +import { getAuthClient } from '@/lib/auth-client'; +import { wasSignedOut } from '@/lib/session-utils'; +import { LoadingSpinner } from '@/components/Spinner'; + +export function AuthLoader({ children }: { children: ReactNode }) { + const { authEnabled, baseUrl } = useAuthConfig(); + const { data: session, isPending } = useAuthSession(); + const [isAutoLoggingIn, setIsAutoLoggingIn] = useState(false); + const [isCheckingSignOut, setIsCheckingSignOut] = useState(true); + + useEffect(() => { + // Determine if we need to check sign-out status or proceed + const checkStatus = async () => { + // If auth is disabled, stop checking immediately + if (!authEnabled) { + setIsCheckingSignOut(false); + return; + } + + // If session is still loading, wait + if (isPending) return; + + // If we have a session, we are done checking + if (session) { + setIsCheckingSignOut(false); + return; + } + + // No session, check if explicitly signed out + const userWasSignedOut = await wasSignedOut(); + if (userWasSignedOut) { + setIsCheckingSignOut(false); // Render children unauthenticated + return; + } + + // Not signed out, start auto-login + setIsAutoLoggingIn(true); + setIsCheckingSignOut(false); // Stop checking sign-out, now in auto-login mode + + try { + const client = getAuthClient(baseUrl); + await client.signIn.anonymous(); + } catch (err) { + console.error('Auto-login failed', err); + } finally { + setIsAutoLoggingIn(false); + } + }; + + checkStatus(); + }, [session, isPending, authEnabled, baseUrl]); + + // Show loader if: + // 1. Auth client is initializing (isPending) AND auth is enabled + // 2. We are checking the sign-out status (Dexie) + // 3. We are actively auto-logging in + const isLoading = (isPending && authEnabled) || isCheckingSignOut || isAutoLoggingIn; + + if (isLoading) { + return ( +
+ +

+ {isAutoLoggingIn ? 'Logging in anonymously...' : 'Loading...'} +

+
+ ); + } + + return <>{children}; +} diff --git a/src/components/auth/SessionManager.tsx b/src/components/auth/SessionManager.tsx new file mode 100644 index 0000000..1d70d6d --- /dev/null +++ b/src/components/auth/SessionManager.tsx @@ -0,0 +1,48 @@ +'use client'; + +import { useEffect, useRef } from 'react'; +import { useAuthConfig } from '@/contexts/AuthConfigContext'; +import { useAuthSession } from '@/hooks/useAuth'; +import { getAuthClient } from '@/lib/auth-client'; +import { wasSignedOut } from '@/lib/session-utils'; + +export function SessionManager() { + const { authEnabled, baseUrl } = useAuthConfig(); + const { data: session, isPending } = useAuthSession(); + const attemptRef = useRef(false); + + useEffect(() => { + // Only run if auth is enabled + if (!authEnabled) return; + + // Wait for session check to complete + if (isPending) return; + + // If we have a session, we're good + if (session) return; + + // Prevent multiple attempts + if (attemptRef.current) return; + + const checkAndSignIn = async () => { + attemptRef.current = true; + try { + // Check if user explicitly signed out + const signedOut = await wasSignedOut(); + + // If not explicitly signed out, sign in anonymously + if (!signedOut) { + console.log('No session found, signing in anonymously...'); + const client = getAuthClient(baseUrl); + await client.signIn.anonymous(); + } + } catch (error) { + console.error('Error in session manager:', error); + } + }; + + checkAndSignIn(); + }, [session, isPending, authEnabled, baseUrl]); + + return null; +} diff --git a/src/components/auth/UserMenu.tsx b/src/components/auth/UserMenu.tsx new file mode 100644 index 0000000..cfeec5d --- /dev/null +++ b/src/components/auth/UserMenu.tsx @@ -0,0 +1,66 @@ +'use client'; + +import { Button } from '@headlessui/react'; +import Link from 'next/link'; +import { useAuthConfig } from '@/contexts/AuthConfigContext'; +import { useAuthSession } from '@/hooks/useAuth'; +import { getAuthClient } from '@/lib/auth-client'; +import { markSignedOut } from '@/lib/session-utils'; +import { useRouter } from 'next/navigation'; + +export function UserMenu() { + const { authEnabled, baseUrl } = useAuthConfig(); + const { data: session, isPending } = useAuthSession(); + const router = useRouter(); + + if (!authEnabled || isPending) return null; + + const handleSignOut = async () => { + await markSignedOut(); + const client = getAuthClient(baseUrl); + await client.signOut(); + router.refresh(); + }; + + if (!session || session.user.isAnonymous) { + return ( +
+ + + + + + +
+ ); + } + + return ( +
+
+ + {session.user.name || 'Guest'} + + + {session.user.email} + +
+ + +
+ ); +} diff --git a/src/components/privacy-popup.tsx b/src/components/privacy-popup.tsx new file mode 100644 index 0000000..9775544 --- /dev/null +++ b/src/components/privacy-popup.tsx @@ -0,0 +1,211 @@ +'use client'; + +import { Fragment, useState, useEffect, useCallback } from 'react'; +import { + Dialog, + DialogPanel, + DialogTitle, + Transition, + TransitionChild, + Button, +} from '@headlessui/react'; +import { updateAppConfig, getAppConfig } from '@/lib/dexie'; + +interface PrivacyPopupProps { + onAccept?: () => void; +} + +export function PrivacyPopup({ onAccept }: PrivacyPopupProps) { + const [isOpen, setIsOpen] = useState(false); + + const checkPrivacyAccepted = useCallback(async () => { + const config = await getAppConfig(); + if (!config?.privacyAccepted) { + setIsOpen(true); + } + }, []); + + useEffect(() => { + checkPrivacyAccepted(); + }, [checkPrivacyAccepted]); + + const handleAccept = async () => { + await updateAppConfig({ privacyAccepted: true }); + setIsOpen(false); + onAccept?.(); + }; + + return ( + + { }}> + +
+ + +
+
+ + + + Privacy & Data Usage + + +
+

Documents are uploaded to your local browser cache.

+

+ Each paragraph of the document you are viewing is sent to Deepinfra + for audio generation through a Vercel backend proxy, containing a + shared caching pool. +

+

The audio is streamed back to your browser and played in real-time.

+

+ Self-hosting is the recommended way to use this app for a truly secure experience. +

+

+ This site uses Vercel Analytics to collect anonymous usage data to help improve the service. +

+
+ +
+ +
+
+
+
+
+
+
+ ); +} + +/** + * Function to programmatically show the privacy popup + * This can be called from signin/signup components + */ +export function showPrivacyPopup(): void { + // Create a temporary container for the popup + const container = document.createElement('div'); + container.id = 'privacy-popup-container'; + document.body.appendChild(container); + + // Import React and render the popup + import('react-dom/client').then(({ createRoot }) => { + import('react').then((React) => { + const root = createRoot(container); + + const PopupWrapper = () => { + const [show, setShow] = useState(true); + + const handleClose = () => { + setShow(false); + setTimeout(() => { + root.unmount(); + container.remove(); + }, 300); + }; + + if (!show) return null; + + return ( + + + +
+ + +
+
+ + + + Privacy & Data Usage + + +
+

Documents are uploaded to your local browser cache.

+

+ Each paragraph of the document you are viewing is sent to Deepinfra + for audio generation through a Vercel backend proxy, containing a + shared caching pool. +

+

The audio is streamed back to your browser and played in real-time.

+

+ Self-hosting is the recommended way to use this app for a truly secure experience. +

+

+ This site uses Vercel Analytics to collect anonymous usage data. +

+
+ +
+ +
+
+
+
+
+
+
+ ); + }; + + root.render(React.createElement(PopupWrapper)); + }); + }); +} diff --git a/src/components/rate-limit-banner.tsx b/src/components/rate-limit-banner.tsx new file mode 100644 index 0000000..49a8a7c --- /dev/null +++ b/src/components/rate-limit-banner.tsx @@ -0,0 +1,85 @@ +'use client'; + +import { useRateLimit, formatCharCount } from '@/components/rate-limit-provider'; +import { useAuthConfig } from '@/contexts/AuthConfigContext'; +import Link from 'next/link'; + +interface RateLimitBannerProps { + className?: string; +} + +export function RateLimitBanner({ className = '' }: RateLimitBannerProps) { + const { status, isAtLimit, timeUntilReset } = useRateLimit(); + const { authEnabled } = useAuthConfig(); + + // Don't show banner if auth is not enabled or if not at limit + if (!authEnabled || !status?.authEnabled || !isAtLimit) { + return null; + } + + const isAnonymous = status.userType === 'anonymous'; + + return ( +
+
+
+

+ Daily TTS limit reached +

+

+ {`You've used ${formatCharCount(status.currentCount)} of ${formatCharCount(status.limit)} characters today.`} + {' Resets in '}{timeUntilReset}. +

+
+ + {isAnonymous && ( + + Sign up for 4x more + + )} +
+
+ ); +} + +/** + * Compact version for inline display + */ +export function RateLimitIndicator({ className = '' }: RateLimitBannerProps) { + const { status, isAtLimit } = useRateLimit(); + const { authEnabled } = useAuthConfig(); + + // Don't show if auth is not enabled + if (!authEnabled || !status?.authEnabled) { + return null; + } + + const percentage = status.limit === Infinity + ? 0 + : Math.min(100, (status.currentCount / status.limit) * 100); + + const isWarning = percentage >= 80; + + if (isAtLimit) { + return ( + + Limit reached + + ); + } + + if (isWarning) { + return ( + + {formatCharCount(status.remainingChars)} chars left + + ); + } + + return null; +} diff --git a/src/components/rate-limit-provider.tsx b/src/components/rate-limit-provider.tsx new file mode 100644 index 0000000..6eb4581 --- /dev/null +++ b/src/components/rate-limit-provider.tsx @@ -0,0 +1,206 @@ +'use client'; + +import React, { createContext, useContext, useState, useEffect, useCallback, useRef } from 'react'; +import { useAuthConfig } from '@/contexts/AuthConfigContext'; + +export interface RateLimitStatus { + allowed: boolean; + currentCount: number; + limit: number; + remainingChars: number; + resetTime: Date; + userType: 'anonymous' | 'authenticated' | 'unauthenticated'; + authEnabled: boolean; +} + +export interface RateLimitContextType { + status: RateLimitStatus | null; + loading: boolean; + error: string | null; + refresh: () => Promise; + isAtLimit: boolean; + timeUntilReset: string; + incrementCount: (charCount: number) => void; + onTTSStart: () => void; + onTTSComplete: () => void; +} + +const RateLimitContext = createContext(null); + +export function useRateLimit(): RateLimitContextType { + const context = useContext(RateLimitContext); + if (!context) { + throw new Error('useRateLimit must be used within a RateLimitProvider'); + } + return context; +} + +interface RateLimitProviderProps { + children: React.ReactNode; +} + +function calculateTimeUntilReset(resetTime: Date): string { + const now = new Date(); + const timeDiff = resetTime.getTime() - now.getTime(); + + if (timeDiff <= 0) { + return 'Soon'; + } + + const hours = Math.floor(timeDiff / (1000 * 60 * 60)); + const minutes = Math.floor((timeDiff % (1000 * 60 * 60)) / (1000 * 60)); + + if (hours > 0) { + return `${hours}h ${minutes}m`; + } else { + return `${minutes}m`; + } +} + +function formatCharCount(count: number): string { + if (count >= 1_000_000) { + return `${(count / 1_000_000).toFixed(1)}M`; + } else if (count >= 1_000) { + return `${(count / 1_000).toFixed(0)}K`; + } + return count.toString(); +} + +export { formatCharCount }; + +export function RateLimitProvider({ children }: RateLimitProviderProps) { + const [status, setStatus] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const { authEnabled } = useAuthConfig(); + + // Track pending TTS operations to delay count updates + const pendingTTSRef = useRef(0); + const updateTimeoutRef = useRef(null); + + const fetchStatus = useCallback(async () => { + // Skip if auth is not enabled + if (!authEnabled) { + const tomorrow = new Date(); + tomorrow.setDate(tomorrow.getDate() + 1); + tomorrow.setHours(0, 0, 0, 0); + + setStatus({ + allowed: true, + currentCount: 0, + limit: Infinity, + remainingChars: Infinity, + resetTime: tomorrow, + userType: 'unauthenticated', + authEnabled: false + }); + setLoading(false); + return; + } + + try { + setLoading(true); + setError(null); + + const response = await fetch('/api/rate-limit/status'); + + if (!response.ok) { + throw new Error(`Failed to fetch rate limit status: ${response.status}`); + } + + const data = await response.json(); + + setStatus({ + ...data, + resetTime: new Date(data.resetTime) + }); + } catch (err) { + console.error('Error fetching rate limit status:', err); + setError(err instanceof Error ? err.message : 'Unknown error'); + } finally { + setLoading(false); + } + }, [authEnabled]); + + useEffect(() => { + fetchStatus(); + }, [fetchStatus]); + + // Calculate time until reset + const timeUntilReset = status ? calculateTimeUntilReset(status.resetTime) : ''; + const isAtLimit = status ? status.remainingChars <= 0 : false; + + // Increment count locally (for immediate UI feedback) + const incrementCount = useCallback((charCount: number) => { + setStatus(prevStatus => { + if (!prevStatus) return prevStatus; + + const newCurrentCount = prevStatus.currentCount + charCount; + const newRemainingChars = Math.max(0, prevStatus.limit - newCurrentCount); + + return { + ...prevStatus, + currentCount: newCurrentCount, + remainingChars: newRemainingChars, + allowed: newRemainingChars > 0 + }; + }); + }, []); + + // Called when a TTS request starts + const onTTSStart = useCallback(() => { + pendingTTSRef.current += 1; + + // Clear any existing timeout + if (updateTimeoutRef.current) { + clearTimeout(updateTimeoutRef.current); + updateTimeoutRef.current = null; + } + }, []); + + // Called when a TTS request completes (success or error) + const onTTSComplete = useCallback(() => { + pendingTTSRef.current = Math.max(0, pendingTTSRef.current - 1); + + // Clear any existing timeout + if (updateTimeoutRef.current) { + clearTimeout(updateTimeoutRef.current); + updateTimeoutRef.current = null; + } + + // If no more pending requests, schedule an update + if (pendingTTSRef.current === 0) { + updateTimeoutRef.current = setTimeout(() => { + fetchStatus(); + updateTimeoutRef.current = null; + }, 1000); // Wait 1 second after completion to refresh + } + }, [fetchStatus]); + + // Cleanup timeout on unmount + useEffect(() => { + return () => { + if (updateTimeoutRef.current) { + clearTimeout(updateTimeoutRef.current); + } + }; + }, []); + + const contextValue: RateLimitContextType = { + status, + loading, + error, + refresh: fetchStatus, + isAtLimit, + timeUntilReset, + incrementCount, + onTTSStart, + onTTSComplete + }; + + return ( + + {children} + + ); +} \ No newline at end of file diff --git a/src/contexts/AuthConfigContext.tsx b/src/contexts/AuthConfigContext.tsx new file mode 100644 index 0000000..f95dd22 --- /dev/null +++ b/src/contexts/AuthConfigContext.tsx @@ -0,0 +1,33 @@ +'use client'; + +import { createContext, useContext, ReactNode } from 'react'; + +interface AuthConfig { + authEnabled: boolean; + baseUrl: string | null; +} + +const AuthConfigContext = createContext({ + authEnabled: false, + baseUrl: null, +}); + +export function AuthConfigProvider({ + children, + authEnabled, + baseUrl, +}: { + children: ReactNode; + authEnabled: boolean; + baseUrl: string | null; +}) { + return ( + + {children} + + ); +} + +export function useAuthConfig() { + return useContext(AuthConfigContext); +} diff --git a/src/hooks/useAuth.ts b/src/hooks/useAuth.ts new file mode 100644 index 0000000..2f18287 --- /dev/null +++ b/src/hooks/useAuth.ts @@ -0,0 +1,36 @@ +'use client'; + +import { useMemo } from 'react'; +import { useAuthConfig } from '@/contexts/AuthConfigContext'; +import { getAuthClient } from '@/lib/auth-client'; + +/** + * Hook that provides auth client methods configured with the correct baseUrl from server. + * Use this hook in components that need to call auth methods (signIn, signUp, etc.) + */ +export function useAuth() { + const { baseUrl, authEnabled } = useAuthConfig(); + + const client = useMemo(() => { + return getAuthClient(baseUrl); + }, [baseUrl]); + + return { + authEnabled, + baseUrl, + signIn: client.signIn, + signUp: client.signUp, + signOut: client.signOut, + useSession: client.useSession, + getSession: client.getSession, + }; +} + +/** + * Hook for session that uses the correct baseUrl from context + */ +export function useAuthSession() { + const { baseUrl } = useAuthConfig(); + const client = useMemo(() => getAuthClient(baseUrl), [baseUrl]); + return client.useSession(); +} diff --git a/src/lib/auth-client.ts b/src/lib/auth-client.ts new file mode 100644 index 0000000..cabc47a --- /dev/null +++ b/src/lib/auth-client.ts @@ -0,0 +1,35 @@ +import { createAuthClient } from "better-auth/react"; +import { anonymousClient } from "better-auth/client/plugins"; + +// Factory function to create auth client with specific baseUrl +function createAuthClientWithUrl(baseUrl: string) { + return createAuthClient({ + baseURL: baseUrl, + plugins: [anonymousClient()], + }); +} + +// Cache for auth client instances by baseUrl +const clientCache = new Map>(); + +export function getAuthClient(baseUrl: string | null) { + const effectiveUrl = baseUrl || "http://localhost:3003"; + + if (!clientCache.has(effectiveUrl)) { + clientCache.set(effectiveUrl, createAuthClientWithUrl(effectiveUrl)); + } + + return clientCache.get(effectiveUrl)!; +} + +// Default client for backwards compatibility (will use localhost in dev) +// Components should prefer useAuth() hook which gets baseUrl from context +export const authClient = getAuthClient(null); + +export const { + signIn, + signUp, + signOut, + useSession, + getSession +} = authClient; \ No newline at end of file diff --git a/src/lib/server/auth-config.ts b/src/lib/server/auth-config.ts new file mode 100644 index 0000000..2279e9e --- /dev/null +++ b/src/lib/server/auth-config.ts @@ -0,0 +1,17 @@ +/** + * Centralized auth configuration check. + * Auth is only enabled when BOTH BETTER_AUTH_SECRET and BETTER_AUTH_URL are set. + */ +export function isAuthEnabled(): boolean { + return !!(process.env.BETTER_AUTH_SECRET && process.env.BETTER_AUTH_URL); +} + +/** + * Get the auth base URL if auth is enabled, otherwise null. + */ +export function getAuthBaseUrl(): string | null { + if (!isAuthEnabled()) { + return null; + } + return process.env.BETTER_AUTH_URL || null; +} diff --git a/src/lib/server/auth.ts b/src/lib/server/auth.ts new file mode 100644 index 0000000..fcb4414 --- /dev/null +++ b/src/lib/server/auth.ts @@ -0,0 +1,106 @@ +import { betterAuth } from "better-auth"; +import { nextCookies } from "better-auth/next-js"; +import { anonymous } from "better-auth/plugins"; +import { Pool } from "pg"; +import { rateLimiter } from "@/lib/server/rate-limiter"; +import { isAuthEnabled } from "@/lib/server/auth-config"; + +const getAuthDatabase = () => { + if (process.env.POSTGRES_URL) { + return new Pool({ + connectionString: process.env.POSTGRES_URL, + ssl: process.env.NODE_ENV === "production" ? { rejectUnauthorized: false } : false, + }); + } + + // Fallback to SQLite + // We need to dynamically import or require better-sqlite3 to avoid build issues if it's not used? + // Actually better-auth supports it directly, we just pass the instance. + /* eslint-disable @typescript-eslint/no-require-imports */ + const Database = require("better-sqlite3"); + const path = require("path"); + const fs = require("fs"); + /* eslint-enable @typescript-eslint/no-require-imports */ + + // Ensure directory exists + const dbPath = path.join(process.cwd(), 'docstore', 'sqlite3.db'); + const dir = path.dirname(dbPath); + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + + return new Database(dbPath); +}; + +const createAuth = () => betterAuth({ + database: getAuthDatabase(), + secret: process.env.BETTER_AUTH_SECRET!, + baseURL: process.env.BETTER_AUTH_URL || "http://localhost:3003", + emailAndPassword: { + enabled: true, + requireEmailVerification: false, // Set to true in production + async sendResetPassword(data) { + // Send an email to the user with a link to reset their password + console.log("Password reset requested for:", data.user.email); + }, + }, + socialProviders: { + ...(process.env.GITHUB_CLIENT_ID && process.env.GITHUB_CLIENT_SECRET && { + github: { + clientId: process.env.GITHUB_CLIENT_ID, + clientSecret: process.env.GITHUB_CLIENT_SECRET, + }, + }), + }, + session: { + expiresIn: 60 * 60 * 24 * 7, // 7 days (reasonable for user experience) + updateAge: 60 * 60 * 1, // 1 hour (refresh more frequently) + cookieCache: { + maxAge: 60 * 60 * 24 * 7, // 7 days for cookie cache + }, + }, + advanced: { + database: { + generateId: () => { + // Generate user-friendly IDs similar to current system + const timestamp = Date.now(); + const random = Math.random().toString(36).substring(2, 8); + return `user-${timestamp}-${random}`; + }, + }, + }, + plugins: [ + nextCookies(), // Enable Next.js cookie handling + anonymous({ + onLinkAccount: async ({ anonymousUser, newUser }) => { + try { + // Log when anonymous user links to a real account + console.log("Anonymous user linked to account:", { + anonymousUserId: anonymousUser.user.id, + newUserId: newUser.user.id, + newUserEmail: newUser.user.email, + }); + + // Transfer rate limiting data (TTS char counts) from anonymous user to authenticated user + try { + await rateLimiter.transferAnonymousUsage(anonymousUser.user.id, newUser.user.id); + console.log(`Successfully transferred rate limit data from anonymous user ${anonymousUser.user.id} to user ${newUser.user.id}`); + } catch (error) { + console.error("Error transferring rate limit data during account linking:", error); + // Don't throw here to prevent blocking the account linking process + } + } catch (error) { + console.error("Error in onLinkAccount callback:", error); + // Don't throw here to prevent blocking the account linking process + } + // Note: Anonymous user will be automatically deleted after this callback completes + }, + }), + ], +}); + +export const auth = isAuthEnabled() ? createAuth() : null; + +type AuthInstance = ReturnType; +export type Session = AuthInstance["$Infer"]["Session"]; +export type User = AuthInstance["$Infer"]["Session"]["user"]; \ No newline at end of file diff --git a/src/lib/server/db-adapter.ts b/src/lib/server/db-adapter.ts new file mode 100644 index 0000000..8b6d11f --- /dev/null +++ b/src/lib/server/db-adapter.ts @@ -0,0 +1,125 @@ +import { Pool, PoolClient } from "pg"; +import Database from "better-sqlite3"; +import fs from "fs"; +import path from "path"; + +/** + * Common interface for database operations to support both Postgres and SQLite + */ +export interface DBAdapter { + query(text: string, params?: unknown[]): Promise<{ rows: unknown[], rowCount?: number }>; + transaction(callback: (client: DBAdapter) => Promise): Promise; +} + +export class PostgresAdapter implements DBAdapter { + constructor(private pool: Pool) { } + + async query(text: string, params?: unknown[]) { + try { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const result = await this.pool.query(text, params as any[]); + return { + rows: result.rows, + rowCount: result.rowCount ?? undefined + }; + } catch (error) { + console.error("Postgres Query Error:", error); + throw error; + } + } + + async transaction(callback: (client: DBAdapter) => Promise): Promise { + const client = await this.pool.connect(); + try { + await client.query("BEGIN"); + const connectedAdapter = new PostgresConnectedAdapter(client); + const result = await callback(connectedAdapter); + await client.query("COMMIT"); + return result; + } catch (e) { + await client.query("ROLLBACK"); + throw e; + } finally { + client.release(); + } + } +} + +class PostgresConnectedAdapter implements DBAdapter { + constructor(private client: PoolClient) { } + + async query(text: string, params?: unknown[]) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const result = await this.client.query(text, params as any[]); + return { + rows: result.rows, + rowCount: result.rowCount ?? undefined + }; + } + + async transaction(callback: (client: DBAdapter) => Promise): Promise { + // Nested transactions not explicitly supported, just pass through + return callback(this); + } +} + +export class SqliteAdapter implements DBAdapter { + private db: Database.Database; + + constructor(filePath: string) { + // Ensure directory exists + const dir = path.dirname(filePath); + if (!fs.existsSync(dir)) { + try { + fs.mkdirSync(dir, { recursive: true }); + console.log(`Created directory for SQLite: ${dir}`); + } catch (e) { + console.error(`Failed to create directory ${dir}:`, e); + } + } + + this.db = new Database(filePath); + // Enable WAL mode for better concurrency + this.db.pragma('journal_mode = WAL'); + } + + async query(text: string, params?: unknown[]) { + // simple heuristic to convert Postgres $n params to SQLite ? + // This assumes we aren't using $n inside string literals. + const convertedSql = text.replace(/\$\d+/g, "?"); + + try { + const stmt = this.db.prepare(convertedSql); + const safeParams = params || []; + + const lowerSql = convertedSql.trim().toLowerCase(); + + // If it's a SELECT or RETURNING, we use .all() or .get() + if (lowerSql.startsWith("select") || /returning\s+/.test(lowerSql)) { + const rows = stmt.all(...safeParams); + return { rows, rowCount: rows.length }; + } else { + // INSERT/UPDATE/DELETE with no RETURNING + const info = stmt.run(...safeParams); + return { rows: [], rowCount: info.changes }; + } + } catch (error) { + console.error("SQLite Query Error:", error); + throw error; + } + } + + async transaction(callback: (client: DBAdapter) => Promise): Promise { + // Manual transaction management + // better-sqlite3 is synchronous, but we simulate async interface + this.db.exec("BEGIN"); + try { + const result = await callback(this); + this.db.exec("COMMIT"); + return result; + } catch (e) { + this.db.exec("ROLLBACK"); + throw e; + } + } +} diff --git a/src/lib/server/db.ts b/src/lib/server/db.ts new file mode 100644 index 0000000..d078edf --- /dev/null +++ b/src/lib/server/db.ts @@ -0,0 +1,45 @@ +import { Pool } from 'pg'; +import path from 'path'; +import { DBAdapter, PostgresAdapter, SqliteAdapter } from '@/lib/server/db-adapter'; +import { isAuthEnabled } from '@/lib/server/auth-config'; + +// Singleton instance +let dbInstance: DBAdapter | null = null; + +class NoOpAdapter implements DBAdapter { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async query(text: string, params?: unknown[]) { + return { rows: [], rowCount: 0 }; + } + + async transaction(callback: (client: DBAdapter) => Promise): Promise { + return callback(this); + } +} + +export function getDB(): DBAdapter { + if (dbInstance) return dbInstance; + + // Avoid creating database connection/file if auth is disabled + if (!isAuthEnabled()) { + dbInstance = new NoOpAdapter(); + return dbInstance; + } + + if (process.env.POSTGRES_URL) { + const pool = new Pool({ + connectionString: process.env.POSTGRES_URL, + ssl: process.env.NODE_ENV === "production" ? { rejectUnauthorized: false } : false, + }); + dbInstance = new PostgresAdapter(pool); + } else { + // Fallback to SQLite + const dbPath = path.join(process.cwd(), 'docstore', 'sqlite3.db'); + console.log(`Using SQLite database at ${dbPath}`); + dbInstance = new SqliteAdapter(dbPath); + } + + return dbInstance!; +} + +export const db = getDB(); diff --git a/src/lib/server/rate-limiter.ts b/src/lib/server/rate-limiter.ts new file mode 100644 index 0000000..4876cb8 --- /dev/null +++ b/src/lib/server/rate-limiter.ts @@ -0,0 +1,230 @@ +import { db } from '@/lib/server/db'; +import { isAuthEnabled } from '@/lib/server/auth-config'; + +// Rate limits configuration - character counts per day +export const RATE_LIMITS = { + ANONYMOUS: 250_000, // 250K characters per day for anonymous users + AUTHENTICATED: 1_000_000 // 1M characters per day for authenticated users +} as const; + +// Initialize rate limiting table +export async function initializeRateLimitTable() { + // Use transaction to ensure safe initialization + await db.transaction(async (client) => { + // Check if table exists first to avoid errors on some DBs + // Simple create table if not exists + await client.query(` + CREATE TABLE IF NOT EXISTS user_tts_chars ( + user_id VARCHAR(255) NOT NULL, + date DATE NOT NULL, + char_count BIGINT DEFAULT 0, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (user_id, date) + ) + `); + + // Create index for faster queries + await client.query(` + CREATE INDEX IF NOT EXISTS idx_user_tts_chars_date ON user_tts_chars(date) + `); + }); +} + +export interface RateLimitResult { + allowed: boolean; + currentCount: number; + limit: number; + resetTime: Date; + remainingChars: number; +} + +interface DBCharCountRow { + char_count: string | number; +} + +export interface UserInfo { + id: string; + isAnonymous?: boolean; + isPro?: boolean; +} + +export class RateLimiter { + constructor() { } + + /** + * Check if a user can use TTS and increment their char count if allowed + * @param charCount - Number of characters to add + */ + async checkAndIncrementLimit(user: UserInfo, charCount: number): Promise { + // If auth is not enabled, always allow + if (!isAuthEnabled()) { + return { + allowed: true, + currentCount: 0, + limit: Infinity, + resetTime: this.getResetTime(), + remainingChars: Infinity + }; + } + + await initializeRateLimitTable(); + + return db.transaction(async (client) => { + const today = new Date().toISOString().split('T')[0]; // YYYY-MM-DD format + const limit = user.isAnonymous ? RATE_LIMITS.ANONYMOUS : RATE_LIMITS.AUTHENTICATED; + + // Get or create today's record for this user + // Postgres supports RETURNING, but SQLite behavior can vary with ON CONFLICT + // We'll use a standard upsert pattern compatible with both via the adapter logic or simple queries + + // First, ensure record exists + // SQLite/Postgres compatible UPSERT + await client.query(` + INSERT INTO user_tts_chars (user_id, date, char_count) + VALUES ($1, $2, 0) + ON CONFLICT (user_id, date) + DO UPDATE SET updated_at = CURRENT_TIMESTAMP + `, [user.id, today]); + + // Get current count + const result = await client.query(` + SELECT char_count FROM user_tts_chars + WHERE user_id = $1 AND date = $2 + `, [user.id, today]); + + const currentCount = parseInt(((result.rows[0] as unknown) as DBCharCountRow)?.char_count?.toString() || '0', 10); + + // Check if adding these chars would exceed the limit + if (currentCount + charCount > limit) { + return { + allowed: false, + currentCount, + limit, + resetTime: this.getResetTime(), + remainingChars: Math.max(0, limit - currentCount) + }; + } + + // Increment the count + await client.query(` + UPDATE user_tts_chars + SET char_count = char_count + $3, updated_at = CURRENT_TIMESTAMP + WHERE user_id = $1 AND date = $2 + `, [user.id, today, charCount]); + + const newCount = currentCount + charCount; + + return { + allowed: true, + currentCount: newCount, + limit, + resetTime: this.getResetTime(), + remainingChars: Math.max(0, limit - newCount) + }; + }); + } + + /** + * Get current usage for a user without incrementing + */ + async getCurrentUsage(user: UserInfo): Promise { + // If auth is not enabled, return unlimited + if (!isAuthEnabled()) { + return { + allowed: true, + currentCount: 0, + limit: Infinity, + resetTime: this.getResetTime(), + remainingChars: Infinity + }; + } + + await initializeRateLimitTable(); + + const today = new Date().toISOString().split('T')[0]; + const limit = user.isAnonymous ? RATE_LIMITS.ANONYMOUS : RATE_LIMITS.AUTHENTICATED; + + const result = await db.query( + 'SELECT char_count FROM user_tts_chars WHERE user_id = $1 AND date = $2', + [user.id, today] + ); + + const currentCount = result.rows.length > 0 ? parseInt(((result.rows[0] as unknown) as DBCharCountRow).char_count.toString(), 10) : 0; + + return { + allowed: currentCount < limit, + currentCount, + limit, + resetTime: this.getResetTime(), + remainingChars: Math.max(0, limit - currentCount) + }; + } + + /** + * Transfer char counts when anonymous user creates an account + */ + async transferAnonymousUsage(anonymousUserId: string, authenticatedUserId: string): Promise { + if (!isAuthEnabled()) return; + + await initializeRateLimitTable(); + + await db.transaction(async (client) => { + const today = new Date().toISOString().split('T')[0]; + + // Get anonymous user's current count + const anonymousResult = await client.query( + 'SELECT char_count FROM user_tts_chars WHERE user_id = $1 AND date = $2', + [anonymousUserId, today] + ); + + if (anonymousResult.rows.length > 0) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const anonymousCount = parseInt((anonymousResult.rows[0] as any).char_count, 10); + + // Update or create record for authenticated user + await client.query(` + INSERT INTO user_tts_chars (user_id, date, char_count) + VALUES ($1, $2, $3) + ON CONFLICT (user_id, date) + DO UPDATE SET + char_count = CASE + WHEN user_tts_chars.char_count > $3 THEN user_tts_chars.char_count + ELSE $3 + END, + updated_at = CURRENT_TIMESTAMP + `, [authenticatedUserId, today, anonymousCount]); + + // Remove anonymous user's record + await client.query( + 'DELETE FROM user_tts_chars WHERE user_id = $1 AND date = $2', + [anonymousUserId, today] + ); + } + }); + } + + /** + * Clean up old records (optional maintenance) + */ + async cleanupOldRecords(daysToKeep: number = 30): Promise { + const cutoffDate = new Date(); + cutoffDate.setDate(cutoffDate.getDate() - daysToKeep); + const cutoffDateStr = cutoffDate.toISOString().split('T')[0]; + + await db.query( + 'DELETE FROM user_tts_chars WHERE date < $1', + [cutoffDateStr] + ); + } + + private getResetTime(): Date { + const tomorrow = new Date(); + tomorrow.setDate(tomorrow.getDate() + 1); + tomorrow.setHours(0, 0, 0, 0); // Start of next day + return tomorrow; + } +} + +// Export singleton instance +export const rateLimiter = new RateLimiter(); \ No newline at end of file diff --git a/src/lib/session-utils.ts b/src/lib/session-utils.ts new file mode 100644 index 0000000..4536fae --- /dev/null +++ b/src/lib/session-utils.ts @@ -0,0 +1,26 @@ +// Session utilities using Dexie for persistence +// Used by auth components for sign-out state tracking + +import { updateAppConfig, getAppConfig } from '@/lib/dexie'; + +/** + * Check if user was explicitly signed out (for showing appropriate message on signin page) + */ +export async function wasSignedOut(): Promise { + const config = await getAppConfig(); + return config?.signedOut ?? false; +} + +/** + * Clear the signed-out flag after displaying the message + */ +export async function clearSignedOut(): Promise { + await updateAppConfig({ signedOut: false }); +} + +/** + * Mark that the user explicitly signed out + */ +export async function markSignedOut(): Promise { + await updateAppConfig({ signedOut: true }); +} diff --git a/src/types/config.ts b/src/types/config.ts index 92b4af3..6a39db3 100644 --- a/src/types/config.ts +++ b/src/types/config.ts @@ -30,6 +30,8 @@ export interface AppConfigValues { epubWordHighlightEnabled: boolean; firstVisit: boolean; documentListState: DocumentListState; + signedOut: boolean; + privacyAccepted: boolean; } export const APP_CONFIG_DEFAULTS: AppConfigValues = { @@ -63,6 +65,8 @@ export const APP_CONFIG_DEFAULTS: AppConfigValues = { showHint: true, viewMode: 'grid', }, + signedOut: false, + privacyAccepted: false, }; export interface AppConfigRow extends AppConfigValues { diff --git a/tests/helpers.ts b/tests/helpers.ts index f200a7a..ab1e8bf 100644 --- a/tests/helpers.ts +++ b/tests/helpers.ts @@ -45,7 +45,7 @@ export async function uploadAndDisplay(page: Page, fileName: string) { await uploadFile(page, fileName); const lower = fileName.toLowerCase(); - + if (lower.endsWith('.docx')) { await expect(page.getByText('Converting DOCX to PDF...')).toBeVisible(); const pdfName = fileName.replace(/\.docx$/i, '.pdf'); @@ -94,7 +94,7 @@ export async function playTTSAndWaitForASecond(page: Page, fileName: string) { export async function pauseTTSAndVerify(page: Page) { // Click pause to stop playback await page.getByRole('button', { name: 'Pause' }).click(); - + // Check for play button to be visible await expect(page.getByRole('button', { name: 'Play' })).toBeVisible({ timeout: 10000 }); } @@ -118,6 +118,8 @@ export async function setupTest(page: Page) { // Click the "done" button to dismiss the welcome message await page.getByRole('button', { name: 'Save' }).click(); + + await page.getByRole('button', { name: 'I Understand' }).click(); } @@ -303,8 +305,8 @@ export async function expectMediaState(page: Page, state: 'playing' | 'paused') // Consider playing if not paused AND time has advanced at least a tiny amount if (!audio.paused && curr > 0 && curr > last) return true; } else { - // paused target - if (audio.paused) return true; + // paused target + if (audio.paused) return true; } } return false;