diff --git a/.env.example b/.env.example index 8805438..4ec9f43 100644 --- a/.env.example +++ b/.env.example @@ -1,19 +1,40 @@ -# Docker Registry Configuration -DOCKER_REGISTRY=ghcr.io -DOCKER_IMAGE=arunavo4/gitea-mirror -DOCKER_TAG=latest +# Gitea Mirror Configuration +# Copy this to .env and update with your values + +# =========================================== +# CORE CONFIGURATION +# =========================================== # Application Configuration NODE_ENV=production HOST=0.0.0.0 PORT=4321 + +# Database Configuration +# For self-hosted, SQLite is used by default DATABASE_URL=sqlite://data/gitea-mirror.db # Security -JWT_SECRET=change-this-to-a-secure-random-string-in-production +# Generate with: openssl rand -base64 32 +BETTER_AUTH_SECRET=change-this-to-a-secure-random-string-in-production +BETTER_AUTH_URL=http://localhost:4321 +# ENCRYPTION_SECRET=optional-encryption-key-for-token-encryption # Generate with: openssl rand -base64 48 -# Optional GitHub/Gitea Mirror Configuration (for docker-compose, can also be set via web UI) -# Uncomment and set as needed. These are passed as environment variables to the container. +# =========================================== +# DOCKER CONFIGURATION (Optional) +# =========================================== + +# Docker Registry Configuration +DOCKER_REGISTRY=ghcr.io +DOCKER_IMAGE=arunavo4/gitea-mirror +DOCKER_TAG=latest + +# =========================================== +# MIRROR CONFIGURATION (Optional) +# Can also be configured via web UI +# =========================================== + +# GitHub Configuration # GITHUB_USERNAME=your-github-username # GITHUB_TOKEN=your-github-personal-access-token # SKIP_FORKS=false @@ -25,6 +46,8 @@ JWT_SECRET=change-this-to-a-secure-random-string-in-production # PRESERVE_ORG_STRUCTURE=false # ONLY_MIRROR_ORGS=false # SKIP_STARRED_ISSUES=false + +# Gitea Configuration # GITEA_URL=http://gitea:3000 # GITEA_TOKEN=your-local-gitea-token # GITEA_USERNAME=your-local-gitea-username @@ -32,15 +55,27 @@ JWT_SECRET=change-this-to-a-secure-random-string-in-production # GITEA_ORG_VISIBILITY=public # DELAY=3600 -# Optional Database Cleanup Configuration (configured via web UI) -# These environment variables are optional and only used as defaults -# Users can configure cleanup settings through the web interface +# =========================================== +# OPTIONAL FEATURES +# =========================================== + +# Database Cleanup Configuration # CLEANUP_ENABLED=false # CLEANUP_RETENTION_DAYS=7 -# Optional TLS/SSL Configuration -# Option 1: Mount custom CA certificates in ./certs directory as .crt files -# The container will automatically combine them into a CA bundle -# Option 2: Mount your system CA bundle at /etc/ssl/certs/ca-certificates.crt -# See docker-compose.yml for volume mount examples -# GITEA_SKIP_TLS_VERIFY=false # WARNING: Only use for testing, disables TLS verification +# TLS/SSL Configuration +# GITEA_SKIP_TLS_VERIFY=false # WARNING: Only use for testing + +# =========================================== +# AUTHENTICATION CONFIGURATION +# =========================================== + +# Header Authentication (for Reverse Proxy SSO) +# Enable automatic authentication via reverse proxy headers +# HEADER_AUTH_ENABLED=false +# HEADER_AUTH_USER_HEADER=X-Authentik-Username +# HEADER_AUTH_EMAIL_HEADER=X-Authentik-Email +# HEADER_AUTH_NAME_HEADER=X-Authentik-Name +# HEADER_AUTH_AUTO_PROVISION=false +# HEADER_AUTH_ALLOWED_DOMAINS=example.com,company.org + diff --git a/.gitignore b/.gitignore index e644028..8fe8431 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,4 @@ certs/*.crt certs/*.pem certs/*.cer !certs/README.md + diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..cac2a3e --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,182 @@ +# Contributing to Gitea Mirror + +Thank you for your interest in contributing to Gitea Mirror! This document provides guidelines and instructions for contributing to the open-source version of the project. + +## ๐ŸŽฏ Project Overview + +Gitea Mirror is an open-source, self-hosted solution for mirroring GitHub repositories to Gitea instances. This guide provides everything you need to know about contributing to the project. + +## ๐Ÿš€ Getting Started + +1. Fork the repository +2. Clone your fork: + ```bash + git clone https://github.com/yourusername/gitea-mirror.git + cd gitea-mirror + ``` + +3. Install dependencies: + ```bash + bun install + ``` + +4. Set up your environment: + ```bash + cp .env.example .env + # Edit .env with your configuration + ``` + +5. Start development: + ```bash + bun run dev + ``` + +## ๐Ÿ›  Development Workflow + +### Running the Application + +```bash +# Development mode +bun run dev + +# Build for production +bun run build + +# Run tests +bun test +``` + +### Database Management + +```bash +# Initialize database +bun run init-db + +# Reset database +bun run cleanup-db && bun run init-db +``` + +## ๐Ÿ“ Code Guidelines + +### General Principles + +1. **Keep it Simple**: Gitea Mirror should remain easy to self-host +2. **Focus on Core Features**: Prioritize repository mirroring and synchronization +3. **Database**: Use SQLite for simplicity and portability +4. **Dependencies**: Minimize external dependencies for easier deployment + +### Code Style + +- Use TypeScript for all new code +- Follow the existing code formatting (Prettier is configured) +- Write meaningful commit messages +- Add tests for new features + +### Scope of Contributions + +This project focuses on personal/small team use cases. Please keep contributions aligned with: +- Core mirroring functionality +- Self-hosted simplicity +- Minimal external dependencies +- SQLite as the database +- Single-instance deployments + +## ๐Ÿ› Reporting Issues + +1. Check existing issues first +2. Use issue templates when available +3. Provide clear reproduction steps +4. Include relevant logs and screenshots + +## ๐ŸŽฏ Pull Request Process + +1. Create a feature branch: + ```bash + git checkout -b feature/your-feature-name + ``` + +2. Make your changes following the code guidelines + +3. Test your changes: + ```bash + # Run tests + bun test + + # Build and check + bun run build:oss + ``` + +4. Commit your changes: + ```bash + git commit -m "feat: add new feature" + ``` + +5. Push to your fork and create a Pull Request + +### PR Requirements + +- Clear description of changes +- Tests for new functionality +- Documentation updates if needed +- No breaking changes without discussion +- Passes all CI checks + +## ๐Ÿ— Architecture Overview + +``` +src/ +โ”œโ”€โ”€ components/ # React components +โ”œโ”€โ”€ lib/ # Core utilities +โ”‚ โ”œโ”€โ”€ db/ # Database queries (SQLite only) +โ”‚ โ”œโ”€โ”€ github/ # GitHub API integration +โ”‚ โ”œโ”€โ”€ gitea/ # Gitea API integration +โ”‚ โ””โ”€โ”€ utils/ # Helper functions +โ”œโ”€โ”€ pages/ # Astro pages +โ”‚ โ””โ”€โ”€ api/ # API endpoints +โ””โ”€โ”€ types/ # TypeScript types +``` + +## ๐Ÿงช Testing + +```bash +# Run all tests +bun test + +# Run tests in watch mode +bun test:watch + +# Run with coverage +bun test:coverage +``` + +## ๐Ÿ“š Documentation + +- Update README.md for user-facing changes +- Add JSDoc comments for new functions +- Update .env.example for new environment variables + +## ๐Ÿ’ก Feature Requests + +We welcome feature requests! When proposing new features, please consider: +- Does it enhance the core mirroring functionality? +- Will it benefit self-hosted users? +- Can it be implemented without complex external dependencies? +- Does it maintain the project's simplicity? + +## ๐Ÿค Community + +- Be respectful and constructive +- Help others in issues and discussions +- Share your use cases and feedback + +## ๐Ÿ“„ License + +By contributing, you agree that your contributions will be licensed under the same license as the project (MIT). + +## Questions? + +Feel free to open an issue for any questions about contributing! + +--- + +Thank you for helping make Gitea Mirror better! ๐ŸŽ‰ \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index d3cefe2..c661c2b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -31,6 +31,7 @@ COPY --from=builder /app/dist ./dist COPY --from=builder /app/package.json ./package.json COPY --from=builder /app/docker-entrypoint.sh ./docker-entrypoint.sh COPY --from=builder /app/scripts ./scripts +COPY --from=builder /app/drizzle ./drizzle ENV NODE_ENV=production ENV HOST=0.0.0.0 diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md new file mode 100644 index 0000000..4654b00 --- /dev/null +++ b/MIGRATION_GUIDE.md @@ -0,0 +1,248 @@ +# Migration Guide + +This guide covers database migrations and version upgrades for Gitea Mirror. + +## Version 3.0 Migration Guide + +### Overview of v3 Changes + +Version 3.0 introduces significant security improvements and authentication changes: +- **Token Encryption**: All GitHub and Gitea tokens are now encrypted in the database +- **Better Auth**: Complete authentication system overhaul with session-based auth +- **SSO/OIDC Support**: Enterprise authentication options +- **Enhanced Security**: Improved error handling and security practices + +### Breaking Changes in v3 + +#### 1. Authentication System Overhaul +- Users now log in with **email** instead of username +- Session-based authentication replaces JWT tokens +- New auth endpoints: `/api/auth/[...all]` instead of `/api/auth/login` +- Password reset may be required for existing users + +#### 2. Token Encryption +- All stored GitHub and Gitea tokens are encrypted using AES-256-GCM +- Requires encryption secret configuration +- Existing unencrypted tokens must be migrated + +#### 3. Environment Variables +**Required changes:** +- `JWT_SECRET` โ†’ `BETTER_AUTH_SECRET` (backward compatible) +- New: `BETTER_AUTH_URL` (required) +- New: `ENCRYPTION_SECRET` (recommended) + +#### 4. Database Schema Updates +New tables added: +- `sessions` - User session management +- `accounts` - Authentication accounts +- `verification_tokens` - Email verification +- `oauth_applications` - OAuth app registrations +- `sso_providers` - SSO configuration + +### Migration Steps from v2 to v3 + +**โš ๏ธ IMPORTANT: Backup your database before upgrading!** + +```bash +cp data/gitea-mirror.db data/gitea-mirror.db.backup +``` + +#### Automated Migration (Docker Compose) + +For Docker Compose users, v3 migration is **fully automated**: + +1. **Update your docker-compose.yml** to use v3: +```yaml +services: + gitea-mirror: + image: ghcr.io/raylabshq/gitea-mirror:v3 +``` + +2. **Pull and restart the container**: +```bash +docker compose pull +docker compose down +docker compose up -d +``` + +**That's it!** The container will automatically: +- โœ… Generate BETTER_AUTH_SECRET (from existing JWT_SECRET if available) +- โœ… Generate ENCRYPTION_SECRET for token encryption +- โœ… Create Better Auth database tables +- โœ… Migrate existing users to Better Auth system +- โœ… Encrypt all stored GitHub/Gitea tokens +- โœ… Apply all necessary database migrations + +#### Manual Migration (Non-Docker) + +#### Step 1: Update Environment Variables +Add to your `.env` file: +```bash +# Set your application URL (required) +BETTER_AUTH_URL=http://localhost:4321 # or your production URL + +# Optional: These will be auto-generated if not provided +# BETTER_AUTH_SECRET=your-existing-jwt-secret # Will use existing JWT_SECRET +# ENCRYPTION_SECRET=your-48-character-secret # Will be auto-generated +``` + +#### Step 2: Stop the Application +```bash +# Stop your running instance +pkill -f "bun run start" # or your process manager command +``` + +#### Step 3: Update to v3 +```bash +# Pull latest changes +git pull origin v3 + +# Install dependencies +bun install +``` + +#### Step 4: Run Migrations +```bash +# Option 1: Automatic migration on startup +bun run build +bun run start # Migrations run automatically + +# Option 2: Manual migration +bun run migrate:better-auth # Migrate users to Better Auth +bun run migrate:encrypt-tokens # Encrypt stored tokens +``` + +### Post-Migration Tasks + +1. **All users must log in again** - Sessions are invalidated +2. **Users log in with email** - Not username anymore +3. **Check token encryption** - Verify GitHub/Gitea connections still work +4. **Update API integrations** - Switch to new auth endpoints + +### Troubleshooting v3 Migration + +#### Users Can't Log In +- Ensure they're using email, not username +- They may need to reset password if migration failed +- Check Better Auth migration logs + +#### Token Decryption Errors +- Verify ENCRYPTION_SECRET is set correctly +- Re-run token encryption migration +- Users may need to re-enter tokens + +#### Database Errors +- Ensure all migrations completed +- Check disk space for new tables +- Review migration logs in console + +### Rollback Procedure +If migration fails: +```bash +# Stop application +pkill -f "bun run start" + +# Restore database backup +cp data/gitea-mirror.db.backup data/gitea-mirror.db + +# Checkout previous version +git checkout v2.22.0 + +# Restart with old version +bun run start +``` + +--- + +## Drizzle Kit Migration Guide + +This project uses Drizzle Kit for database migrations, providing better schema management and migration tracking. + +## Overview + +- **Database**: SQLite (with preparation for future PostgreSQL migration) +- **ORM**: Drizzle ORM with Drizzle Kit for migrations +- **Schema Location**: `/src/lib/db/schema.ts` +- **Migrations Folder**: `/drizzle` +- **Configuration**: `/drizzle.config.ts` + +## Available Commands + +### Database Management +- `bun run init-db` - Initialize database with all migrations +- `bun run check-db` - Check database status and recent migrations +- `bun run reset-users` - Remove all users and related data +- `bun run cleanup-db` - Remove database files + +### Drizzle Kit Commands +- `bun run db:generate` - Generate new migration files from schema changes +- `bun run db:migrate` - Apply pending migrations to database +- `bun run db:push` - Push schema changes directly (development) +- `bun run db:pull` - Pull schema from database +- `bun run db:check` - Check for migration issues +- `bun run db:studio` - Open Drizzle Studio for database browsing + +## Making Schema Changes + +1. **Update Schema**: Edit `/src/lib/db/schema.ts` +2. **Generate Migration**: Run `bun run db:generate` +3. **Review Migration**: Check the generated SQL in `/drizzle` folder +4. **Apply Migration**: Run `bun run db:migrate` or restart the application + +## Migration Process + +The application automatically runs migrations on startup: +- Checks for pending migrations +- Creates migrations table if needed +- Applies all pending migrations in order +- Tracks migration history + +## Schema Organization + +### Tables +- `users` - User authentication and accounts +- `configs` - GitHub/Gitea configurations +- `repositories` - Repository mirror tracking +- `organizations` - GitHub organizations +- `mirror_jobs` - Job tracking with resilience +- `events` - Real-time event notifications + +### Indexes +All performance-critical indexes are automatically created: +- User lookups +- Repository status queries +- Organization filtering +- Job tracking +- Event channels + +## Future PostgreSQL Migration + +The setup is designed for easy PostgreSQL migration: + +1. Update `drizzle.config.ts`: +```typescript +export default defineConfig({ + dialect: "postgresql", + schema: "./src/lib/db/schema.ts", + out: "./drizzle", + dbCredentials: { + connectionString: process.env.DATABASE_URL, + }, +}); +``` + +2. Update connection in `/src/lib/db/index.ts` +3. Generate new migrations: `bun run db:generate` +4. Apply to PostgreSQL: `bun run db:migrate` + +## Troubleshooting + +### Migration Errors +- Check `/drizzle` folder for migration files +- Verify database permissions +- Review migration SQL for conflicts + +### Schema Conflicts +- Use `bun run db:check` to identify issues +- Review generated migrations before applying +- Keep schema.ts as single source of truth \ No newline at end of file diff --git a/README.md b/README.md index 9aa68d6..82eac8a 100644 --- a/README.md +++ b/README.md @@ -170,6 +170,12 @@ bun run dev - Override individual repository destinations in the table view - Starred repositories automatically go to a dedicated organization +## Troubleshooting + +### Reverse Proxy Configuration + +If using a reverse proxy (e.g., nginx proxy manager) and experiencing issues with JavaScript files not loading properly, try enabling HTTP/2 support in your proxy configuration. While not required by the application, some proxy configurations may have better compatibility with HTTP/2 enabled. See [issue #43](https://github.com/RayLabsHQ/gitea-mirror/issues/43) for reference. + ## Development ```bash @@ -193,6 +199,90 @@ bun run build - **APIs**: GitHub (Octokit), Gitea REST API - **Auth**: JWT tokens with bcryptjs password hashing +## Security + +### Token Encryption +- All GitHub and Gitea API tokens are encrypted at rest using AES-256-GCM +- Encryption is automatic and transparent to users +- Set `ENCRYPTION_SECRET` environment variable for production deployments +- Falls back to `BETTER_AUTH_SECRET` or `JWT_SECRET` if not set + +### Password Security +- User passwords are hashed using bcrypt (via Better Auth) +- Never stored in plaintext +- Secure session management with JWT tokens + +### Migration +If upgrading from a version without token encryption: +```bash +bun run migrate:encrypt-tokens +``` + +## Authentication + +Gitea Mirror supports multiple authentication methods. **Email/password authentication is the default and always enabled.** + +### 1. Email & Password (Default) +The standard authentication method. First user to sign up becomes the admin. + +### 2. Single Sign-On (SSO) with OIDC +Enable users to sign in with external identity providers like Google, Azure AD, Okta, Authentik, or any OIDC-compliant service. + +**Configuration:** +1. Navigate to Settings โ†’ Authentication & SSO +2. Click "Add Provider" +3. Enter your OIDC provider details: + - Issuer URL (e.g., `https://accounts.google.com`) + - Client ID and Secret from your provider + - Use the "Discover" button to auto-fill endpoints + +**Redirect URL for your provider:** +``` +https://your-domain.com/api/auth/sso/callback/{provider-id} +``` + +### 3. Header Authentication (Reverse Proxy) +Perfect for automatic authentication when using reverse proxies like Authentik, Authelia, or Traefik Forward Auth. + +**Environment Variables:** +```bash +# Enable header authentication +HEADER_AUTH_ENABLED=true + +# Header names (customize based on your proxy) +HEADER_AUTH_USER_HEADER=X-Authentik-Username +HEADER_AUTH_EMAIL_HEADER=X-Authentik-Email +HEADER_AUTH_NAME_HEADER=X-Authentik-Name + +# Auto-provision new users +HEADER_AUTH_AUTO_PROVISION=true + +# Restrict to specific email domains (optional) +HEADER_AUTH_ALLOWED_DOMAINS=example.com,company.org +``` + +**How it works:** +- Users authenticated by your reverse proxy are automatically logged in +- No additional login step required +- New users can be auto-provisioned if enabled +- Falls back to regular authentication if headers are missing + +**Example Authentik Configuration:** +```nginx +# In your reverse proxy configuration +proxy_set_header X-Authentik-Username $authentik_username; +proxy_set_header X-Authentik-Email $authentik_email; +proxy_set_header X-Authentik-Name $authentik_name; +``` + +### 4. OAuth Applications (Act as Identity Provider) +Gitea Mirror can also act as an OIDC provider for other applications. Register OAuth applications in Settings โ†’ Authentication & SSO โ†’ OAuth Applications tab. + +**Use cases:** +- Allow other services to authenticate using Gitea Mirror accounts +- Create service-to-service authentication +- Build integrations with your Gitea Mirror instance + ## Contributing Contributions are welcome! Please read our [Contributing Guidelines](CONTRIBUTING.md) for details on our code of conduct and the process for submitting pull requests. @@ -201,6 +291,16 @@ Contributions are welcome! Please read our [Contributing Guidelines](CONTRIBUTIN GNU General Public License v3.0 - see [LICENSE](LICENSE) file for details. +## Star History + + + + + + Star History Chart + + + ## Support - ๐Ÿ“– [Documentation](https://github.com/RayLabsHQ/gitea-mirror/tree/main/docs) diff --git a/bun.lock b/bun.lock index 3feb12d..68fe96c 100644 --- a/bun.lock +++ b/bun.lock @@ -33,6 +33,7 @@ "@types/react-dom": "^19.1.6", "astro": "5.11.0", "bcryptjs": "^3.0.2", + "better-auth": "^1.2.12", "canvas-confetti": "^1.9.3", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -58,9 +59,11 @@ "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.3.0", "@types/bcryptjs": "^3.0.0", + "@types/bun": "^1.2.18", "@types/jsonwebtoken": "^9.0.10", "@types/uuid": "^10.0.0", "@vitejs/plugin-react": "^4.6.0", + "drizzle-kit": "^0.31.4", "jsdom": "^26.1.0", "tsx": "^4.20.3", "vitest": "^3.2.4", @@ -134,6 +137,10 @@ "@babel/types": ["@babel/types@7.27.3", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-Y1GkI4ktrtvmawoSq+4FCVHNryea6uR+qUQy0AGxLSsjCX0nVmkYQMBLHDkXZuo5hGx7eYdnIaslsdBFm7zbUw=="], + "@better-auth/utils": ["@better-auth/utils@0.2.5", "", { "dependencies": { "typescript": "^5.8.2", "uncrypto": "^0.1.3" } }, "sha512-uI2+/8h/zVsH8RrYdG8eUErbuGBk16rZKQfz8CjxQOyCE6v7BqFYEbFwvOkvl1KbUdxhqOnXp78+uE5h8qVEgQ=="], + + "@better-fetch/fetch": ["@better-fetch/fetch@1.1.18", "", {}, "sha512-rEFOE1MYIsBmoMJtQbl32PGHHXuG2hDxvEd7rUHE0vCBoFQVSDqaVs9hkZEtHCxRoY+CljXKFCOuJ8uxqw1LcA=="], + "@capsizecss/unpack": ["@capsizecss/unpack@2.4.0", "", { "dependencies": { "blob-to-buffer": "^1.2.8", "cross-fetch": "^3.0.4", "fontkit": "^2.0.2" } }, "sha512-GrSU71meACqcmIUxPYOJvGKF0yryjN/L1aCuE9DViCTJI7bfkjgYDPD1zbNDcINJwSSP6UaBZY9GAbYDO7re0Q=="], "@csstools/color-helpers": ["@csstools/color-helpers@5.0.2", "", {}, "sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA=="], @@ -146,6 +153,8 @@ "@csstools/css-tokenizer": ["@csstools/css-tokenizer@3.0.4", "", {}, "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw=="], + "@drizzle-team/brocli": ["@drizzle-team/brocli@0.10.2", "", {}, "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w=="], + "@emmetio/abbreviation": ["@emmetio/abbreviation@2.3.3", "", { "dependencies": { "@emmetio/scanner": "^1.0.4" } }, "sha512-mgv58UrU3rh4YgbE/TzgLQwJ3pFsHHhCLqY20aJq+9comytTXUDNGG/SMtSeMJdkpxgXSXunBGLD8Boka3JyVA=="], "@emmetio/css-abbreviation": ["@emmetio/css-abbreviation@2.1.8", "", { "dependencies": { "@emmetio/scanner": "^1.0.4" } }, "sha512-s9yjhJ6saOO/uk1V74eifykk2CBYi01STTK3WlXWGOepyKa23ymJ053+DNQjpFcy1ingpaO7AxCcwLvHFY9tuw=="], @@ -162,6 +171,10 @@ "@emnapi/runtime": ["@emnapi/runtime@1.4.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ=="], + "@esbuild-kit/core-utils": ["@esbuild-kit/core-utils@3.3.2", "", { "dependencies": { "esbuild": "~0.18.20", "source-map-support": "^0.5.21" } }, "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ=="], + + "@esbuild-kit/esm-loader": ["@esbuild-kit/esm-loader@2.6.5", "", { "dependencies": { "@esbuild-kit/core-utils": "^3.3.2", "get-tsconfig": "^4.7.0" } }, "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA=="], + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA=="], "@esbuild/android-arm": ["@esbuild/android-arm@0.25.5", "", { "os": "android", "cpu": "arm" }, "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA=="], @@ -220,6 +233,8 @@ "@floating-ui/utils": ["@floating-ui/utils@0.2.9", "", {}, "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg=="], + "@hexagon/base64": ["@hexagon/base64@1.1.28", "", {}, "sha512-lhqDEAvWixy3bZ+UOYbPwUbBkwBq5C1LAJ/xPC8Oi+lL54oyakv/npbA0aU2hgCsx/1NUd4IBvV03+aUBWxerw=="], + "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.0.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ=="], "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.0.4" }, "os": "darwin", "cpu": "x64" }, "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q=="], @@ -270,8 +285,14 @@ "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="], + "@levischuck/tiny-cbor": ["@levischuck/tiny-cbor@0.2.11", "", {}, "sha512-llBRm4dT4Z89aRsm6u2oEZ8tfwL/2l6BwpZ7JcyieouniDECM5AqNgr/y08zalEIvW3RSK4upYyybDcmjXqAow=="], + "@mdx-js/mdx": ["@mdx-js/mdx@3.1.0", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdx": "^2.0.0", "collapse-white-space": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-util-scope": "^1.0.0", "estree-walker": "^3.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "markdown-extensions": "^2.0.0", "recma-build-jsx": "^1.0.0", "recma-jsx": "^1.0.0", "recma-stringify": "^1.0.0", "rehype-recma": "^1.0.0", "remark-mdx": "^3.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "source-map": "^0.7.0", "unified": "^11.0.0", "unist-util-position-from-estree": "^2.0.0", "unist-util-stringify-position": "^4.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-/QxEhPAvGwbQmy1Px8F899L5Uc2KZ6JtXwlCgJmjSTBedwOZkByYcBG4GceIGPXRDsmfxhHazuS+hlOShRLeDw=="], + "@noble/ciphers": ["@noble/ciphers@0.6.0", "", {}, "sha512-mIbq/R9QXk5/cTfESb1OKtyFnk7oc1Om/8onA1158K9/OZUQFDEVy55jVTato+xmp3XX6F6Qh0zz0Nc1AxAlRQ=="], + + "@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], + "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], @@ -304,6 +325,16 @@ "@oslojs/encoding": ["@oslojs/encoding@1.1.0", "", {}, "sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ=="], + "@peculiar/asn1-android": ["@peculiar/asn1-android@2.3.16", "", { "dependencies": { "@peculiar/asn1-schema": "^2.3.15", "asn1js": "^3.0.5", "tslib": "^2.8.1" } }, "sha512-a1viIv3bIahXNssrOIkXZIlI2ePpZaNmR30d4aBL99mu2rO+mT9D6zBsp7H6eROWGtmwv0Ionp5olJurIo09dw=="], + + "@peculiar/asn1-ecc": ["@peculiar/asn1-ecc@2.3.15", "", { "dependencies": { "@peculiar/asn1-schema": "^2.3.15", "@peculiar/asn1-x509": "^2.3.15", "asn1js": "^3.0.5", "tslib": "^2.8.1" } }, "sha512-/HtR91dvgog7z/WhCVdxZJ/jitJuIu8iTqiyWVgRE9Ac5imt2sT/E4obqIVGKQw7PIy+X6i8lVBoT6wC73XUgA=="], + + "@peculiar/asn1-rsa": ["@peculiar/asn1-rsa@2.3.15", "", { "dependencies": { "@peculiar/asn1-schema": "^2.3.15", "@peculiar/asn1-x509": "^2.3.15", "asn1js": "^3.0.5", "tslib": "^2.8.1" } }, "sha512-p6hsanvPhexRtYSOHihLvUUgrJ8y0FtOM97N5UEpC+VifFYyZa0iZ5cXjTkZoDwxJ/TTJ1IJo3HVTB2JJTpXvg=="], + + "@peculiar/asn1-schema": ["@peculiar/asn1-schema@2.3.15", "", { "dependencies": { "asn1js": "^3.0.5", "pvtsutils": "^1.3.6", "tslib": "^2.8.1" } }, "sha512-QPeD8UA8axQREpgR5UTAfu2mqQmm97oUqahDtNdBcfj3qAnoXzFdQW+aNf/tD2WVXF8Fhmftxoj0eMIT++gX2w=="], + + "@peculiar/asn1-x509": ["@peculiar/asn1-x509@2.3.15", "", { "dependencies": { "@peculiar/asn1-schema": "^2.3.15", "asn1js": "^3.0.5", "pvtsutils": "^1.3.6", "tslib": "^2.8.1" } }, "sha512-0dK5xqTqSLaxv1FHXIcd4Q/BZNuopg+u1l23hT9rOmQ1g4dNtw0g/RnEi+TboB0gOwGtrWn269v27cMgchFIIg=="], + "@radix-ui/number": ["@radix-ui/number@1.1.1", "", {}, "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g=="], "@radix-ui/primitive": ["@radix-ui/primitive@1.1.2", "", {}, "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA=="], @@ -452,6 +483,10 @@ "@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="], + "@simplewebauthn/browser": ["@simplewebauthn/browser@13.1.2", "", {}, "sha512-aZnW0KawAM83fSBUgglP5WofbrLbLyr7CoPqYr66Eppm7zO86YX6rrCjRB3hQKPrL7ATvY4FVXlykZ6w6FwYYw=="], + + "@simplewebauthn/server": ["@simplewebauthn/server@13.1.2", "", { "dependencies": { "@hexagon/base64": "^1.1.27", "@levischuck/tiny-cbor": "^0.2.2", "@peculiar/asn1-android": "^2.3.10", "@peculiar/asn1-ecc": "^2.3.8", "@peculiar/asn1-rsa": "^2.3.8", "@peculiar/asn1-schema": "^2.3.8", "@peculiar/asn1-x509": "^2.3.8" } }, "sha512-VwoDfvLXSCaRiD+xCIuyslU0HLxVggeE5BL06+GbsP2l1fGf5op8e0c3ZtKoi+vSg1q4ikjtAghC23ze2Q3H9g=="], + "@swc/helpers": ["@swc/helpers@0.5.17", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A=="], "@tailwindcss/node": ["@tailwindcss/node@4.1.11", "", { "dependencies": { "@ampproject/remapping": "^2.3.0", "enhanced-resolve": "^5.18.1", "jiti": "^2.4.2", "lightningcss": "1.30.1", "magic-string": "^0.30.17", "source-map-js": "^1.2.1", "tailwindcss": "4.1.11" } }, "sha512-yzhzuGRmv5QyU9qLNg4GTlYI6STedBWRE7NjxP45CsFYYq9taI0zJXZBMqIC/c8fViNLhmrbpSFS57EoxUmD6Q=="], @@ -506,6 +541,8 @@ "@types/bcryptjs": ["@types/bcryptjs@3.0.0", "", { "dependencies": { "bcryptjs": "*" } }, "sha512-WRZOuCuaz8UcZZE4R5HXTco2goQSI2XxjGY3hbM/xDvwmqFWd4ivooImsMx65OKM6CtNKbnZ5YL+YwAwK7c1dg=="], + "@types/bun": ["@types/bun@1.2.18", "", { "dependencies": { "bun-types": "1.2.18" } }, "sha512-Xf6RaWVheyemaThV0kUfaAUvCNokFr+bH8Jxp+tTZfx7dAPA8z9ePnP9S9+Vspzuxxx9JRAXhnyccRj3GyCMdQ=="], + "@types/canvas-confetti": ["@types/canvas-confetti@1.9.0", "", {}, "sha512-aBGj/dULrimR1XDZLtG9JwxX1b4HPRF6CX9Yfwh3NvstZEm1ZL7RBnel4keCPSqs1ANRu1u2Aoz9R+VmtjYuTg=="], "@types/chai": ["@types/chai@5.2.2", "", { "dependencies": { "@types/deep-eql": "*" } }, "sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg=="], @@ -600,6 +637,8 @@ "array-iterate": ["array-iterate@2.0.1", "", {}, "sha512-I1jXZMjAgCMmxT4qxXfPXa6SthSoE8h6gkSI9BGGNv8mP8G/v0blc+qFnZu6K42vTOiuME596QaLO0TP3Lk0xg=="], + "asn1js": ["asn1js@3.0.6", "", { "dependencies": { "pvtsutils": "^1.3.6", "pvutils": "^1.1.3", "tslib": "^2.8.1" } }, "sha512-UOCGPYbl0tv8+006qks/dTgV9ajs97X2p0FAbyS2iyCRrmLSRolDaHdp+v/CLgnzHc3fVB+CwYiUmei7ndFcgA=="], + "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], "astring": ["astring@1.9.0", "", { "bin": { "astring": "bin/astring" } }, "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg=="], @@ -618,6 +657,10 @@ "before-after-hook": ["before-after-hook@4.0.0", "", {}, "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ=="], + "better-auth": ["better-auth@1.2.12", "", { "dependencies": { "@better-auth/utils": "0.2.5", "@better-fetch/fetch": "^1.1.18", "@noble/ciphers": "^0.6.0", "@noble/hashes": "^1.6.1", "@simplewebauthn/browser": "^13.0.0", "@simplewebauthn/server": "^13.0.0", "better-call": "^1.0.8", "defu": "^6.1.4", "jose": "^6.0.11", "kysely": "^0.28.2", "nanostores": "^0.11.3", "zod": "^3.24.1" } }, "sha512-YicCyjQ+lxb7YnnaCewrVOjj3nPVa0xcfrOJK7k5MLMX9Mt9UnJ8GYaVQNHOHLyVxl92qc3C758X1ihqAUzm4w=="], + + "better-call": ["better-call@1.0.12", "", { "dependencies": { "@better-fetch/fetch": "^1.1.4", "rou3": "^0.5.1", "set-cookie-parser": "^2.7.1", "uncrypto": "^0.1.3" } }, "sha512-ssq5OfB9Ungv2M1WVrRnMBomB0qz1VKuhkY2WxjHaLtlsHoSe9EPolj1xf7xf8LY9o3vfk3Rx6rCWI4oVHeBRg=="], + "blob-to-buffer": ["blob-to-buffer@1.2.9", "", {}, "sha512-BF033y5fN6OCofD3vgHmNtwZWRcq9NLyyxyILx9hfMy1sXYy4ojFl765hJ2lP0YaN2fuxPaLO2Vzzoxy0FLFFA=="], "boxen": ["boxen@8.0.1", "", { "dependencies": { "ansi-align": "^3.0.1", "camelcase": "^8.0.0", "chalk": "^5.3.0", "cli-boxes": "^3.0.0", "string-width": "^7.2.0", "type-fest": "^4.21.0", "widest-line": "^5.0.0", "wrap-ansi": "^9.0.0" } }, "sha512-F3PH5k5juxom4xktynS7MoFY+NUWH5LC4CnH11YB8NPew+HLpmBLCybSAEyb2F+4pRXhuhWqFesoQd6DAyc2hw=="], @@ -630,6 +673,10 @@ "buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="], + "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], + + "bun-types": ["bun-types@1.2.18", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-04+Eha5NP7Z0A9YgDAzMk5PHR16ZuLVa83b26kH5+cp1qZW4F6FmAURngE7INf4tKOvCE69vYvDEwoNl1tGiWw=="], + "cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="], "camelcase": ["camelcase@8.0.0", "", {}, "sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA=="], @@ -742,6 +789,8 @@ "dom-accessibility-api": ["dom-accessibility-api@0.6.3", "", {}, "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w=="], + "drizzle-kit": ["drizzle-kit@0.31.4", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.25.4", "esbuild-register": "^3.5.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-tCPWVZWZqWVx2XUsVpJRnH9Mx0ClVOf5YUHerZ5so1OKSlqww4zy1R5ksEdGRcO3tM3zj0PYN6V48TbQCL1RfA=="], + "drizzle-orm": ["drizzle-orm@0.44.2", "", { "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.13", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@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", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-zGAqBzWWkVSFjZpwPOrmCrgO++1kZ5H/rZ4qTGeGOe18iXGVJWf3WPfHOVwFIbmi8kHjfJstC6rJomzGx8g/dQ=="], "dset": ["dset@3.1.4", "", {}, "sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA=="], @@ -770,6 +819,8 @@ "esbuild": ["esbuild@0.25.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.5", "@esbuild/android-arm": "0.25.5", "@esbuild/android-arm64": "0.25.5", "@esbuild/android-x64": "0.25.5", "@esbuild/darwin-arm64": "0.25.5", "@esbuild/darwin-x64": "0.25.5", "@esbuild/freebsd-arm64": "0.25.5", "@esbuild/freebsd-x64": "0.25.5", "@esbuild/linux-arm": "0.25.5", "@esbuild/linux-arm64": "0.25.5", "@esbuild/linux-ia32": "0.25.5", "@esbuild/linux-loong64": "0.25.5", "@esbuild/linux-mips64el": "0.25.5", "@esbuild/linux-ppc64": "0.25.5", "@esbuild/linux-riscv64": "0.25.5", "@esbuild/linux-s390x": "0.25.5", "@esbuild/linux-x64": "0.25.5", "@esbuild/netbsd-arm64": "0.25.5", "@esbuild/netbsd-x64": "0.25.5", "@esbuild/openbsd-arm64": "0.25.5", "@esbuild/openbsd-x64": "0.25.5", "@esbuild/sunos-x64": "0.25.5", "@esbuild/win32-arm64": "0.25.5", "@esbuild/win32-ia32": "0.25.5", "@esbuild/win32-x64": "0.25.5" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ=="], + "esbuild-register": ["esbuild-register@3.6.0", "", { "dependencies": { "debug": "^4.3.4" }, "peerDependencies": { "esbuild": ">=0.12 <1" } }, "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg=="], + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], @@ -926,6 +977,8 @@ "jiti": ["jiti@2.4.2", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A=="], + "jose": ["jose@6.0.11", "", {}, "sha512-QxG7EaliDARm1O1S8BGakqncGT9s25bKL1WSf6/oa17Tkqwi8D2ZNglqCF+DsYF88/rV66Q/Q2mFAy697E1DUg=="], + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], "js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="], @@ -948,6 +1001,8 @@ "kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="], + "kysely": ["kysely@0.28.2", "", {}, "sha512-4YAVLoF0Sf0UTqlhgQMFU9iQECdah7n+13ANkiuVfRvlK+uI0Etbgd7bVP36dKlG+NXWbhGua8vnGt+sdhvT7A=="], + "lightningcss": ["lightningcss@1.30.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-darwin-arm64": "1.30.1", "lightningcss-darwin-x64": "1.30.1", "lightningcss-freebsd-x64": "1.30.1", "lightningcss-linux-arm-gnueabihf": "1.30.1", "lightningcss-linux-arm64-gnu": "1.30.1", "lightningcss-linux-arm64-musl": "1.30.1", "lightningcss-linux-x64-gnu": "1.30.1", "lightningcss-linux-x64-musl": "1.30.1", "lightningcss-win32-arm64-msvc": "1.30.1", "lightningcss-win32-x64-msvc": "1.30.1" } }, "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg=="], "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.30.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ=="], @@ -1134,6 +1189,8 @@ "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + "nanostores": ["nanostores@0.11.4", "", {}, "sha512-k1oiVNN4hDK8NcNERSZLQiMfRzEGtfnvZvdBvey3SQbgn8Dcrk0h1I6vpxApjb10PFUflZrgJ2WEZyJQ+5v7YQ=="], + "neotraverse": ["neotraverse@0.6.18", "", {}, "sha512-Z4SmBUweYa09+o6pG+eASabEpP6QkQ70yHj351pQoEXIs8uHbaU2DWVmzBANKgflPa47A50PtB2+NgRpQvr7vA=="], "next-themes": ["next-themes@0.4.6", "", { "peerDependencies": { "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA=="], @@ -1202,6 +1259,10 @@ "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], + "pvtsutils": ["pvtsutils@1.3.6", "", { "dependencies": { "tslib": "^2.8.1" } }, "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg=="], + + "pvutils": ["pvutils@1.1.3", "", {}, "sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ=="], + "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], "radix3": ["radix3@1.1.2", "", {}, "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA=="], @@ -1286,6 +1347,8 @@ "rollup": ["rollup@4.41.1", "", { "dependencies": { "@types/estree": "1.0.7" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.41.1", "@rollup/rollup-android-arm64": "4.41.1", "@rollup/rollup-darwin-arm64": "4.41.1", "@rollup/rollup-darwin-x64": "4.41.1", "@rollup/rollup-freebsd-arm64": "4.41.1", "@rollup/rollup-freebsd-x64": "4.41.1", "@rollup/rollup-linux-arm-gnueabihf": "4.41.1", "@rollup/rollup-linux-arm-musleabihf": "4.41.1", "@rollup/rollup-linux-arm64-gnu": "4.41.1", "@rollup/rollup-linux-arm64-musl": "4.41.1", "@rollup/rollup-linux-loongarch64-gnu": "4.41.1", "@rollup/rollup-linux-powerpc64le-gnu": "4.41.1", "@rollup/rollup-linux-riscv64-gnu": "4.41.1", "@rollup/rollup-linux-riscv64-musl": "4.41.1", "@rollup/rollup-linux-s390x-gnu": "4.41.1", "@rollup/rollup-linux-x64-gnu": "4.41.1", "@rollup/rollup-linux-x64-musl": "4.41.1", "@rollup/rollup-win32-arm64-msvc": "4.41.1", "@rollup/rollup-win32-ia32-msvc": "4.41.1", "@rollup/rollup-win32-x64-msvc": "4.41.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-cPmwD3FnFv8rKMBc1MxWCwVQFxwf1JEmSX3iQXrRVVG15zerAIXRjMFVWnd5Q5QvgKF7Aj+5ykXFhUl+QGnyOw=="], + "rou3": ["rou3@0.5.1", "", {}, "sha512-OXMmJ3zRk2xeXFGfA3K+EOPHC5u7RDFG7lIOx0X1pdnhUkI8MdVrbV+sNsD80ElpUZ+MRHdyxPnFthq9VHs8uQ=="], + "rrweb-cssom": ["rrweb-cssom@0.8.0", "", {}, "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw=="], "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], @@ -1304,6 +1367,8 @@ "server-destroy": ["server-destroy@1.0.1", "", {}, "sha512-rb+9B5YBIEzYcD6x2VKidaa+cqYBJQKnU4oe4E3ANwRRN56yk/ua1YCJT1n21NTS8w6CcOclAKNP3PhdCXKYtQ=="], + "set-cookie-parser": ["set-cookie-parser@2.7.1", "", {}, "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ=="], + "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], "sharp": ["sharp@0.33.5", "", { "dependencies": { "color": "^4.2.3", "detect-libc": "^2.0.3", "semver": "^7.6.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.33.5", "@img/sharp-darwin-x64": "0.33.5", "@img/sharp-libvips-darwin-arm64": "1.0.4", "@img/sharp-libvips-darwin-x64": "1.0.4", "@img/sharp-libvips-linux-arm": "1.0.5", "@img/sharp-libvips-linux-arm64": "1.0.4", "@img/sharp-libvips-linux-s390x": "1.0.4", "@img/sharp-libvips-linux-x64": "1.0.4", "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", "@img/sharp-libvips-linuxmusl-x64": "1.0.4", "@img/sharp-linux-arm": "0.33.5", "@img/sharp-linux-arm64": "0.33.5", "@img/sharp-linux-s390x": "0.33.5", "@img/sharp-linux-x64": "0.33.5", "@img/sharp-linuxmusl-arm64": "0.33.5", "@img/sharp-linuxmusl-x64": "0.33.5", "@img/sharp-wasm32": "0.33.5", "@img/sharp-win32-ia32": "0.33.5", "@img/sharp-win32-x64": "0.33.5" } }, "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw=="], @@ -1324,6 +1389,8 @@ "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + "source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="], + "space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="], "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], @@ -1576,6 +1643,8 @@ "@babel/template/@babel/parser": ["@babel/parser@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" }, "bin": "./bin/babel-parser.js" }, "sha512-xyYxRj6+tLNDTWi0KCBcZ9V7yg3/lwL9DWh9Uwh/RIVlIfFidggcgxKX3GCXwCiswwcGRawBKbEg2LG/Y8eJhw=="], + "@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="], + "@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.4.3", "", { "dependencies": { "@emnapi/wasi-threads": "1.0.2", "tslib": "^2.4.0" }, "bundled": true }, "sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g=="], @@ -1626,6 +1695,8 @@ "prompts/kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="], + "source-map-support/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + "strip-literal/js-tokens": ["js-tokens@9.0.1", "", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="], "vscode-json-languageservice/jsonc-parser": ["jsonc-parser@3.3.1", "", {}, "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ=="], @@ -1654,6 +1725,50 @@ "@babel/helper-module-transforms/@babel/traverse/@babel/parser": ["@babel/parser@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" }, "bin": "./bin/babel-parser.js" }, "sha512-xyYxRj6+tLNDTWi0KCBcZ9V7yg3/lwL9DWh9Uwh/RIVlIfFidggcgxKX3GCXwCiswwcGRawBKbEg2LG/Y8eJhw=="], + "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.18.20", "", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.18.20", "", { "os": "android", "cpu": "arm64" }, "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.18.20", "", { "os": "android", "cpu": "x64" }, "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.18.20", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.18.20", "", { "os": "darwin", "cpu": "x64" }, "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.18.20", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.18.20", "", { "os": "freebsd", "cpu": "x64" }, "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.18.20", "", { "os": "linux", "cpu": "arm" }, "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.18.20", "", { "os": "linux", "cpu": "arm64" }, "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.18.20", "", { "os": "linux", "cpu": "ia32" }, "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.18.20", "", { "os": "linux", "cpu": "ppc64" }, "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.18.20", "", { "os": "linux", "cpu": "s390x" }, "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.18.20", "", { "os": "linux", "cpu": "x64" }, "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.18.20", "", { "os": "none", "cpu": "x64" }, "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.18.20", "", { "os": "openbsd", "cpu": "x64" }, "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.18.20", "", { "os": "sunos", "cpu": "x64" }, "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.18.20", "", { "os": "win32", "cpu": "arm64" }, "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.18.20", "", { "os": "win32", "cpu": "ia32" }, "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.18.20", "", { "os": "win32", "cpu": "x64" }, "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="], + "boxen/string-width/emoji-regex": ["emoji-regex@10.4.0", "", {}, "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw=="], "boxen/string-width/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], diff --git a/certs/README.md b/certs/README.md index dcf4243..98c0a94 100644 --- a/certs/README.md +++ b/certs/README.md @@ -1,149 +1,236 @@ -# Custom CA Certificate Support +# CA Certificates Configuration -This guide explains how to configure Gitea Mirror to work with self-signed certificates or custom Certificate Authorities (CAs). - -> **๐Ÿ“ This is the certs directory!** Place your `.crt` certificate files directly in this directory and they will be automatically loaded when the Docker container starts. +This document explains how to configure custom Certificate Authority (CA) certificates for Gitea Mirror when connecting to self-signed or privately signed Gitea instances. ## Overview -When connecting to a Gitea instance that uses self-signed certificates or certificates from a private CA, you need to configure the application to trust these certificates. Gitea Mirror supports mounting custom CA certificates that will be automatically configured for use. +When your Gitea instance uses a self-signed certificate or a certificate signed by a private Certificate Authority (CA), you need to configure Gitea Mirror to trust these certificates. -## Configuration Steps +## Common SSL/TLS Errors -### 1. Prepare Your CA Certificates +If you encounter any of these errors, you need to configure CA certificates: -You're already in the right place! Simply copy your CA certificate(s) into this `certs` directory with `.crt` extension: +- `UNABLE_TO_VERIFY_LEAF_SIGNATURE` +- `SELF_SIGNED_CERT_IN_CHAIN` +- `UNABLE_TO_GET_ISSUER_CERT_LOCALLY` +- `CERT_UNTRUSTED` +- `unable to verify the first certificate` -```bash -# From the project root: -cp /path/to/your/ca-certificate.crt ./certs/ +## Configuration by Deployment Method -# Or if you're already in the certs directory: -cp /path/to/your/ca-certificate.crt . -``` +### Docker -You can add multiple CA certificates - they will all be combined into a single bundle. +#### Method 1: Volume Mount (Recommended) -### 2. Mount Certificates in Docker - -Edit your `docker-compose.yml` file to mount the certificates. You have two options: - -**Option 1: Mount individual certificates from certs directory** -```yaml -services: - gitea-mirror: - # ... other configuration ... - volumes: - - gitea-mirror-data:/app/data - - ./certs:/app/certs:ro # Mount CA certificates directory -``` - -**Option 2: Mount system CA bundle (if your CA is already installed system-wide)** -```yaml -services: - gitea-mirror: - # ... other configuration ... - volumes: - - gitea-mirror-data:/app/data - - /etc/ssl/certs/ca-certificates.crt:/etc/ssl/certs/ca-certificates.crt:ro -``` - -> **Note**: Use Option 2 if you've already added your CA certificate to your system's certificate store using `update-ca-certificates` or similar commands. - -> **System CA Bundle Locations**: -> - Debian/Ubuntu: `/etc/ssl/certs/ca-certificates.crt` -> - RHEL/CentOS/Fedora: `/etc/pki/tls/certs/ca-bundle.crt` -> - Alpine Linux: `/etc/ssl/certs/ca-certificates.crt` -> - macOS: `/etc/ssl/cert.pem` - -### 3. Start the Container - -Start or restart your container: - -```bash -docker-compose up -d -``` - -The container will automatically: -1. Detect any `.crt` files in `/app/certs` (Option 1) OR detect mounted system CA bundle (Option 2) -2. For Option 1: Combine certificates into a CA bundle -3. Configure Node.js to use these certificates via `NODE_EXTRA_CA_CERTS` - -You should see log messages like: - -**For Option 1 (individual certificates):** -``` -Custom CA certificates found, configuring Node.js to use them... -Adding certificate: my-ca.crt -NODE_EXTRA_CA_CERTS set to: /app/certs/ca-bundle.crt -``` - -**For Option 2 (system CA bundle):** -``` -System CA bundle mounted, configuring Node.js to use it... -NODE_EXTRA_CA_CERTS set to: /etc/ssl/certs/ca-certificates.crt -``` - -## Testing & Troubleshooting - -### Disable TLS Verification (Testing Only) - -For testing purposes only, you can disable TLS verification entirely: - -```yaml -environment: - - GITEA_SKIP_TLS_VERIFY=true -``` - -**WARNING**: This is insecure and should never be used in production! - -### Common Issues - -1. **Certificate not recognized**: Ensure your certificate file has a `.crt` extension -2. **Connection still fails**: Check that the certificate is in PEM format -3. **Multiple certificates needed**: Add all required certificates (root and intermediate) to the certs directory - -### Verifying Certificate Loading - -Check the container logs to confirm certificates are loaded: - -```bash -docker-compose logs gitea-mirror | grep "CA certificates" -``` - -## Security Considerations - -- Always use proper CA certificates in production -- Never disable TLS verification in production environments -- Keep your CA certificates secure and limit access to the certs directory -- Regularly update certificates before they expire - -## Example Setup - -Here's a complete example for a self-hosted Gitea with custom CA: - -1. Copy your Gitea server's CA certificate to this directory: +1. Create a certificates directory: ```bash - cp /etc/ssl/certs/my-company-ca.crt ./certs/ + mkdir -p ./certs ``` -2. Update `docker-compose.yml`: +2. Copy your CA certificate(s): + ```bash + cp /path/to/your-ca-cert.crt ./certs/ + ``` + +3. Update `docker-compose.yml`: ```yaml + version: '3.8' services: gitea-mirror: - image: ghcr.io/raylabshq/gitea-mirror:latest + image: raylabs/gitea-mirror:latest volumes: - - gitea-mirror-data:/app/data - - ./certs:/app/certs:ro + - ./data:/app/data + - ./certs:/usr/local/share/ca-certificates:ro environment: - - GITEA_URL=https://gitea.mycompany.local - - GITEA_TOKEN=your-token - # ... other configuration ... + - NODE_EXTRA_CA_CERTS=/usr/local/share/ca-certificates/your-ca-cert.crt ``` -3. Start the service: +4. Restart the container: ```bash - docker-compose up -d + docker-compose down && docker-compose up -d ``` -The application will now trust your custom CA when connecting to your Gitea instance. \ No newline at end of file +#### Method 2: Custom Docker Image + +Create a `Dockerfile`: + +```dockerfile +FROM raylabs/gitea-mirror:latest + +# Copy CA certificates +COPY ./certs/*.crt /usr/local/share/ca-certificates/ + +# Update CA certificates +RUN update-ca-certificates + +# Set environment variable +ENV NODE_EXTRA_CA_CERTS=/usr/local/share/ca-certificates/your-ca-cert.crt +``` + +Build and use: +```bash +docker build -t my-gitea-mirror . +``` + +### Native/Bun + +#### Method 1: Environment Variable + +```bash +export NODE_EXTRA_CA_CERTS=/path/to/your-ca-cert.crt +bun run start +``` + +#### Method 2: .env File + +Add to your `.env` file: +``` +NODE_EXTRA_CA_CERTS=/path/to/your-ca-cert.crt +``` + +#### Method 3: System CA Store + +**Ubuntu/Debian:** +```bash +sudo cp your-ca-cert.crt /usr/local/share/ca-certificates/ +sudo update-ca-certificates +``` + +**RHEL/CentOS/Fedora:** +```bash +sudo cp your-ca-cert.crt /etc/pki/ca-trust/source/anchors/ +sudo update-ca-trust +``` + +**macOS:** +```bash +sudo security add-trusted-cert -d -r trustRoot \ + -k /Library/Keychains/System.keychain your-ca-cert.crt +``` + +### LXC Container (Proxmox VE) + +1. Enter the container: + ```bash + pct enter + ``` + +2. Create certificates directory: + ```bash + mkdir -p /usr/local/share/ca-certificates + ``` + +3. Copy your CA certificate: + ```bash + cat > /usr/local/share/ca-certificates/your-ca.crt + ``` + (Paste certificate content and press Ctrl+D) + +4. Update the systemd service: + ```bash + cat >> /etc/systemd/system/gitea-mirror.service << EOF + Environment="NODE_EXTRA_CA_CERTS=/usr/local/share/ca-certificates/your-ca.crt" + EOF + ``` + +5. Reload and restart: + ```bash + systemctl daemon-reload + systemctl restart gitea-mirror + ``` + +## Multiple CA Certificates + +### Option 1: Bundle Certificates + +```bash +cat ca-cert1.crt ca-cert2.crt ca-cert3.crt > ca-bundle.crt +export NODE_EXTRA_CA_CERTS=/path/to/ca-bundle.crt +``` + +### Option 2: System CA Store + +```bash +# Copy all certificates +cp *.crt /usr/local/share/ca-certificates/ +update-ca-certificates +``` + +## Verification + +### 1. Test Gitea Connection +Use the "Test Connection" button in the Gitea configuration section. + +### 2. Check Logs + +**Docker:** +```bash +docker logs gitea-mirror +``` + +**Native:** +Check terminal output + +**LXC:** +```bash +journalctl -u gitea-mirror -f +``` + +### 3. Manual Certificate Test + +```bash +openssl s_client -connect your-gitea-domain.com:443 -CAfile /path/to/ca-cert.crt +``` + +## Best Practices + +1. **Certificate Security** + - Keep CA certificates secure + - Use read-only mounts in Docker + - Limit certificate file permissions + - Regularly update certificates + +2. **Certificate Management** + - Use descriptive certificate filenames + - Document certificate purposes + - Track certificate expiration dates + - Maintain certificate backups + +3. **Production Deployment** + - Use proper SSL certificates when possible + - Consider Let's Encrypt for public instances + - Implement certificate rotation procedures + - Monitor certificate expiration + +## Troubleshooting + +### Certificate not being recognized +- Ensure the certificate is in PEM format +- Check that `NODE_EXTRA_CA_CERTS` points to the correct file +- Restart the application after adding certificates + +### Still getting SSL errors +- Verify the complete certificate chain is included +- Check if intermediate certificates are needed +- Ensure the certificate matches the server hostname + +### Certificate expired +- Check validity: `openssl x509 -in cert.crt -noout -dates` +- Update with new certificate from your CA +- Restart Gitea Mirror after updating + +## Certificate Format + +Certificates must be in PEM format. Example: + +``` +-----BEGIN CERTIFICATE----- +MIIDXTCCAkWgAwIBAgIJAKl8bUgMdErlMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV +[... certificate content ...] +-----END CERTIFICATE----- +``` + +If your certificate is in DER format, convert it: +```bash +openssl x509 -inform der -in certificate.cer -out certificate.crt +``` \ No newline at end of file diff --git a/docker-compose.alt.yml b/docker-compose.alt.yml index ac011be..9e6a76e 100644 --- a/docker-compose.alt.yml +++ b/docker-compose.alt.yml @@ -15,7 +15,7 @@ services: - DATABASE_URL=file:data/gitea-mirror.db - HOST=0.0.0.0 - PORT=4321 - - JWT_SECRET=${JWT_SECRET:-your-secret-key-change-this-in-production} + - BETTER_AUTH_SECRET=${BETTER_AUTH_SECRET:-your-secret-key-change-this-in-production} healthcheck: test: ["CMD", "wget", "--no-verbose", "--tries=3", "--spider", "http://localhost:4321/api/health"] interval: 30s diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index a4824c9..321297e 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -66,7 +66,7 @@ services: - DATABASE_URL=file:data/gitea-mirror.db - HOST=0.0.0.0 - PORT=4321 - - JWT_SECRET=dev-secret-key + - BETTER_AUTH_SECRET=dev-secret-key # GitHub/Gitea Mirror Config - GITHUB_USERNAME=${GITHUB_USERNAME:-your-github-username} - GITHUB_TOKEN=${GITHUB_TOKEN:-your-github-token} diff --git a/docker-compose.yml b/docker-compose.yml index 527ca05..ec9110e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -28,7 +28,10 @@ services: - DATABASE_URL=file:data/gitea-mirror.db - HOST=0.0.0.0 - PORT=4321 - - JWT_SECRET=${JWT_SECRET:-your-secret-key-change-this-in-production} + - BETTER_AUTH_SECRET=${BETTER_AUTH_SECRET:-your-secret-key-change-this-in-production} + - BETTER_AUTH_URL=${BETTER_AUTH_URL:-http://localhost:4321} + # Optional: ENCRYPTION_SECRET will be auto-generated if not provided + # - ENCRYPTION_SECRET=${ENCRYPTION_SECRET:-} # GitHub/Gitea Mirror Config - GITHUB_USERNAME=${GITHUB_USERNAME:-} - GITHUB_TOKEN=${GITHUB_TOKEN:-} @@ -49,6 +52,13 @@ services: - DELAY=${DELAY:-3600} # Optional: Skip TLS verification (insecure, use only for testing) # - GITEA_SKIP_TLS_VERIFY=${GITEA_SKIP_TLS_VERIFY:-false} + # Header Authentication (for Reverse Proxy SSO) + - HEADER_AUTH_ENABLED=${HEADER_AUTH_ENABLED:-false} + - HEADER_AUTH_USER_HEADER=${HEADER_AUTH_USER_HEADER:-X-Authentik-Username} + - HEADER_AUTH_EMAIL_HEADER=${HEADER_AUTH_EMAIL_HEADER:-X-Authentik-Email} + - HEADER_AUTH_NAME_HEADER=${HEADER_AUTH_NAME_HEADER:-X-Authentik-Name} + - HEADER_AUTH_AUTO_PROVISION=${HEADER_AUTH_AUTO_PROVISION:-false} + - HEADER_AUTH_ALLOWED_DOMAINS=${HEADER_AUTH_ALLOWED_DOMAINS:-} healthcheck: test: ["CMD", "wget", "--no-verbose", "--tries=3", "--spider", "http://localhost:4321/api/health"] interval: 30s diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index 2ea977d..c010719 100644 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -52,15 +52,26 @@ if [ "$GITEA_SKIP_TLS_VERIFY" = "true" ]; then export NODE_TLS_REJECT_UNAUTHORIZED=0 fi -# Generate a secure JWT secret if one isn't provided or is using the default value -JWT_SECRET_FILE="/app/data/.jwt_secret" -if [ "$JWT_SECRET" = "your-secret-key-change-this-in-production" ] || [ -z "$JWT_SECRET" ]; then +# Generate a secure BETTER_AUTH_SECRET if one isn't provided or is using the default value +BETTER_AUTH_SECRET_FILE="/app/data/.better_auth_secret" +JWT_SECRET_FILE="/app/data/.jwt_secret" # Old file for backward compatibility + +if [ "$BETTER_AUTH_SECRET" = "your-secret-key-change-this-in-production" ] || [ -z "$BETTER_AUTH_SECRET" ]; then # Check if we have a previously generated secret - if [ -f "$JWT_SECRET_FILE" ]; then - echo "Using previously generated JWT secret" - export JWT_SECRET=$(cat "$JWT_SECRET_FILE") + if [ -f "$BETTER_AUTH_SECRET_FILE" ]; then + echo "Using previously generated BETTER_AUTH_SECRET" + export BETTER_AUTH_SECRET=$(cat "$BETTER_AUTH_SECRET_FILE") + # Check for old JWT_SECRET file for backward compatibility + elif [ -f "$JWT_SECRET_FILE" ]; then + echo "Migrating from old JWT_SECRET to BETTER_AUTH_SECRET" + export BETTER_AUTH_SECRET=$(cat "$JWT_SECRET_FILE") + # Save to new file + echo "$BETTER_AUTH_SECRET" > "$BETTER_AUTH_SECRET_FILE" + chmod 600 "$BETTER_AUTH_SECRET_FILE" + # Optionally remove old file after successful migration + rm -f "$JWT_SECRET_FILE" else - echo "Generating a secure random JWT secret" + echo "Generating a secure random BETTER_AUTH_SECRET" # Try to generate a secure random string using OpenSSL if command -v openssl >/dev/null 2>&1; then GENERATED_SECRET=$(openssl rand -hex 32) @@ -69,12 +80,38 @@ if [ "$JWT_SECRET" = "your-secret-key-change-this-in-production" ] || [ -z "$JWT echo "OpenSSL not found, using fallback method for random generation" GENERATED_SECRET=$(head -c 32 /dev/urandom | sha256sum | cut -d' ' -f1) fi - export JWT_SECRET="$GENERATED_SECRET" + export BETTER_AUTH_SECRET="$GENERATED_SECRET" # Save the secret to a file for persistence across container restarts - echo "$GENERATED_SECRET" > "$JWT_SECRET_FILE" - chmod 600 "$JWT_SECRET_FILE" + echo "$GENERATED_SECRET" > "$BETTER_AUTH_SECRET_FILE" + chmod 600 "$BETTER_AUTH_SECRET_FILE" fi - echo "JWT_SECRET has been set to a secure random value" + echo "BETTER_AUTH_SECRET has been set to a secure random value" +fi + +# Generate a secure ENCRYPTION_SECRET if one isn't provided +ENCRYPTION_SECRET_FILE="/app/data/.encryption_secret" + +if [ -z "$ENCRYPTION_SECRET" ]; then + # Check if we have a previously generated secret + if [ -f "$ENCRYPTION_SECRET_FILE" ]; then + echo "Using previously generated ENCRYPTION_SECRET" + export ENCRYPTION_SECRET=$(cat "$ENCRYPTION_SECRET_FILE") + else + echo "Generating a secure random ENCRYPTION_SECRET" + # Generate a 48-character secret for encryption + if command -v openssl >/dev/null 2>&1; then + GENERATED_ENCRYPTION_SECRET=$(openssl rand -base64 36) + else + # Fallback to using /dev/urandom if openssl is not available + echo "OpenSSL not found, using fallback method for encryption secret generation" + GENERATED_ENCRYPTION_SECRET=$(head -c 36 /dev/urandom | base64 | tr -d '\n' | head -c 48) + fi + export ENCRYPTION_SECRET="$GENERATED_ENCRYPTION_SECRET" + # Save the secret to a file for persistence across container restarts + echo "$GENERATED_ENCRYPTION_SECRET" > "$ENCRYPTION_SECRET_FILE" + chmod 600 "$ENCRYPTION_SECRET_FILE" + fi + echo "ENCRYPTION_SECRET has been set to a secure random value" fi @@ -245,6 +282,69 @@ else else echo "Warning: Could not find mirror_jobs table update script." fi + + # Run v3 migrations if needed + echo "Checking for v3 migrations..." + + # Check if we need to run Better Auth migration (check if accounts table exists) + if ! sqlite3 /app/data/gitea-mirror.db "SELECT name FROM sqlite_master WHERE type='table' AND name='accounts';" | grep -q accounts; then + echo "๐Ÿ”„ v3 Migration: Creating Better Auth tables..." + # Create Better Auth tables + sqlite3 /app/data/gitea-mirror.db < { + // Custom user provisioning logic + return data; + }, + }), + ], +}); +``` + +### 2. Register OIDC Providers + +```typescript +// Example: Register an OIDC provider +await authClient.sso.register({ + issuer: "https://idp.example.com", + domain: "example.com", + clientId: "your-client-id", + clientSecret: "your-client-secret", + providerId: "example-provider", +}); +``` + +### 3. Enable OIDC Provider Mode + +To make Gitea Mirror act as an OIDC provider: + +```typescript +// src/lib/auth.ts +import { oidcProvider } from "better-auth/plugins/oidc"; + +export const auth = betterAuth({ + // ... existing config + plugins: [ + oidcProvider({ + loginPage: "/signin", + consentPage: "/oauth/consent", + metadata: { + issuer: process.env.BETTER_AUTH_URL || "http://localhost:3000", + }, + }), + ], +}); +``` + +### 4. Database Migration for SSO + +When enabling SSO/OIDC, run migrations to add required tables: + +```bash +# Generate the schema +bun drizzle-kit generate + +# Apply the migration +bun drizzle-kit migrate +``` + +New tables that will be added: +- `sso_providers` - SSO provider configurations +- `oauth_applications` - OAuth2 client applications +- `oauth_access_tokens` - OAuth2 access tokens +- `oauth_consents` - User consent records + +## Environment Variables + +Required environment variables: + +```env +# Better Auth configuration +BETTER_AUTH_SECRET=your-secret-key +BETTER_AUTH_URL=http://localhost:3000 + +# Legacy (kept for compatibility) +JWT_SECRET=your-secret-key +``` + +## Migration Script + +To migrate existing users to Better Auth: + +```bash +bun run migrate:better-auth +``` + +This script: +1. Creates credential accounts for existing users +2. Moves password hashes to the accounts table +3. Preserves user creation dates + +## Troubleshooting + +### Login Issues +- Ensure users log in with email, not username +- Check that BETTER_AUTH_SECRET is set +- Verify database migrations have been applied + +### Session Issues +- Clear browser cookies if experiencing session problems +- Check middleware is properly configured +- Ensure auth routes are accessible at `/api/auth/*` + +### Development Tips +- Use `bun db:studio` to inspect database tables +- Check `/api/auth/session` to verify current session +- Enable debug logging in Better Auth for troubleshooting + +## Resources + +- [Better Auth Documentation](https://better-auth.com) +- [Better Auth Astro Integration](https://better-auth.com/docs/integrations/astro) +- [Better Auth Plugins](https://better-auth.com/docs/plugins) \ No newline at end of file diff --git a/docs/BUILD_GUIDE.md b/docs/BUILD_GUIDE.md new file mode 100644 index 0000000..c74146c --- /dev/null +++ b/docs/BUILD_GUIDE.md @@ -0,0 +1,205 @@ +# Build Guide + +This guide covers building the open-source version of Gitea Mirror. + +## Prerequisites + +- **Bun** >= 1.2.9 (primary runtime) +- **Node.js** >= 20 (for compatibility) +- **Git** + +## Quick Start + +```bash +# Clone repository +git clone https://github.com/yourusername/gitea-mirror.git +cd gitea-mirror + +# Install dependencies +bun install + +# Initialize database +bun run init-db + +# Build for production +bun run build + +# Start the application +bun run start +``` + +## Build Commands + +| Command | Description | +|---------|-------------| +| `bun run build` | Production build | +| `bun run dev` | Development server | +| `bun run preview` | Preview production build | +| `bun test` | Run tests | +| `bun run cleanup-db` | Remove database files | + +## Build Output + +The build creates: +- `dist/` - Production-ready server files +- `.astro/` - Build cache (git-ignored) +- `data/` - SQLite database location + +## Development Build + +For active development with hot reload: + +```bash +bun run dev +``` + +Access the application at http://localhost:4321 + +## Production Build + +```bash +# Build +bun run build + +# Test the build +bun run preview + +# Run in production +bun run start +``` + +## Docker Build + +```dockerfile +# Build Docker image +docker build -t gitea-mirror:latest . + +# Run container +docker run -p 3000:3000 gitea-mirror:latest +``` + +## Environment Variables + +Create a `.env` file: + +```env +# Database +DATABASE_PATH=./data/gitea-mirror.db + +# Authentication +JWT_SECRET=your-secret-here + +# GitHub Configuration +GITHUB_TOKEN=ghp_... +GITHUB_WEBHOOK_SECRET=... + +# Gitea Configuration +GITEA_URL=https://your-gitea.com +GITEA_TOKEN=... +``` + +## Common Build Issues + +### Missing Dependencies + +```bash +# Solution +bun install +``` + +### Database Not Initialized + +```bash +# Solution +bun run init-db +``` + +### Port Already in Use + +```bash +# Change port +PORT=3001 bun run dev +``` + +### Build Cache Issues + +```bash +# Clear cache +rm -rf .astro/ dist/ +bun run build +``` + +## Build Optimization + +### Development Speed + +- Use `bun run dev` for hot reload +- Skip type checking during rapid development +- Keep `.astro/` cache between builds + +### Production Optimization + +- Minification enabled automatically +- Tree shaking removes unused code +- Image optimization with Sharp + +## Validation + +After building, verify: + +```bash +# Check build output +ls -la dist/ + +# Test server starts +bun run start + +# Check health endpoint +curl http://localhost:3000/api/health +``` + +## CI/CD Build + +Example GitHub Actions workflow: + +```yaml +name: Build and Test +on: [push, pull_request] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: oven-sh/setup-bun@v2 + - run: bun install + - run: bun run build + - run: bun test +``` + +## Troubleshooting + +### Build Fails + +1. Check Bun version: `bun --version` +2. Clear dependencies: `rm -rf node_modules && bun install` +3. Check for syntax errors: `bunx tsc --noEmit` + +### Runtime Errors + +1. Check environment variables +2. Verify database exists +3. Check file permissions + +## Performance + +Expected build times: +- Clean build: ~5-10 seconds +- Incremental build: ~2-5 seconds +- Development startup: ~1-2 seconds + +## Next Steps + +- Configure with [Configuration Guide](./CONFIGURATION.md) +- Deploy with [Deployment Guide](./DEPLOYMENT.md) +- Set up authentication with [SSO Guide](./SSO-OIDC-SETUP.md) \ No newline at end of file diff --git a/docs/DEVELOPMENT_WORKFLOW.md b/docs/DEVELOPMENT_WORKFLOW.md new file mode 100644 index 0000000..f103788 --- /dev/null +++ b/docs/DEVELOPMENT_WORKFLOW.md @@ -0,0 +1,355 @@ +# Development Workflow + +This guide covers the development workflow for the open-source Gitea Mirror. + +## Getting Started + +### Prerequisites + +- Bun >= 1.2.9 +- Node.js >= 20 +- Git +- GitHub account (for API access) +- Gitea instance (for testing) + +### Initial Setup + +1. **Clone the repository**: +```bash +git clone https://github.com/yourusername/gitea-mirror.git +cd gitea-mirror +``` + +2. **Install dependencies**: +```bash +bun install +``` + +3. **Initialize database**: +```bash +bun run init-db +``` + +4. **Configure environment**: +```bash +cp .env.example .env +# Edit .env with your settings +``` + +5. **Start development server**: +```bash +bun run dev +``` + +## Development Commands + +| Command | Description | +|---------|-------------| +| `bun run dev` | Start development server with hot reload | +| `bun run build` | Build for production | +| `bun run preview` | Preview production build | +| `bun test` | Run all tests | +| `bun test:watch` | Run tests in watch mode | +| `bun run db:studio` | Open database GUI | + +## Project Structure + +``` +gitea-mirror/ +โ”œโ”€โ”€ src/ +โ”‚ โ”œโ”€โ”€ components/ # React components +โ”‚ โ”œโ”€โ”€ pages/ # Astro pages & API routes +โ”‚ โ”œโ”€โ”€ lib/ # Core logic +โ”‚ โ”‚ โ”œโ”€โ”€ db/ # Database queries +โ”‚ โ”‚ โ”œโ”€โ”€ utils/ # Helper functions +โ”‚ โ”‚ โ””โ”€โ”€ modules/ # Module system +โ”‚ โ”œโ”€โ”€ hooks/ # React hooks +โ”‚ โ””โ”€โ”€ types/ # TypeScript types +โ”œโ”€โ”€ public/ # Static assets +โ”œโ”€โ”€ scripts/ # Utility scripts +โ””โ”€โ”€ tests/ # Test files +``` + +## Feature Development + +### Adding a New Feature + +1. **Create feature branch**: +```bash +git checkout -b feature/my-feature +``` + +2. **Plan your changes**: +- UI components in `/src/components/` +- API endpoints in `/src/pages/api/` +- Database queries in `/src/lib/db/queries/` +- Types in `/src/types/` + +3. **Implement the feature**: + +**Example: Adding a new API endpoint** +```typescript +// src/pages/api/my-endpoint.ts +import type { APIRoute } from 'astro'; +import { getUserFromCookie } from '@/lib/auth-utils'; + +export const GET: APIRoute = async ({ request }) => { + const user = await getUserFromCookie(request); + if (!user) { + return new Response('Unauthorized', { status: 401 }); + } + + // Your logic here + return new Response(JSON.stringify({ data: 'success' }), { + headers: { 'Content-Type': 'application/json' } + }); +}; +``` + +4. **Write tests**: +```typescript +// src/lib/my-feature.test.ts +import { describe, it, expect } from 'bun:test'; + +describe('My Feature', () => { + it('should work correctly', () => { + expect(myFunction()).toBe('expected'); + }); +}); +``` + +5. **Update documentation**: +- Add JSDoc comments +- Update README if needed +- Document API changes + +## Database Development + +### Schema Changes + +1. **Modify schema**: +```typescript +// src/lib/db/schema.ts +export const myTable = sqliteTable('my_table', { + id: text('id').primaryKey(), + name: text('name').notNull(), + createdAt: integer('created_at').notNull(), +}); +``` + +2. **Generate migration**: +```bash +bun run db:generate +``` + +3. **Apply migration**: +```bash +bun run db:migrate +``` + +### Writing Queries + +```typescript +// src/lib/db/queries/my-queries.ts +import { db } from '../index'; +import { myTable } from '../schema'; + +export async function getMyData(userId: string) { + return db.select() + .from(myTable) + .where(eq(myTable.userId, userId)); +} +``` + +## Testing + +### Unit Tests + +```bash +# Run all tests +bun test + +# Run specific test file +bun test auth + +# Watch mode +bun test:watch + +# Coverage +bun test:coverage +``` + +### Manual Testing Checklist + +- [ ] Feature works as expected +- [ ] No console errors +- [ ] Responsive on mobile +- [ ] Handles errors gracefully +- [ ] Loading states work +- [ ] Form validation works +- [ ] API returns correct status codes + +## Debugging + +### VSCode Configuration + +Create `.vscode/launch.json`: +```json +{ + "version": "0.2.0", + "configurations": [ + { + "type": "bun", + "request": "launch", + "name": "Debug Bun", + "program": "${workspaceFolder}/src/index.ts", + "cwd": "${workspaceFolder}", + "env": { + "NODE_ENV": "development" + } + } + ] +} +``` + +### Debug Logging + +```typescript +// Development only logging +if (import.meta.env.DEV) { + console.log('[Debug]', data); +} +``` + +## Code Style + +### TypeScript + +- Use strict mode +- Define interfaces for all data structures +- Avoid `any` type +- Use proper error handling + +### React Components + +- Use functional components +- Implement proper loading states +- Handle errors with error boundaries +- Use TypeScript for props + +### API Routes + +- Always validate input +- Return proper status codes +- Use consistent error format +- Document with JSDoc + +## Git Workflow + +### Commit Messages + +Follow conventional commits: +``` +feat: add repository filtering +fix: resolve sync timeout issue +docs: update API documentation +style: format code with prettier +refactor: simplify auth logic +test: add user creation tests +chore: update dependencies +``` + +### Pull Request Process + +1. Create feature branch +2. Make changes +3. Write/update tests +4. Update documentation +5. Create PR with description +6. Address review feedback +7. Squash and merge + +## Performance + +### Development Tips + +- Use React DevTools +- Monitor bundle size +- Profile database queries +- Check memory usage + +### Optimization + +- Lazy load components +- Optimize images +- Use database indexes +- Cache API responses + +## Common Issues + +### Port Already in Use + +```bash +# Use different port +PORT=3001 bun run dev +``` + +### Database Locked + +```bash +# Reset database +bun run cleanup-db +bun run init-db +``` + +### Type Errors + +```bash +# Check types +bunx tsc --noEmit +``` + +## Release Process + +1. **Update version**: +```bash +npm version patch # or minor/major +``` + +2. **Update CHANGELOG.md** + +3. **Build and test**: +```bash +bun run build +bun test +``` + +4. **Create release**: +```bash +git tag v2.23.0 +git push origin v2.23.0 +``` + +5. **Create GitHub release** + +## Contributing + +1. Fork the repository +2. Create your feature branch +3. Commit your changes +4. Push to your fork +5. Create a Pull Request + +## Resources + +- [Astro Documentation](https://docs.astro.build) +- [Bun Documentation](https://bun.sh/docs) +- [Drizzle ORM](https://orm.drizzle.team) +- [React Documentation](https://react.dev) +- [TypeScript Handbook](https://www.typescriptlang.org/docs/) + +## Getting Help + +- Check existing [issues](https://github.com/yourusername/gitea-mirror/issues) +- Join [discussions](https://github.com/yourusername/gitea-mirror/discussions) +- Read the [FAQ](./FAQ.md) \ No newline at end of file diff --git a/docs/EXTENDING.md b/docs/EXTENDING.md new file mode 100644 index 0000000..8affc7d --- /dev/null +++ b/docs/EXTENDING.md @@ -0,0 +1,77 @@ +# Extending Gitea Mirror + +Gitea Mirror is designed with extensibility in mind through a module system. + +## Module System + +The application provides a module interface that allows extending functionality: + +```typescript +export interface Module { + name: string; + version: string; + init(app: AppContext): Promise; + cleanup?(): Promise; +} +``` + +## Creating Custom Modules + +You can create custom modules to add features: + +```typescript +// my-module.ts +export class MyModule implements Module { + name = 'my-module'; + version = '1.0.0'; + + async init(app: AppContext) { + // Add your functionality + app.addRoute('/api/my-endpoint', this.handler); + } + + async handler(context) { + return new Response('Hello from my module!'); + } +} +``` + +## Module Context + +Modules receive an `AppContext` with: +- Database access +- Event system +- Route registration +- Configuration + +## Private Extensions + +If you're developing private extensions: + +1. Create a separate package/repository +2. Implement the module interface +3. Use Bun's linking feature for development: + ```bash + # In your extension + bun link + + # In gitea-mirror + bun link your-extension + ``` + +## Best Practices + +- Keep modules focused on a single feature +- Use TypeScript for type safety +- Handle errors gracefully +- Clean up resources in `cleanup()` +- Document your module's API + +## Community Modules + +Share your modules with the community: +- Create a GitHub repository +- Tag it with `gitea-mirror-module` +- Submit a PR to list it in our docs + +For more details on the module system, see the source code in `/src/lib/modules/`. \ No newline at end of file diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..818c2b0 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,118 @@ +# Gitea Mirror Documentation + +Welcome to the Gitea Mirror documentation. This guide covers everything you need to know about developing, building, and deploying the open-source version of Gitea Mirror. + +## Documentation Overview + +### Getting Started + +- **[Development Workflow](./DEVELOPMENT_WORKFLOW.md)** - Set up your development environment and start contributing +- **[Build Guide](./BUILD_GUIDE.md)** - Build Gitea Mirror from source +- **[Configuration Guide](./CONFIGURATION.md)** - Configure all available options + +### Deployment + +- **[Deployment Guide](./DEPLOYMENT.md)** - Deploy to production environments +- **[Docker Guide](./DOCKER.md)** - Container-based deployment +- **[Reverse Proxy Setup](./REVERSE_PROXY.md)** - Configure with nginx/Caddy + +### Features + +- **[SSO/OIDC Setup](./SSO-OIDC-SETUP.md)** - Configure authentication providers +- **[Sponsor Integration](./SPONSOR_INTEGRATION.md)** - GitHub Sponsors integration +- **[Webhook Configuration](./WEBHOOKS.md)** - Set up GitHub webhooks + +### Architecture + +- **[Architecture Overview](./ARCHITECTURE.md)** - System design and components +- **[API Documentation](./API.md)** - REST API endpoints +- **[Database Schema](./DATABASE.md)** - SQLite structure + +### Maintenance + +- **[Migration Guide](../MIGRATION_GUIDE.md)** - Upgrade from previous versions +- **[Better Auth Migration](./BETTER_AUTH_MIGRATION.md)** - Migrate authentication system +- **[Troubleshooting](./TROUBLESHOOTING.md)** - Common issues and solutions +- **[Backup & Restore](./BACKUP.md)** - Data management + +## Quick Start + +1. **Clone and install**: +```bash +git clone https://github.com/yourusername/gitea-mirror.git +cd gitea-mirror +bun install +``` + +2. **Configure**: +```bash +cp .env.example .env +# Edit .env with your GitHub and Gitea tokens +``` + +3. **Initialize and run**: +```bash +bun run init-db +bun run dev +``` + +4. **Access**: Open http://localhost:4321 + +## Key Features + +- ๐Ÿ”„ **Automatic Mirroring** - Keep repositories synchronized +- ๐Ÿ—‚๏ธ **Organization Support** - Mirror entire organizations +- โญ **Starred Repos** - Mirror your starred repositories +- ๐Ÿ” **Self-Hosted** - Full control over your data +- ๐Ÿš€ **Fast** - Built with Bun for optimal performance +- ๐Ÿ”’ **Secure** - JWT authentication, encrypted tokens + +## Technology Stack + +- **Runtime**: Bun +- **Framework**: Astro with React +- **Database**: SQLite with Drizzle ORM +- **Styling**: Tailwind CSS v4 +- **Authentication**: Better Auth + +## System Requirements + +- Bun >= 1.2.9 +- Node.js >= 20 (optional, for compatibility) +- SQLite 3 +- 512MB RAM minimum +- 1GB disk space + +## Contributing + +We welcome contributions! Please see our [Contributing Guide](../CONTRIBUTING.md) for details. + +### Development Setup + +1. Fork the repository +2. Create a feature branch +3. Make your changes +4. Add tests +5. Submit a pull request + +### Code of Conduct + +Please read our [Code of Conduct](../CODE_OF_CONDUCT.md) before contributing. + +## Support + +- **Issues**: [GitHub Issues](https://github.com/yourusername/gitea-mirror/issues) +- **Discussions**: [GitHub Discussions](https://github.com/yourusername/gitea-mirror/discussions) +- **Wiki**: [GitHub Wiki](https://github.com/yourusername/gitea-mirror/wiki) + +## Security + +For security issues, please see [SECURITY.md](../SECURITY.md). + +## License + +Gitea Mirror is open source software licensed under the [MIT License](../LICENSE). + +--- + +For detailed information on any topic, please refer to the specific documentation guides listed above. \ No newline at end of file diff --git a/docs/SPONSOR_INTEGRATION.md b/docs/SPONSOR_INTEGRATION.md new file mode 100644 index 0000000..0000566 --- /dev/null +++ b/docs/SPONSOR_INTEGRATION.md @@ -0,0 +1,91 @@ +# GitHub Sponsors Integration + +This guide shows how GitHub Sponsors is integrated into the open-source version of Gitea Mirror. + +## Components + +### GitHubSponsors Card + +A card component that displays in the sidebar or dashboard: + +```tsx +import { GitHubSponsors } from '@/components/sponsors/GitHubSponsors'; + +// In your layout or dashboard + +``` + +### SponsorButton + +A smaller button for headers or navigation: + +```tsx +import { SponsorButton } from '@/components/sponsors/GitHubSponsors'; + +// In your header + +``` + +## Integration Points + +### 1. Dashboard Sidebar + +Add the sponsor card to the dashboard sidebar for visibility: + +```tsx +// src/components/layout/DashboardLayout.tsx + +``` + +### 2. Header Navigation + +Add the sponsor button to the main navigation: + +```tsx +// src/components/layout/Header.tsx + +``` + +### 3. Settings Page + +Add a support section in settings: + +```tsx +// src/components/settings/SupportSection.tsx + + + Support Development + + + + + +``` + +## Behavior + +- **Only appears in self-hosted mode**: The components automatically hide in hosted mode +- **Non-intrusive**: Designed to be helpful without being annoying +- **Multiple options**: GitHub Sponsors, Buy Me a Coffee, and starring the repo + +## Customization + +You can customize the sponsor components by: + +1. Updating the GitHub Sponsors URL +2. Adding/removing donation platforms +3. Changing the styling to match your theme +4. Adjusting the placement based on user feedback + +## Best Practices + +1. **Don't be pushy**: Show sponsor options tastefully +2. **Provide value first**: Ensure the tool is useful before asking for support +3. **Be transparent**: Explain how sponsorships help the project +4. **Thank sponsors**: Acknowledge supporters in README or releases \ No newline at end of file diff --git a/docs/SSO-OIDC-SETUP.md b/docs/SSO-OIDC-SETUP.md new file mode 100644 index 0000000..e1626fa --- /dev/null +++ b/docs/SSO-OIDC-SETUP.md @@ -0,0 +1,205 @@ +# SSO and OIDC Setup Guide + +This guide explains how to configure Single Sign-On (SSO) and OpenID Connect (OIDC) provider functionality in Gitea Mirror. + +## Overview + +Gitea Mirror supports three authentication methods: + +1. **Email & Password** - Traditional authentication (always enabled) +2. **SSO (Single Sign-On)** - Allow users to authenticate using external OIDC providers +3. **OIDC Provider** - Allow other applications to authenticate users through Gitea Mirror + +## Configuration + +All SSO and OIDC settings are managed through the web UI in the Configuration page under the "Authentication" tab. + +## Setting up SSO (Single Sign-On) + +SSO allows your users to sign in using external identity providers like Google, Okta, Azure AD, etc. + +### Adding an SSO Provider + +1. Navigate to Configuration โ†’ Authentication โ†’ SSO Providers +2. Click "Add Provider" +3. Fill in the provider details: + +#### Required Fields + +- **Issuer URL**: The OIDC issuer URL (e.g., `https://accounts.google.com`) +- **Domain**: The email domain for this provider (e.g., `example.com`) +- **Provider ID**: A unique identifier for this provider (e.g., `google-sso`) +- **Client ID**: The OAuth client ID from your provider +- **Client Secret**: The OAuth client secret from your provider + +#### Auto-Discovery + +If your provider supports OIDC discovery, you can: +1. Enter the Issuer URL +2. Click "Discover" +3. The system will automatically fetch the authorization and token endpoints + +#### Manual Configuration + +For providers without discovery support, manually enter: +- **Authorization Endpoint**: The OAuth authorization URL +- **Token Endpoint**: The OAuth token exchange URL +- **JWKS Endpoint**: The JSON Web Key Set URL (optional) +- **UserInfo Endpoint**: The user information endpoint (optional) + +### Redirect URL + +When configuring your SSO provider, use this redirect URL: +``` +https://your-domain.com/api/auth/sso/callback/{provider-id} +``` + +Replace `{provider-id}` with your chosen Provider ID. + +### Example: Google SSO Setup + +1. Go to [Google Cloud Console](https://console.cloud.google.com/) +2. Create a new OAuth 2.0 Client ID +3. Add authorized redirect URI: `https://your-domain.com/api/auth/sso/callback/google-sso` +4. In Gitea Mirror: + - Issuer URL: `https://accounts.google.com` + - Domain: `your-company.com` + - Provider ID: `google-sso` + - Client ID: [Your Google Client ID] + - Client Secret: [Your Google Client Secret] + - Click "Discover" to auto-fill endpoints + +### Example: Okta SSO Setup + +1. In Okta Admin Console, create a new OIDC Web Application +2. Set redirect URI: `https://your-domain.com/api/auth/sso/callback/okta-sso` +3. In Gitea Mirror: + - Issuer URL: `https://your-okta-domain.okta.com` + - Domain: `your-company.com` + - Provider ID: `okta-sso` + - Client ID: [Your Okta Client ID] + - Client Secret: [Your Okta Client Secret] + - Click "Discover" to auto-fill endpoints + +## Setting up OIDC Provider + +The OIDC Provider feature allows other applications to use Gitea Mirror as their authentication provider. + +### Creating OAuth Applications + +1. Navigate to Configuration โ†’ Authentication โ†’ OAuth Applications +2. Click "Create Application" +3. Fill in the application details: + - **Application Name**: Display name for the application + - **Application Type**: Web, Mobile, or Desktop + - **Redirect URLs**: One or more redirect URLs (one per line) + +4. After creation, you'll receive: + - **Client ID**: Share this with the application + - **Client Secret**: Keep this secure and share only once + +### OIDC Endpoints + +Applications can use these standard OIDC endpoints: + +- **Discovery**: `https://your-domain.com/.well-known/openid-configuration` +- **Authorization**: `https://your-domain.com/api/auth/oauth2/authorize` +- **Token**: `https://your-domain.com/api/auth/oauth2/token` +- **UserInfo**: `https://your-domain.com/api/auth/oauth2/userinfo` +- **JWKS**: `https://your-domain.com/api/auth/jwks` + +### Supported Scopes + +- `openid` - Required, provides user ID +- `profile` - User's name, username, and profile picture +- `email` - User's email address and verification status + +### Example: Configuring Another Application + +For an application to use Gitea Mirror as its OIDC provider: + +```javascript +// Example configuration for another app +const oidcConfig = { + issuer: 'https://gitea-mirror.example.com', + clientId: 'client_xxxxxxxxxxxxx', + clientSecret: 'secret_xxxxxxxxxxxxx', + redirectUri: 'https://myapp.com/auth/callback', + scope: 'openid profile email' +}; +``` + +## User Experience + +### Logging In with SSO + +When SSO is configured: + +1. Users see tabs for "Email" and "SSO" on the login page +2. In the SSO tab, they can: + - Click a specific provider button (if configured) + - Enter their work email to be redirected to the appropriate provider + +### OAuth Consent Flow + +When an application requests authentication: + +1. Users are redirected to Gitea Mirror +2. If not logged in, they authenticate first +3. They see a consent screen showing: + - Application name + - Requested permissions + - Option to approve or deny + +## Security Considerations + +1. **Client Secrets**: Store OAuth client secrets securely +2. **Redirect URLs**: Only add trusted redirect URLs for applications +3. **Scopes**: Applications only receive the data for approved scopes +4. **Token Security**: Access tokens expire and can be revoked + +## Troubleshooting + +### SSO Login Issues + +1. **"Invalid origin" error**: Check that your Gitea Mirror URL matches the configured redirect URI +2. **"Provider not found" error**: Ensure the provider is properly configured and enabled +3. **Redirect loop**: Verify the redirect URI in both Gitea Mirror and the SSO provider match exactly + +### OIDC Provider Issues + +1. **Application not found**: Ensure the client ID is correct +2. **Invalid redirect URI**: The redirect URI must match exactly what's configured +3. **Consent not working**: Check browser cookies are enabled + +## Managing Access + +### Revoking SSO Access + +Currently, SSO sessions are managed through the identity provider. To revoke access: +1. Log out of Gitea Mirror +2. Revoke access in your identity provider's settings + +### Disabling OAuth Applications + +To disable an application: +1. Go to Configuration โ†’ Authentication โ†’ OAuth Applications +2. Find the application +3. Click the delete button + +This immediately prevents the application from authenticating new users. + +## Best Practices + +1. **Use HTTPS**: Always use HTTPS in production for security +2. **Regular Audits**: Periodically review configured SSO providers and OAuth applications +3. **Principle of Least Privilege**: Only grant necessary scopes to applications +4. **Monitor Usage**: Keep track of which applications are accessing your OIDC provider +5. **Secure Storage**: Store client secrets in a secure location, never in code + +## Migration Notes + +If migrating from the previous JWT-based authentication: +- Existing users remain unaffected +- Users can continue using email/password authentication +- SSO can be added as an additional authentication method \ No newline at end of file diff --git a/drizzle.config.ts b/drizzle.config.ts new file mode 100644 index 0000000..ec27a84 --- /dev/null +++ b/drizzle.config.ts @@ -0,0 +1,16 @@ +import { defineConfig } from "drizzle-kit"; + +export default defineConfig({ + dialect: "sqlite", + schema: "./src/lib/db/schema.ts", + out: "./drizzle", + dbCredentials: { + url: "./data/gitea-mirror.db", + }, + verbose: true, + strict: true, + migrations: { + table: "__drizzle_migrations", + schema: "main", + }, +}); \ No newline at end of file diff --git a/drizzle/0000_init.sql b/drizzle/0000_init.sql new file mode 100644 index 0000000..99809c7 --- /dev/null +++ b/drizzle/0000_init.sql @@ -0,0 +1,180 @@ +CREATE TABLE `accounts` ( + `id` text PRIMARY KEY NOT NULL, + `account_id` text NOT NULL, + `user_id` text NOT NULL, + `provider_id` text NOT NULL, + `provider_user_id` text, + `access_token` text, + `refresh_token` text, + `expires_at` integer, + `password` text, + `created_at` integer DEFAULT (unixepoch()) NOT NULL, + `updated_at` integer DEFAULT (unixepoch()) NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action +); +--> statement-breakpoint +CREATE INDEX `idx_accounts_account_id` ON `accounts` (`account_id`);--> statement-breakpoint +CREATE INDEX `idx_accounts_user_id` ON `accounts` (`user_id`);--> statement-breakpoint +CREATE INDEX `idx_accounts_provider` ON `accounts` (`provider_id`,`provider_user_id`);--> statement-breakpoint +CREATE TABLE `configs` ( + `id` text PRIMARY KEY NOT NULL, + `user_id` text NOT NULL, + `name` text NOT NULL, + `is_active` integer DEFAULT true NOT NULL, + `github_config` text NOT NULL, + `gitea_config` text NOT NULL, + `include` text DEFAULT '["*"]' NOT NULL, + `exclude` text DEFAULT '[]' NOT NULL, + `schedule_config` text NOT NULL, + `cleanup_config` text NOT NULL, + `created_at` integer DEFAULT (unixepoch()) NOT NULL, + `updated_at` integer DEFAULT (unixepoch()) NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action +); +--> statement-breakpoint +CREATE TABLE `events` ( + `id` text PRIMARY KEY NOT NULL, + `user_id` text NOT NULL, + `channel` text NOT NULL, + `payload` text NOT NULL, + `read` integer DEFAULT false NOT NULL, + `created_at` integer DEFAULT (unixepoch()) NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action +); +--> statement-breakpoint +CREATE INDEX `idx_events_user_channel` ON `events` (`user_id`,`channel`);--> statement-breakpoint +CREATE INDEX `idx_events_created_at` ON `events` (`created_at`);--> statement-breakpoint +CREATE INDEX `idx_events_read` ON `events` (`read`);--> statement-breakpoint +CREATE TABLE `mirror_jobs` ( + `id` text PRIMARY KEY NOT NULL, + `user_id` text NOT NULL, + `repository_id` text, + `repository_name` text, + `organization_id` text, + `organization_name` text, + `details` text, + `status` text DEFAULT 'imported' NOT NULL, + `message` text NOT NULL, + `timestamp` integer DEFAULT (unixepoch()) NOT NULL, + `job_type` text DEFAULT 'mirror' NOT NULL, + `batch_id` text, + `total_items` integer, + `completed_items` integer DEFAULT 0, + `item_ids` text, + `completed_item_ids` text DEFAULT '[]', + `in_progress` integer DEFAULT false NOT NULL, + `started_at` integer, + `completed_at` integer, + `last_checkpoint` integer, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action +); +--> statement-breakpoint +CREATE INDEX `idx_mirror_jobs_user_id` ON `mirror_jobs` (`user_id`);--> statement-breakpoint +CREATE INDEX `idx_mirror_jobs_batch_id` ON `mirror_jobs` (`batch_id`);--> statement-breakpoint +CREATE INDEX `idx_mirror_jobs_in_progress` ON `mirror_jobs` (`in_progress`);--> statement-breakpoint +CREATE INDEX `idx_mirror_jobs_job_type` ON `mirror_jobs` (`job_type`);--> statement-breakpoint +CREATE INDEX `idx_mirror_jobs_timestamp` ON `mirror_jobs` (`timestamp`);--> statement-breakpoint +CREATE TABLE `organizations` ( + `id` text PRIMARY KEY NOT NULL, + `user_id` text NOT NULL, + `config_id` text NOT NULL, + `name` text NOT NULL, + `avatar_url` text NOT NULL, + `membership_role` text DEFAULT 'member' NOT NULL, + `is_included` integer DEFAULT true NOT NULL, + `destination_org` text, + `status` text DEFAULT 'imported' NOT NULL, + `last_mirrored` integer, + `error_message` text, + `repository_count` integer DEFAULT 0 NOT NULL, + `created_at` integer DEFAULT (unixepoch()) NOT NULL, + `updated_at` integer DEFAULT (unixepoch()) NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action, + FOREIGN KEY (`config_id`) REFERENCES `configs`(`id`) ON UPDATE no action ON DELETE no action +); +--> statement-breakpoint +CREATE INDEX `idx_organizations_user_id` ON `organizations` (`user_id`);--> statement-breakpoint +CREATE INDEX `idx_organizations_config_id` ON `organizations` (`config_id`);--> statement-breakpoint +CREATE INDEX `idx_organizations_status` ON `organizations` (`status`);--> statement-breakpoint +CREATE INDEX `idx_organizations_is_included` ON `organizations` (`is_included`);--> statement-breakpoint +CREATE TABLE `repositories` ( + `id` text PRIMARY KEY NOT NULL, + `user_id` text NOT NULL, + `config_id` text NOT NULL, + `name` text NOT NULL, + `full_name` text NOT NULL, + `url` text NOT NULL, + `clone_url` text NOT NULL, + `owner` text NOT NULL, + `organization` text, + `mirrored_location` text DEFAULT '', + `is_private` integer DEFAULT false NOT NULL, + `is_fork` integer DEFAULT false NOT NULL, + `forked_from` text, + `has_issues` integer DEFAULT false NOT NULL, + `is_starred` integer DEFAULT false NOT NULL, + `is_archived` integer DEFAULT false NOT NULL, + `size` integer DEFAULT 0 NOT NULL, + `has_lfs` integer DEFAULT false NOT NULL, + `has_submodules` integer DEFAULT false NOT NULL, + `language` text, + `description` text, + `default_branch` text NOT NULL, + `visibility` text DEFAULT 'public' NOT NULL, + `status` text DEFAULT 'imported' NOT NULL, + `last_mirrored` integer, + `error_message` text, + `destination_org` text, + `created_at` integer DEFAULT (unixepoch()) NOT NULL, + `updated_at` integer DEFAULT (unixepoch()) NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action, + FOREIGN KEY (`config_id`) REFERENCES `configs`(`id`) ON UPDATE no action ON DELETE no action +); +--> statement-breakpoint +CREATE INDEX `idx_repositories_user_id` ON `repositories` (`user_id`);--> statement-breakpoint +CREATE INDEX `idx_repositories_config_id` ON `repositories` (`config_id`);--> statement-breakpoint +CREATE INDEX `idx_repositories_status` ON `repositories` (`status`);--> statement-breakpoint +CREATE INDEX `idx_repositories_owner` ON `repositories` (`owner`);--> statement-breakpoint +CREATE INDEX `idx_repositories_organization` ON `repositories` (`organization`);--> statement-breakpoint +CREATE INDEX `idx_repositories_is_fork` ON `repositories` (`is_fork`);--> statement-breakpoint +CREATE INDEX `idx_repositories_is_starred` ON `repositories` (`is_starred`);--> statement-breakpoint +CREATE TABLE `sessions` ( + `id` text PRIMARY KEY NOT NULL, + `token` text NOT NULL, + `user_id` text NOT NULL, + `expires_at` integer NOT NULL, + `ip_address` text, + `user_agent` text, + `created_at` integer DEFAULT (unixepoch()) NOT NULL, + `updated_at` integer DEFAULT (unixepoch()) NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action +); +--> statement-breakpoint +CREATE UNIQUE INDEX `sessions_token_unique` ON `sessions` (`token`);--> statement-breakpoint +CREATE INDEX `idx_sessions_user_id` ON `sessions` (`user_id`);--> statement-breakpoint +CREATE INDEX `idx_sessions_token` ON `sessions` (`token`);--> statement-breakpoint +CREATE INDEX `idx_sessions_expires_at` ON `sessions` (`expires_at`);--> statement-breakpoint +CREATE TABLE `users` ( + `id` text PRIMARY KEY NOT NULL, + `name` text, + `email` text NOT NULL, + `email_verified` integer DEFAULT false NOT NULL, + `image` text, + `created_at` integer DEFAULT (unixepoch()) NOT NULL, + `updated_at` integer DEFAULT (unixepoch()) NOT NULL, + `username` text +); +--> statement-breakpoint +CREATE UNIQUE INDEX `users_email_unique` ON `users` (`email`);--> statement-breakpoint +CREATE TABLE `verification_tokens` ( + `id` text PRIMARY KEY NOT NULL, + `token` text NOT NULL, + `identifier` text NOT NULL, + `type` text NOT NULL, + `expires_at` integer NOT NULL, + `created_at` integer DEFAULT (unixepoch()) NOT NULL +); +--> statement-breakpoint +CREATE UNIQUE INDEX `verification_tokens_token_unique` ON `verification_tokens` (`token`);--> statement-breakpoint +CREATE INDEX `idx_verification_tokens_token` ON `verification_tokens` (`token`);--> statement-breakpoint +CREATE INDEX `idx_verification_tokens_identifier` ON `verification_tokens` (`identifier`); \ No newline at end of file diff --git a/drizzle/0001_polite_exodus.sql b/drizzle/0001_polite_exodus.sql new file mode 100644 index 0000000..5204a57 --- /dev/null +++ b/drizzle/0001_polite_exodus.sql @@ -0,0 +1,64 @@ +CREATE TABLE `oauth_access_tokens` ( + `id` text PRIMARY KEY NOT NULL, + `access_token` text NOT NULL, + `refresh_token` text, + `access_token_expires_at` integer NOT NULL, + `refresh_token_expires_at` integer, + `client_id` text NOT NULL, + `user_id` text NOT NULL, + `scopes` text NOT NULL, + `created_at` integer DEFAULT (unixepoch()) NOT NULL, + `updated_at` integer DEFAULT (unixepoch()) NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action +); +--> statement-breakpoint +CREATE INDEX `idx_oauth_access_tokens_access_token` ON `oauth_access_tokens` (`access_token`);--> statement-breakpoint +CREATE INDEX `idx_oauth_access_tokens_user_id` ON `oauth_access_tokens` (`user_id`);--> statement-breakpoint +CREATE INDEX `idx_oauth_access_tokens_client_id` ON `oauth_access_tokens` (`client_id`);--> statement-breakpoint +CREATE TABLE `oauth_applications` ( + `id` text PRIMARY KEY NOT NULL, + `client_id` text NOT NULL, + `client_secret` text NOT NULL, + `name` text NOT NULL, + `redirect_urls` text NOT NULL, + `metadata` text, + `type` text NOT NULL, + `disabled` integer DEFAULT false NOT NULL, + `user_id` text, + `created_at` integer DEFAULT (unixepoch()) NOT NULL, + `updated_at` integer DEFAULT (unixepoch()) NOT NULL +); +--> statement-breakpoint +CREATE UNIQUE INDEX `oauth_applications_client_id_unique` ON `oauth_applications` (`client_id`);--> statement-breakpoint +CREATE INDEX `idx_oauth_applications_client_id` ON `oauth_applications` (`client_id`);--> statement-breakpoint +CREATE INDEX `idx_oauth_applications_user_id` ON `oauth_applications` (`user_id`);--> statement-breakpoint +CREATE TABLE `oauth_consent` ( + `id` text PRIMARY KEY NOT NULL, + `user_id` text NOT NULL, + `client_id` text NOT NULL, + `scopes` text NOT NULL, + `consent_given` integer NOT NULL, + `created_at` integer DEFAULT (unixepoch()) NOT NULL, + `updated_at` integer DEFAULT (unixepoch()) NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action +); +--> statement-breakpoint +CREATE INDEX `idx_oauth_consent_user_id` ON `oauth_consent` (`user_id`);--> statement-breakpoint +CREATE INDEX `idx_oauth_consent_client_id` ON `oauth_consent` (`client_id`);--> statement-breakpoint +CREATE INDEX `idx_oauth_consent_user_client` ON `oauth_consent` (`user_id`,`client_id`);--> statement-breakpoint +CREATE TABLE `sso_providers` ( + `id` text PRIMARY KEY NOT NULL, + `issuer` text NOT NULL, + `domain` text NOT NULL, + `oidc_config` text NOT NULL, + `user_id` text NOT NULL, + `provider_id` text NOT NULL, + `organization_id` text, + `created_at` integer DEFAULT (unixepoch()) NOT NULL, + `updated_at` integer DEFAULT (unixepoch()) NOT NULL +); +--> statement-breakpoint +CREATE UNIQUE INDEX `sso_providers_provider_id_unique` ON `sso_providers` (`provider_id`);--> statement-breakpoint +CREATE INDEX `idx_sso_providers_provider_id` ON `sso_providers` (`provider_id`);--> statement-breakpoint +CREATE INDEX `idx_sso_providers_domain` ON `sso_providers` (`domain`);--> statement-breakpoint +CREATE INDEX `idx_sso_providers_issuer` ON `sso_providers` (`issuer`); \ No newline at end of file diff --git a/drizzle/meta/0000_snapshot.json b/drizzle/meta/0000_snapshot.json new file mode 100644 index 0000000..231493d --- /dev/null +++ b/drizzle/meta/0000_snapshot.json @@ -0,0 +1,1290 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "7782b8ba-bdae-42e8-b8a7-614f8be30a58", + "prevId": "00000000-0000-0000-0000-000000000000", + "tables": { + "accounts": { + "name": "accounts", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_user_id": { + "name": "provider_user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "idx_accounts_account_id": { + "name": "idx_accounts_account_id", + "columns": [ + "account_id" + ], + "isUnique": false + }, + "idx_accounts_user_id": { + "name": "idx_accounts_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_accounts_provider": { + "name": "idx_accounts_provider", + "columns": [ + "provider_id", + "provider_user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "accounts_user_id_users_id_fk": { + "name": "accounts_user_id_users_id_fk", + "tableFrom": "accounts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "configs": { + "name": "configs", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_active": { + "name": "is_active", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "github_config": { + "name": "github_config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "gitea_config": { + "name": "gitea_config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "include": { + "name": "include", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[\"*\"]'" + }, + "exclude": { + "name": "exclude", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "schedule_config": { + "name": "schedule_config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cleanup_config": { + "name": "cleanup_config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": {}, + "foreignKeys": { + "configs_user_id_users_id_fk": { + "name": "configs_user_id_users_id_fk", + "tableFrom": "configs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "events": { + "name": "events", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "channel": { + "name": "channel", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "payload": { + "name": "payload", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "read": { + "name": "read", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "idx_events_user_channel": { + "name": "idx_events_user_channel", + "columns": [ + "user_id", + "channel" + ], + "isUnique": false + }, + "idx_events_created_at": { + "name": "idx_events_created_at", + "columns": [ + "created_at" + ], + "isUnique": false + }, + "idx_events_read": { + "name": "idx_events_read", + "columns": [ + "read" + ], + "isUnique": false + } + }, + "foreignKeys": { + "events_user_id_users_id_fk": { + "name": "events_user_id_users_id_fk", + "tableFrom": "events", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "mirror_jobs": { + "name": "mirror_jobs", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "repository_id": { + "name": "repository_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "repository_name": { + "name": "repository_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "organization_name": { + "name": "organization_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'imported'" + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "job_type": { + "name": "job_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'mirror'" + }, + "batch_id": { + "name": "batch_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "total_items": { + "name": "total_items", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "completed_items": { + "name": "completed_items", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "item_ids": { + "name": "item_ids", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "completed_item_ids": { + "name": "completed_item_ids", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'[]'" + }, + "in_progress": { + "name": "in_progress", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "completed_at": { + "name": "completed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_checkpoint": { + "name": "last_checkpoint", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_mirror_jobs_user_id": { + "name": "idx_mirror_jobs_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_mirror_jobs_batch_id": { + "name": "idx_mirror_jobs_batch_id", + "columns": [ + "batch_id" + ], + "isUnique": false + }, + "idx_mirror_jobs_in_progress": { + "name": "idx_mirror_jobs_in_progress", + "columns": [ + "in_progress" + ], + "isUnique": false + }, + "idx_mirror_jobs_job_type": { + "name": "idx_mirror_jobs_job_type", + "columns": [ + "job_type" + ], + "isUnique": false + }, + "idx_mirror_jobs_timestamp": { + "name": "idx_mirror_jobs_timestamp", + "columns": [ + "timestamp" + ], + "isUnique": false + } + }, + "foreignKeys": { + "mirror_jobs_user_id_users_id_fk": { + "name": "mirror_jobs_user_id_users_id_fk", + "tableFrom": "mirror_jobs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "organizations": { + "name": "organizations", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config_id": { + "name": "config_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "membership_role": { + "name": "membership_role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'member'" + }, + "is_included": { + "name": "is_included", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "destination_org": { + "name": "destination_org", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'imported'" + }, + "last_mirrored": { + "name": "last_mirrored", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "repository_count": { + "name": "repository_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "idx_organizations_user_id": { + "name": "idx_organizations_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_organizations_config_id": { + "name": "idx_organizations_config_id", + "columns": [ + "config_id" + ], + "isUnique": false + }, + "idx_organizations_status": { + "name": "idx_organizations_status", + "columns": [ + "status" + ], + "isUnique": false + }, + "idx_organizations_is_included": { + "name": "idx_organizations_is_included", + "columns": [ + "is_included" + ], + "isUnique": false + } + }, + "foreignKeys": { + "organizations_user_id_users_id_fk": { + "name": "organizations_user_id_users_id_fk", + "tableFrom": "organizations", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "organizations_config_id_configs_id_fk": { + "name": "organizations_config_id_configs_id_fk", + "tableFrom": "organizations", + "tableTo": "configs", + "columnsFrom": [ + "config_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "repositories": { + "name": "repositories", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config_id": { + "name": "config_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "full_name": { + "name": "full_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "clone_url": { + "name": "clone_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner": { + "name": "owner", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "organization": { + "name": "organization", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mirrored_location": { + "name": "mirrored_location", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "''" + }, + "is_private": { + "name": "is_private", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "is_fork": { + "name": "is_fork", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "forked_from": { + "name": "forked_from", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "has_issues": { + "name": "has_issues", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "is_starred": { + "name": "is_starred", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "is_archived": { + "name": "is_archived", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "has_lfs": { + "name": "has_lfs", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "has_submodules": { + "name": "has_submodules", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "default_branch": { + "name": "default_branch", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "visibility": { + "name": "visibility", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'public'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'imported'" + }, + "last_mirrored": { + "name": "last_mirrored", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "destination_org": { + "name": "destination_org", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "idx_repositories_user_id": { + "name": "idx_repositories_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_repositories_config_id": { + "name": "idx_repositories_config_id", + "columns": [ + "config_id" + ], + "isUnique": false + }, + "idx_repositories_status": { + "name": "idx_repositories_status", + "columns": [ + "status" + ], + "isUnique": false + }, + "idx_repositories_owner": { + "name": "idx_repositories_owner", + "columns": [ + "owner" + ], + "isUnique": false + }, + "idx_repositories_organization": { + "name": "idx_repositories_organization", + "columns": [ + "organization" + ], + "isUnique": false + }, + "idx_repositories_is_fork": { + "name": "idx_repositories_is_fork", + "columns": [ + "is_fork" + ], + "isUnique": false + }, + "idx_repositories_is_starred": { + "name": "idx_repositories_is_starred", + "columns": [ + "is_starred" + ], + "isUnique": false + } + }, + "foreignKeys": { + "repositories_user_id_users_id_fk": { + "name": "repositories_user_id_users_id_fk", + "tableFrom": "repositories", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "repositories_config_id_configs_id_fk": { + "name": "repositories_config_id_configs_id_fk", + "tableFrom": "repositories", + "tableTo": "configs", + "columnsFrom": [ + "config_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sessions": { + "name": "sessions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "sessions_token_unique": { + "name": "sessions_token_unique", + "columns": [ + "token" + ], + "isUnique": true + }, + "idx_sessions_user_id": { + "name": "idx_sessions_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_sessions_token": { + "name": "idx_sessions_token", + "columns": [ + "token" + ], + "isUnique": false + }, + "idx_sessions_expires_at": { + "name": "idx_sessions_expires_at", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email_verified": { + "name": "email_verified", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "users_email_unique": { + "name": "users_email_unique", + "columns": [ + "email" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "verification_tokens": { + "name": "verification_tokens", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "verification_tokens_token_unique": { + "name": "verification_tokens_token_unique", + "columns": [ + "token" + ], + "isUnique": true + }, + "idx_verification_tokens_token": { + "name": "idx_verification_tokens_token", + "columns": [ + "token" + ], + "isUnique": false + }, + "idx_verification_tokens_identifier": { + "name": "idx_verification_tokens_identifier", + "columns": [ + "identifier" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/0001_snapshot.json b/drizzle/meta/0001_snapshot.json new file mode 100644 index 0000000..92545f6 --- /dev/null +++ b/drizzle/meta/0001_snapshot.json @@ -0,0 +1,1722 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "4e9ce026-e4e3-4a68-a7f2-37ac7747e2a3", + "prevId": "7782b8ba-bdae-42e8-b8a7-614f8be30a58", + "tables": { + "accounts": { + "name": "accounts", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_user_id": { + "name": "provider_user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "idx_accounts_account_id": { + "name": "idx_accounts_account_id", + "columns": [ + "account_id" + ], + "isUnique": false + }, + "idx_accounts_user_id": { + "name": "idx_accounts_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_accounts_provider": { + "name": "idx_accounts_provider", + "columns": [ + "provider_id", + "provider_user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "accounts_user_id_users_id_fk": { + "name": "accounts_user_id_users_id_fk", + "tableFrom": "accounts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "configs": { + "name": "configs", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_active": { + "name": "is_active", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "github_config": { + "name": "github_config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "gitea_config": { + "name": "gitea_config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "include": { + "name": "include", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[\"*\"]'" + }, + "exclude": { + "name": "exclude", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "schedule_config": { + "name": "schedule_config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cleanup_config": { + "name": "cleanup_config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": {}, + "foreignKeys": { + "configs_user_id_users_id_fk": { + "name": "configs_user_id_users_id_fk", + "tableFrom": "configs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "events": { + "name": "events", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "channel": { + "name": "channel", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "payload": { + "name": "payload", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "read": { + "name": "read", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "idx_events_user_channel": { + "name": "idx_events_user_channel", + "columns": [ + "user_id", + "channel" + ], + "isUnique": false + }, + "idx_events_created_at": { + "name": "idx_events_created_at", + "columns": [ + "created_at" + ], + "isUnique": false + }, + "idx_events_read": { + "name": "idx_events_read", + "columns": [ + "read" + ], + "isUnique": false + } + }, + "foreignKeys": { + "events_user_id_users_id_fk": { + "name": "events_user_id_users_id_fk", + "tableFrom": "events", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "mirror_jobs": { + "name": "mirror_jobs", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "repository_id": { + "name": "repository_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "repository_name": { + "name": "repository_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "organization_name": { + "name": "organization_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'imported'" + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "job_type": { + "name": "job_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'mirror'" + }, + "batch_id": { + "name": "batch_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "total_items": { + "name": "total_items", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "completed_items": { + "name": "completed_items", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "item_ids": { + "name": "item_ids", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "completed_item_ids": { + "name": "completed_item_ids", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'[]'" + }, + "in_progress": { + "name": "in_progress", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "completed_at": { + "name": "completed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_checkpoint": { + "name": "last_checkpoint", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_mirror_jobs_user_id": { + "name": "idx_mirror_jobs_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_mirror_jobs_batch_id": { + "name": "idx_mirror_jobs_batch_id", + "columns": [ + "batch_id" + ], + "isUnique": false + }, + "idx_mirror_jobs_in_progress": { + "name": "idx_mirror_jobs_in_progress", + "columns": [ + "in_progress" + ], + "isUnique": false + }, + "idx_mirror_jobs_job_type": { + "name": "idx_mirror_jobs_job_type", + "columns": [ + "job_type" + ], + "isUnique": false + }, + "idx_mirror_jobs_timestamp": { + "name": "idx_mirror_jobs_timestamp", + "columns": [ + "timestamp" + ], + "isUnique": false + } + }, + "foreignKeys": { + "mirror_jobs_user_id_users_id_fk": { + "name": "mirror_jobs_user_id_users_id_fk", + "tableFrom": "mirror_jobs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "oauth_access_tokens": { + "name": "oauth_access_tokens", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "idx_oauth_access_tokens_access_token": { + "name": "idx_oauth_access_tokens_access_token", + "columns": [ + "access_token" + ], + "isUnique": false + }, + "idx_oauth_access_tokens_user_id": { + "name": "idx_oauth_access_tokens_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_oauth_access_tokens_client_id": { + "name": "idx_oauth_access_tokens_client_id", + "columns": [ + "client_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "oauth_access_tokens_user_id_users_id_fk": { + "name": "oauth_access_tokens_user_id_users_id_fk", + "tableFrom": "oauth_access_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "oauth_applications": { + "name": "oauth_applications", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "redirect_urls": { + "name": "redirect_urls", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "disabled": { + "name": "disabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "oauth_applications_client_id_unique": { + "name": "oauth_applications_client_id_unique", + "columns": [ + "client_id" + ], + "isUnique": true + }, + "idx_oauth_applications_client_id": { + "name": "idx_oauth_applications_client_id", + "columns": [ + "client_id" + ], + "isUnique": false + }, + "idx_oauth_applications_user_id": { + "name": "idx_oauth_applications_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "oauth_consent": { + "name": "oauth_consent", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "consent_given": { + "name": "consent_given", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "idx_oauth_consent_user_id": { + "name": "idx_oauth_consent_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_oauth_consent_client_id": { + "name": "idx_oauth_consent_client_id", + "columns": [ + "client_id" + ], + "isUnique": false + }, + "idx_oauth_consent_user_client": { + "name": "idx_oauth_consent_user_client", + "columns": [ + "user_id", + "client_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "oauth_consent_user_id_users_id_fk": { + "name": "oauth_consent_user_id_users_id_fk", + "tableFrom": "oauth_consent", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "organizations": { + "name": "organizations", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config_id": { + "name": "config_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "membership_role": { + "name": "membership_role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'member'" + }, + "is_included": { + "name": "is_included", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "destination_org": { + "name": "destination_org", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'imported'" + }, + "last_mirrored": { + "name": "last_mirrored", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "repository_count": { + "name": "repository_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "idx_organizations_user_id": { + "name": "idx_organizations_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_organizations_config_id": { + "name": "idx_organizations_config_id", + "columns": [ + "config_id" + ], + "isUnique": false + }, + "idx_organizations_status": { + "name": "idx_organizations_status", + "columns": [ + "status" + ], + "isUnique": false + }, + "idx_organizations_is_included": { + "name": "idx_organizations_is_included", + "columns": [ + "is_included" + ], + "isUnique": false + } + }, + "foreignKeys": { + "organizations_user_id_users_id_fk": { + "name": "organizations_user_id_users_id_fk", + "tableFrom": "organizations", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "organizations_config_id_configs_id_fk": { + "name": "organizations_config_id_configs_id_fk", + "tableFrom": "organizations", + "tableTo": "configs", + "columnsFrom": [ + "config_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "repositories": { + "name": "repositories", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config_id": { + "name": "config_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "full_name": { + "name": "full_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "clone_url": { + "name": "clone_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner": { + "name": "owner", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "organization": { + "name": "organization", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mirrored_location": { + "name": "mirrored_location", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "''" + }, + "is_private": { + "name": "is_private", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "is_fork": { + "name": "is_fork", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "forked_from": { + "name": "forked_from", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "has_issues": { + "name": "has_issues", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "is_starred": { + "name": "is_starred", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "is_archived": { + "name": "is_archived", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "has_lfs": { + "name": "has_lfs", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "has_submodules": { + "name": "has_submodules", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "default_branch": { + "name": "default_branch", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "visibility": { + "name": "visibility", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'public'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'imported'" + }, + "last_mirrored": { + "name": "last_mirrored", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "destination_org": { + "name": "destination_org", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "idx_repositories_user_id": { + "name": "idx_repositories_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_repositories_config_id": { + "name": "idx_repositories_config_id", + "columns": [ + "config_id" + ], + "isUnique": false + }, + "idx_repositories_status": { + "name": "idx_repositories_status", + "columns": [ + "status" + ], + "isUnique": false + }, + "idx_repositories_owner": { + "name": "idx_repositories_owner", + "columns": [ + "owner" + ], + "isUnique": false + }, + "idx_repositories_organization": { + "name": "idx_repositories_organization", + "columns": [ + "organization" + ], + "isUnique": false + }, + "idx_repositories_is_fork": { + "name": "idx_repositories_is_fork", + "columns": [ + "is_fork" + ], + "isUnique": false + }, + "idx_repositories_is_starred": { + "name": "idx_repositories_is_starred", + "columns": [ + "is_starred" + ], + "isUnique": false + } + }, + "foreignKeys": { + "repositories_user_id_users_id_fk": { + "name": "repositories_user_id_users_id_fk", + "tableFrom": "repositories", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "repositories_config_id_configs_id_fk": { + "name": "repositories_config_id_configs_id_fk", + "tableFrom": "repositories", + "tableTo": "configs", + "columnsFrom": [ + "config_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sessions": { + "name": "sessions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "sessions_token_unique": { + "name": "sessions_token_unique", + "columns": [ + "token" + ], + "isUnique": true + }, + "idx_sessions_user_id": { + "name": "idx_sessions_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_sessions_token": { + "name": "idx_sessions_token", + "columns": [ + "token" + ], + "isUnique": false + }, + "idx_sessions_expires_at": { + "name": "idx_sessions_expires_at", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sso_providers": { + "name": "sso_providers", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "oidc_config": { + "name": "oidc_config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "sso_providers_provider_id_unique": { + "name": "sso_providers_provider_id_unique", + "columns": [ + "provider_id" + ], + "isUnique": true + }, + "idx_sso_providers_provider_id": { + "name": "idx_sso_providers_provider_id", + "columns": [ + "provider_id" + ], + "isUnique": false + }, + "idx_sso_providers_domain": { + "name": "idx_sso_providers_domain", + "columns": [ + "domain" + ], + "isUnique": false + }, + "idx_sso_providers_issuer": { + "name": "idx_sso_providers_issuer", + "columns": [ + "issuer" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email_verified": { + "name": "email_verified", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "users_email_unique": { + "name": "users_email_unique", + "columns": [ + "email" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "verification_tokens": { + "name": "verification_tokens", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "verification_tokens_token_unique": { + "name": "verification_tokens_token_unique", + "columns": [ + "token" + ], + "isUnique": true + }, + "idx_verification_tokens_token": { + "name": "idx_verification_tokens_token", + "columns": [ + "token" + ], + "isUnique": false + }, + "idx_verification_tokens_identifier": { + "name": "idx_verification_tokens_identifier", + "columns": [ + "identifier" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json new file mode 100644 index 0000000..300aa73 --- /dev/null +++ b/drizzle/meta/_journal.json @@ -0,0 +1,20 @@ +{ + "version": "7", + "dialect": "sqlite", + "entries": [ + { + "idx": 0, + "version": "6", + "when": 1752171873627, + "tag": "0000_init", + "breakpoints": true + }, + { + "idx": 1, + "version": "6", + "when": 1752173351102, + "tag": "0001_polite_exodus", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/env.d.ts b/env.d.ts new file mode 100644 index 0000000..1369e80 --- /dev/null +++ b/env.d.ts @@ -0,0 +1,9 @@ +/// +/// + +declare namespace App { + interface Locals { + user: import("better-auth").User | null; + session: import("better-auth").Session | null; + } +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..f08d6d0 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,9087 @@ +{ + "name": "gitea-mirror", + "version": "2.22.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "gitea-mirror", + "version": "2.22.0", + "dependencies": { + "@astrojs/check": "^0.9.4", + "@astrojs/mdx": "^4.3.0", + "@astrojs/node": "9.3.0", + "@astrojs/react": "^4.3.0", + "@octokit/rest": "^22.0.0", + "@radix-ui/react-accordion": "^1.2.11", + "@radix-ui/react-avatar": "^1.1.10", + "@radix-ui/react-checkbox": "^1.3.2", + "@radix-ui/react-collapsible": "^1.1.11", + "@radix-ui/react-dialog": "^1.1.14", + "@radix-ui/react-dropdown-menu": "^2.1.15", + "@radix-ui/react-hover-card": "^1.1.14", + "@radix-ui/react-label": "^2.1.7", + "@radix-ui/react-popover": "^1.1.14", + "@radix-ui/react-radio-group": "^1.3.7", + "@radix-ui/react-scroll-area": "^1.2.9", + "@radix-ui/react-select": "^2.2.5", + "@radix-ui/react-separator": "^1.1.7", + "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-switch": "^1.2.5", + "@radix-ui/react-tabs": "^1.1.12", + "@radix-ui/react-tooltip": "^1.2.7", + "@tailwindcss/vite": "^4.1.11", + "@tanstack/react-virtual": "^3.13.12", + "@types/canvas-confetti": "^1.9.0", + "@types/react": "^19.1.8", + "@types/react-dom": "^19.1.6", + "astro": "5.11.0", + "bcryptjs": "^3.0.2", + "better-auth": "^1.2.12", + "canvas-confetti": "^1.9.3", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "cmdk": "^1.1.1", + "drizzle-orm": "^0.44.2", + "fuse.js": "^7.1.0", + "jsonwebtoken": "^9.0.2", + "lucide-react": "^0.525.0", + "next-themes": "^0.4.6", + "react": "^19.1.0", + "react-dom": "^19.1.0", + "react-icons": "^5.5.0", + "sonner": "^2.0.5", + "tailwind-merge": "^3.3.1", + "tailwindcss": "^4.1.11", + "tw-animate-css": "^1.3.5", + "typescript": "^5.8.3", + "uuid": "^11.1.0", + "vaul": "^1.1.2", + "zod": "^3.25.75" + }, + "devDependencies": { + "@testing-library/jest-dom": "^6.6.3", + "@testing-library/react": "^16.3.0", + "@types/bcryptjs": "^3.0.0", + "@types/bun": "^1.2.18", + "@types/jsonwebtoken": "^9.0.10", + "@types/uuid": "^10.0.0", + "@vitejs/plugin-react": "^4.6.0", + "drizzle-kit": "^0.31.4", + "jsdom": "^26.1.0", + "tsx": "^4.20.3", + "vitest": "^3.2.4" + }, + "engines": { + "bun": ">=1.2.9" + } + }, + "../gitea-mirror-hosted": { + "name": "@gitea-mirror/hosted", + "version": "1.0.0", + "extraneous": true, + "dependencies": { + "@polar-sh/better-auth": "^1.0.6", + "@polar-sh/sdk": "^0.34.5", + "arctic": "^2.0.0", + "drizzle-orm": "^0.44.0", + "ioredis": "^5.4.0", + "lucide-react": "^0.525.0", + "postgres": "^3.4.0" + }, + "devDependencies": { + "@types/node": "^20.0.0", + "@types/react": "^18.0.0", + "@typescript-eslint/eslint-plugin": "^7.0.0", + "@typescript-eslint/parser": "^7.0.0", + "eslint": "^8.0.0", + "typescript": "^5.0.0" + }, + "peerDependencies": { + "@tanstack/react-query": "*", + "astro": "*", + "better-auth": "*", + "drizzle-orm": "*", + "react": "*" + } + }, + "node_modules/@adobe/css-tools": { + "version": "4.4.3", + "dev": true, + "license": "MIT" + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@asamuzakjp/css-color": { + "version": "3.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.3", + "@csstools/css-color-parser": "^3.0.9", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "lru-cache": "^10.4.3" + } + }, + "node_modules/@astrojs/check": { + "version": "0.9.4", + "license": "MIT", + "dependencies": { + "@astrojs/language-server": "^2.15.0", + "chokidar": "^4.0.1", + "kleur": "^4.1.5", + "yargs": "^17.7.2" + }, + "bin": { + "astro-check": "dist/bin.js" + }, + "peerDependencies": { + "typescript": "^5.0.0" + } + }, + "node_modules/@astrojs/compiler": { + "version": "2.12.2", + "license": "MIT" + }, + "node_modules/@astrojs/internal-helpers": { + "version": "0.6.1", + "license": "MIT" + }, + "node_modules/@astrojs/language-server": { + "version": "2.15.4", + "license": "MIT", + "dependencies": { + "@astrojs/compiler": "^2.10.3", + "@astrojs/yaml2ts": "^0.2.2", + "@jridgewell/sourcemap-codec": "^1.4.15", + "@volar/kit": "~2.4.7", + "@volar/language-core": "~2.4.7", + "@volar/language-server": "~2.4.7", + "@volar/language-service": "~2.4.7", + "fast-glob": "^3.2.12", + "muggle-string": "^0.4.1", + "volar-service-css": "0.0.62", + "volar-service-emmet": "0.0.62", + "volar-service-html": "0.0.62", + "volar-service-prettier": "0.0.62", + "volar-service-typescript": "0.0.62", + "volar-service-typescript-twoslash-queries": "0.0.62", + "volar-service-yaml": "0.0.62", + "vscode-html-languageservice": "^5.2.0", + "vscode-uri": "^3.0.8" + }, + "bin": { + "astro-ls": "bin/nodeServer.js" + }, + "peerDependencies": { + "prettier": "^3.0.0", + "prettier-plugin-astro": ">=0.11.0" + }, + "peerDependenciesMeta": { + "prettier": { + "optional": true + }, + "prettier-plugin-astro": { + "optional": true + } + } + }, + "node_modules/@astrojs/markdown-remark": { + "version": "6.3.2", + "license": "MIT", + "dependencies": { + "@astrojs/internal-helpers": "0.6.1", + "@astrojs/prism": "3.3.0", + "github-slugger": "^2.0.0", + "hast-util-from-html": "^2.0.3", + "hast-util-to-text": "^4.0.2", + "import-meta-resolve": "^4.1.0", + "js-yaml": "^4.1.0", + "mdast-util-definitions": "^6.0.0", + "rehype-raw": "^7.0.0", + "rehype-stringify": "^10.0.1", + "remark-gfm": "^4.0.1", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.1.2", + "remark-smartypants": "^3.0.2", + "shiki": "^3.2.1", + "smol-toml": "^1.3.1", + "unified": "^11.0.5", + "unist-util-remove-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "unist-util-visit-parents": "^6.0.1", + "vfile": "^6.0.3" + } + }, + "node_modules/@astrojs/mdx": { + "version": "4.3.0", + "license": "MIT", + "dependencies": { + "@astrojs/markdown-remark": "6.3.2", + "@mdx-js/mdx": "^3.1.0", + "acorn": "^8.14.1", + "es-module-lexer": "^1.6.0", + "estree-util-visit": "^2.0.0", + "hast-util-to-html": "^9.0.5", + "kleur": "^4.1.5", + "rehype-raw": "^7.0.0", + "remark-gfm": "^4.0.1", + "remark-smartypants": "^3.0.2", + "source-map": "^0.7.4", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.3" + }, + "engines": { + "node": "18.20.8 || ^20.3.0 || >=22.0.0" + }, + "peerDependencies": { + "astro": "^5.0.0" + } + }, + "node_modules/@astrojs/node": { + "version": "9.3.0", + "license": "MIT", + "dependencies": { + "@astrojs/internal-helpers": "0.6.1", + "send": "^1.2.0", + "server-destroy": "^1.0.1" + }, + "peerDependencies": { + "astro": "^5.3.0" + } + }, + "node_modules/@astrojs/prism": { + "version": "3.3.0", + "license": "MIT", + "dependencies": { + "prismjs": "^1.30.0" + }, + "engines": { + "node": "18.20.8 || ^20.3.0 || >=22.0.0" + } + }, + "node_modules/@astrojs/react": { + "version": "4.3.0", + "license": "MIT", + "dependencies": { + "@vitejs/plugin-react": "^4.4.1", + "ultrahtml": "^1.6.0", + "vite": "^6.3.5" + }, + "engines": { + "node": "18.20.8 || ^20.3.0 || >=22.0.0" + }, + "peerDependencies": { + "@types/react": "^17.0.50 || ^18.0.21 || ^19.0.0", + "@types/react-dom": "^17.0.17 || ^18.0.6 || ^19.0.0", + "react": "^17.0.2 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.2 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@astrojs/react/node_modules/@vitejs/plugin-react": { + "version": "4.5.0", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.26.10", + "@babel/plugin-transform-react-jsx-self": "^7.25.9", + "@babel/plugin-transform-react-jsx-source": "^7.25.9", + "@rolldown/pluginutils": "1.0.0-beta.9", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0" + } + }, + "node_modules/@astrojs/react/node_modules/@vitejs/plugin-react/node_modules/@babel/core": { + "version": "7.27.3", + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.27.3", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.27.3", + "@babel/helpers": "^7.27.3", + "@babel/parser": "^7.27.3", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.27.3", + "@babel/types": "^7.27.3", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@astrojs/react/node_modules/@vitejs/plugin-react/node_modules/@babel/core/node_modules/@babel/helpers": { + "version": "7.27.3", + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@astrojs/react/node_modules/@vitejs/plugin-react/node_modules/@babel/core/node_modules/@babel/parser": { + "version": "7.27.3", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.3" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@astrojs/react/node_modules/@vitejs/plugin-react/node_modules/@babel/core/node_modules/@babel/traverse": { + "version": "7.27.3", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.27.3", + "@babel/parser": "^7.27.3", + "@babel/template": "^7.27.2", + "@babel/types": "^7.27.3", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@astrojs/react/node_modules/@vitejs/plugin-react/node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@astrojs/react/node_modules/@vitejs/plugin-react/node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.9", + "license": "MIT" + }, + "node_modules/@astrojs/telemetry": { + "version": "3.3.0", + "license": "MIT", + "dependencies": { + "ci-info": "^4.2.0", + "debug": "^4.4.0", + "dlv": "^1.1.3", + "dset": "^3.1.4", + "is-docker": "^3.0.0", + "is-wsl": "^3.1.0", + "which-pm-runs": "^1.1.0" + }, + "engines": { + "node": "18.20.8 || ^20.3.0 || >=22.0.0" + } + }, + "node_modules/@astrojs/yaml2ts": { + "version": "0.2.2", + "license": "MIT", + "dependencies": { + "yaml": "^2.5.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.27.3", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.27.4", + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.27.3", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.27.3", + "@babel/helpers": "^7.27.4", + "@babel/parser": "^7.27.4", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.27.4", + "@babel/types": "^7.27.3", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.27.3", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.27.3", + "@babel/types": "^7.27.3", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/generator/node_modules/@babel/parser": { + "version": "7.27.3", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.3" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { + "version": "5.1.1", + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache/node_modules/yallist": { + "version": "3.1.1", + "license": "ISC" + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports/node_modules/@babel/traverse": { + "version": "7.27.3", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.27.3", + "@babel/parser": "^7.27.3", + "@babel/template": "^7.27.2", + "@babel/types": "^7.27.3", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports/node_modules/@babel/traverse/node_modules/@babel/parser": { + "version": "7.27.3", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.3" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.27.3", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-module-transforms/node_modules/@babel/traverse": { + "version": "7.27.3", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.27.3", + "@babel/parser": "^7.27.3", + "@babel/template": "^7.27.2", + "@babel/types": "^7.27.3", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms/node_modules/@babel/traverse/node_modules/@babel/parser": { + "version": "7.27.3", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.3" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.27.6", + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.27.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers/node_modules/@babel/types": { + "version": "7.27.6", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.27.5", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.3" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.27.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template/node_modules/@babel/parser": { + "version": "7.27.3", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.3" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.27.4", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.27.3", + "@babel/parser": "^7.27.4", + "@babel/template": "^7.27.2", + "@babel/types": "^7.27.3", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.27.3", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@better-auth/utils": { + "version": "0.2.5", + "license": "MIT", + "dependencies": { + "typescript": "^5.8.2", + "uncrypto": "^0.1.3" + } + }, + "node_modules/@better-fetch/fetch": { + "version": "1.1.18" + }, + "node_modules/@capsizecss/unpack": { + "version": "2.4.0", + "license": "MIT", + "dependencies": { + "blob-to-buffer": "^1.2.8", + "cross-fetch": "^3.0.4", + "fontkit": "^2.0.2" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "5.0.2", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.0.10", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.0.2", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@drizzle-team/brocli": { + "version": "0.10.2", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@emmetio/abbreviation": { + "version": "2.3.3", + "license": "MIT", + "dependencies": { + "@emmetio/scanner": "^1.0.4" + } + }, + "node_modules/@emmetio/css-abbreviation": { + "version": "2.1.8", + "license": "MIT", + "dependencies": { + "@emmetio/scanner": "^1.0.4" + } + }, + "node_modules/@emmetio/css-parser": { + "version": "0.4.0", + "license": "MIT", + "dependencies": { + "@emmetio/stream-reader": "^2.2.0", + "@emmetio/stream-reader-utils": "^0.1.0" + } + }, + "node_modules/@emmetio/html-matcher": { + "version": "1.3.0", + "license": "ISC", + "dependencies": { + "@emmetio/scanner": "^1.0.0" + } + }, + "node_modules/@emmetio/scanner": { + "version": "1.0.4", + "license": "MIT" + }, + "node_modules/@emmetio/stream-reader": { + "version": "2.2.0", + "license": "MIT" + }, + "node_modules/@emmetio/stream-reader-utils": { + "version": "0.1.0", + "license": "MIT" + }, + "node_modules/@esbuild-kit/core-utils": { + "version": "3.3.2", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.18.20", + "source-map-support": "^0.5.21" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/esbuild": { + "version": "0.18.20", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.18.20", + "@esbuild/android-arm64": "0.18.20", + "@esbuild/android-x64": "0.18.20", + "@esbuild/darwin-arm64": "0.18.20", + "@esbuild/darwin-x64": "0.18.20", + "@esbuild/freebsd-arm64": "0.18.20", + "@esbuild/freebsd-x64": "0.18.20", + "@esbuild/linux-arm": "0.18.20", + "@esbuild/linux-arm64": "0.18.20", + "@esbuild/linux-ia32": "0.18.20", + "@esbuild/linux-loong64": "0.18.20", + "@esbuild/linux-mips64el": "0.18.20", + "@esbuild/linux-ppc64": "0.18.20", + "@esbuild/linux-riscv64": "0.18.20", + "@esbuild/linux-s390x": "0.18.20", + "@esbuild/linux-x64": "0.18.20", + "@esbuild/netbsd-x64": "0.18.20", + "@esbuild/openbsd-x64": "0.18.20", + "@esbuild/sunos-x64": "0.18.20", + "@esbuild/win32-arm64": "0.18.20", + "@esbuild/win32-ia32": "0.18.20", + "@esbuild/win32-x64": "0.18.20" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/esbuild/node_modules/@esbuild/darwin-arm64": { + "version": "0.18.20", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/esm-loader": { + "version": "2.6.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@esbuild-kit/core-utils": "^3.3.2", + "get-tsconfig": "^4.7.0" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.5", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.7.0", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.9" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.0", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.0", + "@floating-ui/utils": "^0.2.9" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.2", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.9", + "license": "MIT" + }, + "node_modules/@hexagon/base64": { + "version": "1.1.28", + "license": "MIT" + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.33.5", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.0.4", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.8", + "license": "MIT", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@levischuck/tiny-cbor": { + "version": "0.2.11", + "license": "MIT" + }, + "node_modules/@mdx-js/mdx": { + "version": "3.1.0", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdx": "^2.0.0", + "collapse-white-space": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "estree-util-scope": "^1.0.0", + "estree-walker": "^3.0.0", + "hast-util-to-jsx-runtime": "^2.0.0", + "markdown-extensions": "^2.0.0", + "recma-build-jsx": "^1.0.0", + "recma-jsx": "^1.0.0", + "recma-stringify": "^1.0.0", + "rehype-recma": "^1.0.0", + "remark-mdx": "^3.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.0.0", + "source-map": "^0.7.0", + "unified": "^11.0.0", + "unist-util-position-from-estree": "^2.0.0", + "unist-util-stringify-position": "^4.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/@noble/ciphers": { + "version": "0.6.0", + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@octokit/auth-token": { + "version": "6.0.0", + "license": "MIT", + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/core": { + "version": "7.0.2", + "license": "MIT", + "dependencies": { + "@octokit/auth-token": "^6.0.0", + "@octokit/graphql": "^9.0.1", + "@octokit/request": "^10.0.2", + "@octokit/request-error": "^7.0.0", + "@octokit/types": "^14.0.0", + "before-after-hook": "^4.0.0", + "universal-user-agent": "^7.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/endpoint": { + "version": "11.0.0", + "license": "MIT", + "dependencies": { + "@octokit/types": "^14.0.0", + "universal-user-agent": "^7.0.2" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/graphql": { + "version": "9.0.1", + "license": "MIT", + "dependencies": { + "@octokit/request": "^10.0.2", + "@octokit/types": "^14.0.0", + "universal-user-agent": "^7.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/openapi-types": { + "version": "25.1.0", + "license": "MIT" + }, + "node_modules/@octokit/plugin-paginate-rest": { + "version": "13.0.1", + "license": "MIT", + "dependencies": { + "@octokit/types": "^14.1.0" + }, + "engines": { + "node": ">= 20" + }, + "peerDependencies": { + "@octokit/core": ">=6" + } + }, + "node_modules/@octokit/plugin-request-log": { + "version": "6.0.0", + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "peerDependencies": { + "@octokit/core": ">=6" + } + }, + "node_modules/@octokit/plugin-rest-endpoint-methods": { + "version": "16.0.0", + "license": "MIT", + "dependencies": { + "@octokit/types": "^14.1.0" + }, + "engines": { + "node": ">= 20" + }, + "peerDependencies": { + "@octokit/core": ">=6" + } + }, + "node_modules/@octokit/request": { + "version": "10.0.2", + "license": "MIT", + "dependencies": { + "@octokit/endpoint": "^11.0.0", + "@octokit/request-error": "^7.0.0", + "@octokit/types": "^14.0.0", + "fast-content-type-parse": "^3.0.0", + "universal-user-agent": "^7.0.2" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/request-error": { + "version": "7.0.0", + "license": "MIT", + "dependencies": { + "@octokit/types": "^14.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/rest": { + "version": "22.0.0", + "license": "MIT", + "dependencies": { + "@octokit/core": "^7.0.2", + "@octokit/plugin-paginate-rest": "^13.0.1", + "@octokit/plugin-request-log": "^6.0.0", + "@octokit/plugin-rest-endpoint-methods": "^16.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/types": { + "version": "14.1.0", + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^25.1.0" + } + }, + "node_modules/@oslojs/encoding": { + "version": "1.1.0", + "license": "MIT" + }, + "node_modules/@peculiar/asn1-android": { + "version": "2.3.16", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.3.15", + "asn1js": "^3.0.5", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-ecc": { + "version": "2.3.15", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.3.15", + "@peculiar/asn1-x509": "^2.3.15", + "asn1js": "^3.0.5", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-rsa": { + "version": "2.3.15", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.3.15", + "@peculiar/asn1-x509": "^2.3.15", + "asn1js": "^3.0.5", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-schema": { + "version": "2.3.15", + "license": "MIT", + "dependencies": { + "asn1js": "^3.0.5", + "pvtsutils": "^1.3.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-x509": { + "version": "2.3.15", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.3.15", + "asn1js": "^3.0.5", + "pvtsutils": "^1.3.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@radix-ui/number": { + "version": "1.1.1", + "license": "MIT" + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.2", + "license": "MIT" + }, + "node_modules/@radix-ui/react-accordion": { + "version": "1.2.11", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-collapsible": "1.1.11", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-arrow": { + "version": "1.1.7", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-avatar": { + "version": "1.1.10", + "license": "MIT", + "dependencies": { + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-is-hydrated": "0.1.0", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-checkbox": { + "version": "1.3.2", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collapsible": { + "version": "1.1.11", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection": { + "version": "1.1.7", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.1.14", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.10", + "@radix-ui/react-focus-guards": "1.1.2", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-direction": { + "version": "1.1.1", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.10", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dropdown-menu": { + "version": "2.1.15", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-menu": "2.1.15", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.2", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.7", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-hover-card": { + "version": "1.1.14", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.10", + "@radix-ui/react-popper": "1.2.7", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.1", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label": { + "version": "2.1.7", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu": { + "version": "2.1.15", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.10", + "@radix-ui/react-focus-guards": "1.1.2", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.7", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.10", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover": { + "version": "1.1.14", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.10", + "@radix-ui/react-focus-guards": "1.1.2", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.7", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper": { + "version": "1.2.7", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-rect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.1.9", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.4", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-radio-group": { + "version": "1.3.7", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.10", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.10", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-scroll-area": { + "version": "1.2.9", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select": { + "version": "2.2.5", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.10", + "@radix-ui/react-focus-guards": "1.1.2", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.7", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-separator": { + "version": "1.1.7", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-switch": { + "version": "1.2.5", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs": { + "version": "1.1.12", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.10", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip": { + "version": "1.2.7", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.10", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.7", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.2", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.1", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-is-hydrated": { + "version": "0.1.0", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.5.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-previous": { + "version": "1.1.1", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-rect": { + "version": "1.1.1", + "license": "MIT", + "dependencies": { + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-size": { + "version": "1.1.1", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-visually-hidden": { + "version": "1.2.3", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/rect": { + "version": "1.1.1", + "license": "MIT" + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.19", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/pluginutils": { + "version": "5.1.4", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils/node_modules/estree-walker": { + "version": "2.0.2", + "license": "MIT" + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.41.1", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@shikijs/core": { + "version": "3.4.2", + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.4.2", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4", + "hast-util-to-html": "^9.0.5" + } + }, + "node_modules/@shikijs/engine-javascript": { + "version": "3.4.2", + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.4.2", + "@shikijs/vscode-textmate": "^10.0.2", + "oniguruma-to-es": "^4.3.3" + } + }, + "node_modules/@shikijs/engine-oniguruma": { + "version": "3.4.2", + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.4.2", + "@shikijs/vscode-textmate": "^10.0.2" + } + }, + "node_modules/@shikijs/langs": { + "version": "3.4.2", + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.4.2" + } + }, + "node_modules/@shikijs/themes": { + "version": "3.4.2", + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.4.2" + } + }, + "node_modules/@shikijs/types": { + "version": "3.4.2", + "license": "MIT", + "dependencies": { + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + } + }, + "node_modules/@shikijs/vscode-textmate": { + "version": "10.0.2", + "license": "MIT" + }, + "node_modules/@simplewebauthn/browser": { + "version": "13.1.2", + "license": "MIT" + }, + "node_modules/@simplewebauthn/server": { + "version": "13.1.2", + "license": "MIT", + "dependencies": { + "@hexagon/base64": "^1.1.27", + "@levischuck/tiny-cbor": "^0.2.2", + "@peculiar/asn1-android": "^2.3.10", + "@peculiar/asn1-ecc": "^2.3.8", + "@peculiar/asn1-rsa": "^2.3.8", + "@peculiar/asn1-schema": "^2.3.8", + "@peculiar/asn1-x509": "^2.3.8" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@swc/helpers": { + "version": "0.5.17", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@tailwindcss/node": { + "version": "4.1.11", + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "enhanced-resolve": "^5.18.1", + "jiti": "^2.4.2", + "lightningcss": "1.30.1", + "magic-string": "^0.30.17", + "source-map-js": "^1.2.1", + "tailwindcss": "4.1.11" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.1.11", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.4", + "tar": "^7.4.3" + }, + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.1.11", + "@tailwindcss/oxide-darwin-arm64": "4.1.11", + "@tailwindcss/oxide-darwin-x64": "4.1.11", + "@tailwindcss/oxide-freebsd-x64": "4.1.11", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.11", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.11", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.11", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.11", + "@tailwindcss/oxide-linux-x64-musl": "4.1.11", + "@tailwindcss/oxide-wasm32-wasi": "4.1.11", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.11", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.11" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.1.11", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.1.11", + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.1.11", + "@tailwindcss/oxide": "4.1.11", + "tailwindcss": "4.1.11" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7" + } + }, + "node_modules/@tanstack/react-virtual": { + "version": "3.13.12", + "license": "MIT", + "dependencies": { + "@tanstack/virtual-core": "3.13.12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@tanstack/virtual-core": { + "version": "3.13.12", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@testing-library/dom": { + "version": "10.4.0", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "chalk": "^4.1.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/dom/node_modules/aria-query": { + "version": "5.3.0", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/@testing-library/dom/node_modules/chalk": { + "version": "4.1.2", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@testing-library/dom/node_modules/dom-accessibility-api": { + "version": "0.5.16", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.6.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "chalk": "^3.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "lodash": "^4.17.21", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/react": { + "version": "16.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__core/node_modules/@babel/parser": { + "version": "7.27.3", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.3" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template/node_modules/@babel/parser": { + "version": "7.27.3", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.3" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.7", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "node_modules/@types/bcryptjs": { + "version": "3.0.0", + "deprecated": "This is a stub types definition. bcryptjs provides its own type definitions, so you do not need this installed.", + "dev": true, + "license": "MIT", + "dependencies": { + "bcryptjs": "*" + } + }, + "node_modules/@types/bun": { + "version": "1.2.18", + "dev": true, + "license": "MIT", + "dependencies": { + "bun-types": "1.2.18" + } + }, + "node_modules/@types/canvas-confetti": { + "version": "1.9.0", + "license": "MIT" + }, + "node_modules/@types/chai": { + "version": "5.2.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*" + } + }, + "node_modules/@types/debug": { + "version": "4.1.12", + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.7", + "license": "MIT" + }, + "node_modules/@types/estree-jsx": { + "version": "1.0.5", + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/@types/fontkit": { + "version": "2.0.8", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/hast": { + "version": "3.0.4", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.10", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ms": "*", + "@types/node": "*" + } + }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/mdx": { + "version": "2.0.13", + "license": "MIT" + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "license": "MIT" + }, + "node_modules/@types/nlcst": { + "version": "2.0.3", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/node": { + "version": "22.15.23", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/react": { + "version": "19.1.8", + "license": "MIT", + "dependencies": { + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.1.6", + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.0.0" + } + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "license": "MIT" + }, + "node_modules/@types/uuid": { + "version": "10.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "license": "ISC" + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.6.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.19", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" + } + }, + "node_modules/@vitest/expect": { + "version": "3.2.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.4", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.4", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.4", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.4", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@volar/kit": { + "version": "2.4.14", + "license": "MIT", + "dependencies": { + "@volar/language-service": "2.4.14", + "@volar/typescript": "2.4.14", + "typesafe-path": "^0.2.2", + "vscode-languageserver-textdocument": "^1.0.11", + "vscode-uri": "^3.0.8" + }, + "peerDependencies": { + "typescript": "*" + } + }, + "node_modules/@volar/language-core": { + "version": "2.4.14", + "license": "MIT", + "dependencies": { + "@volar/source-map": "2.4.14" + } + }, + "node_modules/@volar/language-server": { + "version": "2.4.14", + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.14", + "@volar/language-service": "2.4.14", + "@volar/typescript": "2.4.14", + "path-browserify": "^1.0.1", + "request-light": "^0.7.0", + "vscode-languageserver": "^9.0.1", + "vscode-languageserver-protocol": "^3.17.5", + "vscode-languageserver-textdocument": "^1.0.11", + "vscode-uri": "^3.0.8" + } + }, + "node_modules/@volar/language-service": { + "version": "2.4.14", + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.14", + "vscode-languageserver-protocol": "^3.17.5", + "vscode-languageserver-textdocument": "^1.0.11", + "vscode-uri": "^3.0.8" + } + }, + "node_modules/@volar/source-map": { + "version": "2.4.14", + "license": "MIT" + }, + "node_modules/@volar/typescript": { + "version": "2.4.14", + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.14", + "path-browserify": "^1.0.1", + "vscode-uri": "^3.0.8" + } + }, + "node_modules/@vscode/emmet-helper": { + "version": "2.11.0", + "license": "MIT", + "dependencies": { + "emmet": "^2.4.3", + "jsonc-parser": "^2.3.0", + "vscode-languageserver-textdocument": "^1.0.1", + "vscode-languageserver-types": "^3.15.1", + "vscode-uri": "^3.0.8" + } + }, + "node_modules/@vscode/emmet-helper/node_modules/jsonc-parser": { + "version": "2.3.1", + "license": "MIT" + }, + "node_modules/@vscode/l10n": { + "version": "0.0.18", + "license": "MIT" + }, + "node_modules/acorn": { + "version": "8.14.1", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "8.17.1", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-align": { + "version": "3.0.1", + "license": "ISC", + "dependencies": { + "string-width": "^4.1.0" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.1", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "license": "Python-2.0" + }, + "node_modules/aria-hidden": { + "version": "1.2.6", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/aria-query": { + "version": "5.3.2", + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/array-iterate": { + "version": "2.0.1", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/asn1js": { + "version": "3.0.6", + "license": "BSD-3-Clause", + "dependencies": { + "pvtsutils": "^1.3.6", + "pvutils": "^1.1.3", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/astring": { + "version": "1.9.0", + "license": "MIT", + "bin": { + "astring": "bin/astring" + } + }, + "node_modules/astro": { + "version": "5.11.0", + "license": "MIT", + "dependencies": { + "@astrojs/compiler": "^2.12.2", + "@astrojs/internal-helpers": "0.6.1", + "@astrojs/markdown-remark": "6.3.2", + "@astrojs/telemetry": "3.3.0", + "@capsizecss/unpack": "^2.4.0", + "@oslojs/encoding": "^1.1.0", + "@rollup/pluginutils": "^5.1.4", + "acorn": "^8.14.1", + "aria-query": "^5.3.2", + "axobject-query": "^4.1.0", + "boxen": "8.0.1", + "ci-info": "^4.2.0", + "clsx": "^2.1.1", + "common-ancestor-path": "^1.0.1", + "cookie": "^1.0.2", + "cssesc": "^3.0.0", + "debug": "^4.4.0", + "deterministic-object-hash": "^2.0.2", + "devalue": "^5.1.1", + "diff": "^5.2.0", + "dlv": "^1.1.3", + "dset": "^3.1.4", + "es-module-lexer": "^1.6.0", + "esbuild": "^0.25.0", + "estree-walker": "^3.0.3", + "flattie": "^1.1.1", + "fontace": "~0.3.0", + "github-slugger": "^2.0.0", + "html-escaper": "3.0.3", + "http-cache-semantics": "^4.1.1", + "import-meta-resolve": "^4.1.0", + "js-yaml": "^4.1.0", + "kleur": "^4.1.5", + "magic-string": "^0.30.17", + "magicast": "^0.3.5", + "mrmime": "^2.0.1", + "neotraverse": "^0.6.18", + "p-limit": "^6.2.0", + "p-queue": "^8.1.0", + "package-manager-detector": "^1.1.0", + "picomatch": "^4.0.2", + "prompts": "^2.4.2", + "rehype": "^13.0.2", + "semver": "^7.7.1", + "shiki": "^3.2.1", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.12", + "tsconfck": "^3.1.5", + "ultrahtml": "^1.6.0", + "unifont": "~0.5.0", + "unist-util-visit": "^5.0.0", + "unstorage": "^1.15.0", + "vfile": "^6.0.3", + "vite": "^6.3.4", + "vitefu": "^1.0.6", + "xxhash-wasm": "^1.1.0", + "yargs-parser": "^21.1.1", + "yocto-spinner": "^0.2.1", + "zod": "^3.24.2", + "zod-to-json-schema": "^3.24.5", + "zod-to-ts": "^1.2.0" + }, + "bin": { + "astro": "astro.js" + }, + "engines": { + "node": "18.20.8 || ^20.3.0 || >=22.0.0", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/astrodotbuild" + }, + "optionalDependencies": { + "sharp": "^0.33.3" + } + }, + "node_modules/astro/node_modules/zod": { + "version": "3.25.64", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/bail": { + "version": "2.0.2", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/base-64": { + "version": "1.0.0", + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bcryptjs": { + "version": "3.0.2", + "license": "BSD-3-Clause", + "bin": { + "bcrypt": "bin/bcrypt" + } + }, + "node_modules/before-after-hook": { + "version": "4.0.0", + "license": "Apache-2.0" + }, + "node_modules/better-auth": { + "version": "1.2.12", + "license": "MIT", + "dependencies": { + "@better-auth/utils": "0.2.5", + "@better-fetch/fetch": "^1.1.18", + "@noble/ciphers": "^0.6.0", + "@noble/hashes": "^1.6.1", + "@simplewebauthn/browser": "^13.0.0", + "@simplewebauthn/server": "^13.0.0", + "better-call": "^1.0.8", + "defu": "^6.1.4", + "jose": "^6.0.11", + "kysely": "^0.28.2", + "nanostores": "^0.11.3", + "zod": "^3.24.1" + } + }, + "node_modules/better-call": { + "version": "1.0.12", + "dependencies": { + "@better-fetch/fetch": "^1.1.4", + "rou3": "^0.5.1", + "set-cookie-parser": "^2.7.1", + "uncrypto": "^0.1.3" + } + }, + "node_modules/blob-to-buffer": { + "version": "1.2.9", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/boxen": { + "version": "8.0.1", + "license": "MIT", + "dependencies": { + "ansi-align": "^3.0.1", + "camelcase": "^8.0.0", + "chalk": "^5.3.0", + "cli-boxes": "^3.0.0", + "string-width": "^7.2.0", + "type-fest": "^4.21.0", + "widest-line": "^5.0.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/boxen/node_modules/chalk": { + "version": "5.4.1", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/boxen/node_modules/string-width": { + "version": "7.2.0", + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/boxen/node_modules/string-width/node_modules/emoji-regex": { + "version": "10.4.0", + "license": "MIT" + }, + "node_modules/boxen/node_modules/string-width/node_modules/strip-ansi": { + "version": "7.1.0", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/boxen/node_modules/string-width/node_modules/strip-ansi/node_modules/ansi-regex": { + "version": "6.1.0", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/brotli": { + "version": "1.3.3", + "license": "MIT", + "dependencies": { + "base64-js": "^1.1.2" + } + }, + "node_modules/browserslist": { + "version": "4.24.5", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001716", + "electron-to-chromium": "^1.5.149", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "license": "BSD-3-Clause" + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "dev": true, + "license": "MIT" + }, + "node_modules/bun-types": { + "version": "1.2.18", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + }, + "peerDependencies": { + "@types/react": "^19" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/camelcase": { + "version": "8.0.0", + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001718", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/canvas-confetti": { + "version": "1.9.3", + "license": "ISC", + "funding": { + "type": "donate", + "url": "https://www.paypal.me/kirilvatev" + } + }, + "node_modules/ccount": { + "version": "2.0.1", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/chai": { + "version": "5.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/chai/node_modules/loupe": { + "version": "3.1.3", + "dev": true, + "license": "MIT" + }, + "node_modules/chalk": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/character-entities": { + "version": "2.0.2", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-reference-invalid": { + "version": "2.0.1", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/check-error": { + "version": "2.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/chownr": { + "version": "3.0.0", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/ci-info": { + "version": "4.2.0", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/class-variance-authority": { + "version": "0.7.1", + "license": "Apache-2.0", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + } + }, + "node_modules/cli-boxes": { + "version": "3.0.0", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/clone": { + "version": "2.1.2", + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/cmdk": { + "version": "1.1.1", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "^1.1.1", + "@radix-ui/react-dialog": "^1.1.6", + "@radix-ui/react-id": "^1.1.0", + "@radix-ui/react-primitive": "^2.0.2" + }, + "peerDependencies": { + "react": "^18 || ^19 || ^19.0.0-rc", + "react-dom": "^18 || ^19 || ^19.0.0-rc" + } + }, + "node_modules/collapse-white-space": { + "version": "2.1.0", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/color": { + "version": "4.2.3", + "license": "MIT", + "optional": true, + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "license": "MIT" + }, + "node_modules/color-string": { + "version": "1.9.1", + "license": "MIT", + "optional": true, + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/common-ancestor-path": { + "version": "1.0.1", + "license": "ISC" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.0.2", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/cookie-es": { + "version": "1.2.2", + "license": "MIT" + }, + "node_modules/cross-fetch": { + "version": "3.2.0", + "license": "MIT", + "dependencies": { + "node-fetch": "^2.7.0" + } + }, + "node_modules/crossws": { + "version": "0.3.5", + "license": "MIT", + "dependencies": { + "uncrypto": "^0.1.3" + } + }, + "node_modules/css-tree": { + "version": "3.1.0", + "license": "MIT", + "dependencies": { + "mdn-data": "2.12.2", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/css.escape": { + "version": "1.5.1", + "dev": true, + "license": "MIT" + }, + "node_modules/cssesc": { + "version": "3.0.0", + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cssstyle": { + "version": "4.3.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^3.1.2", + "rrweb-cssom": "^0.8.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "license": "MIT" + }, + "node_modules/data-urls": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/debug": { + "version": "4.4.1", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.5.0", + "dev": true, + "license": "MIT" + }, + "node_modules/decode-named-character-reference": { + "version": "1.1.0", + "license": "MIT", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/defu": { + "version": "6.1.4", + "license": "MIT" + }, + "node_modules/depd": { + "version": "2.0.0", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/destr": { + "version": "2.0.5", + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.0.4", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "license": "MIT" + }, + "node_modules/deterministic-object-hash": { + "version": "2.0.2", + "license": "MIT", + "dependencies": { + "base-64": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/devalue": { + "version": "5.1.1", + "license": "MIT" + }, + "node_modules/devlop": { + "version": "1.1.0", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/dfa": { + "version": "1.2.0", + "license": "MIT" + }, + "node_modules/diff": { + "version": "5.2.0", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dlv": { + "version": "1.1.3", + "license": "MIT" + }, + "node_modules/dom-accessibility-api": { + "version": "0.6.3", + "dev": true, + "license": "MIT" + }, + "node_modules/drizzle-kit": { + "version": "0.31.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@drizzle-team/brocli": "^0.10.2", + "@esbuild-kit/esm-loader": "^2.5.5", + "esbuild": "^0.25.4", + "esbuild-register": "^3.5.0" + }, + "bin": { + "drizzle-kit": "bin.cjs" + } + }, + "node_modules/drizzle-orm": { + "version": "0.44.2", + "license": "Apache-2.0", + "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.13", + "@prisma/client": "*", + "@tidbcloud/serverless": "*", + "@types/better-sqlite3": "*", + "@types/pg": "*", + "@types/sql.js": "*", + "@upstash/redis": ">=1.34.7", + "@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", + "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 + }, + "@upstash/redis": { + "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 + } + } + }, + "node_modules/dset": { + "version": "3.1.4", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.159", + "license": "ISC" + }, + "node_modules/emmet": { + "version": "2.4.11", + "license": "MIT", + "workspaces": [ + "./packages/scanner", + "./packages/abbreviation", + "./packages/css-abbreviation", + "./" + ], + "dependencies": { + "@emmetio/abbreviation": "^2.3.3", + "@emmetio/css-abbreviation": "^2.1.8" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.18.1", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/entities": { + "version": "6.0.0", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "license": "MIT" + }, + "node_modules/esast-util-from-estree": { + "version": "2.0.0", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "devlop": "^1.0.0", + "estree-util-visit": "^2.0.0", + "unist-util-position-from-estree": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/esast-util-from-js": { + "version": "2.0.1", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "acorn": "^8.0.0", + "esast-util-from-estree": "^2.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/esbuild": { + "version": "0.25.5", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.5", + "@esbuild/android-arm": "0.25.5", + "@esbuild/android-arm64": "0.25.5", + "@esbuild/android-x64": "0.25.5", + "@esbuild/darwin-arm64": "0.25.5", + "@esbuild/darwin-x64": "0.25.5", + "@esbuild/freebsd-arm64": "0.25.5", + "@esbuild/freebsd-x64": "0.25.5", + "@esbuild/linux-arm": "0.25.5", + "@esbuild/linux-arm64": "0.25.5", + "@esbuild/linux-ia32": "0.25.5", + "@esbuild/linux-loong64": "0.25.5", + "@esbuild/linux-mips64el": "0.25.5", + "@esbuild/linux-ppc64": "0.25.5", + "@esbuild/linux-riscv64": "0.25.5", + "@esbuild/linux-s390x": "0.25.5", + "@esbuild/linux-x64": "0.25.5", + "@esbuild/netbsd-arm64": "0.25.5", + "@esbuild/netbsd-x64": "0.25.5", + "@esbuild/openbsd-arm64": "0.25.5", + "@esbuild/openbsd-x64": "0.25.5", + "@esbuild/sunos-x64": "0.25.5", + "@esbuild/win32-arm64": "0.25.5", + "@esbuild/win32-ia32": "0.25.5", + "@esbuild/win32-x64": "0.25.5" + } + }, + "node_modules/esbuild-register": { + "version": "3.6.0", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4" + }, + "peerDependencies": { + "esbuild": ">=0.12 <1" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "5.0.0", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/estree-util-attach-comments": { + "version": "3.0.0", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-build-jsx": { + "version": "3.0.1", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "estree-walker": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-is-identifier-name": { + "version": "3.0.0", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-scope": { + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-to-js": { + "version": "2.0.0", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "astring": "^1.8.0", + "source-map": "^0.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-visit": { + "version": "2.0.0", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "license": "MIT" + }, + "node_modules/expect-type": { + "version": "1.2.1", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "license": "MIT" + }, + "node_modules/fast-content-type-parse": { + "version": "3.0.0", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-uri": { + "version": "3.0.6", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fastq": { + "version": "1.19.1", + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fdir": { + "version": "6.4.4", + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/flattie": { + "version": "1.1.1", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/fontace": { + "version": "0.3.0", + "license": "MIT", + "dependencies": { + "@types/fontkit": "^2.0.8", + "fontkit": "^2.0.4" + } + }, + "node_modules/fontkit": { + "version": "2.0.4", + "license": "MIT", + "dependencies": { + "@swc/helpers": "^0.5.12", + "brotli": "^1.3.2", + "clone": "^2.1.2", + "dfa": "^1.2.0", + "fast-deep-equal": "^3.1.3", + "restructure": "^3.0.0", + "tiny-inflate": "^1.0.3", + "unicode-properties": "^1.4.0", + "unicode-trie": "^2.0.0" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/fuse.js": { + "version": "7.1.0", + "license": "Apache-2.0", + "engines": { + "node": ">=10" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.3.0", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-nonce": { + "version": "1.0.1", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/get-tsconfig": { + "version": "4.10.1", + "devOptional": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/github-slugger": { + "version": "2.0.0", + "license": "ISC" + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/globals": { + "version": "11.12.0", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "license": "ISC" + }, + "node_modules/h3": { + "version": "1.15.3", + "license": "MIT", + "dependencies": { + "cookie-es": "^1.2.2", + "crossws": "^0.3.4", + "defu": "^6.1.4", + "destr": "^2.0.5", + "iron-webcrypto": "^1.2.1", + "node-mock-http": "^1.0.0", + "radix3": "^1.1.2", + "ufo": "^1.6.1", + "uncrypto": "^0.1.3" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hast-util-from-html": { + "version": "2.0.3", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "devlop": "^1.1.0", + "hast-util-from-parse5": "^8.0.0", + "parse5": "^7.0.0", + "vfile": "^6.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-from-parse5": { + "version": "8.0.3", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "devlop": "^1.0.0", + "hastscript": "^9.0.0", + "property-information": "^7.0.0", + "vfile": "^6.0.0", + "vfile-location": "^5.0.0", + "web-namespaces": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-is-element": { + "version": "3.0.0", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-parse-selector": { + "version": "4.0.0", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-raw": { + "version": "9.1.0", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "@ungap/structured-clone": "^1.0.0", + "hast-util-from-parse5": "^8.0.0", + "hast-util-to-parse5": "^8.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "parse5": "^7.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-estree": { + "version": "3.1.3", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-attach-comments": "^3.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-js": "^1.0.0", + "unist-util-position": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-html": { + "version": "9.0.5", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-whitespace": "^3.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "stringify-entities": "^4.0.0", + "zwitch": "^2.0.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-jsx-runtime": { + "version": "2.3.6", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-js": "^1.0.0", + "unist-util-position": "^5.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-parse5": { + "version": "8.0.0", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "property-information": "^6.0.0", + "space-separated-tokens": "^2.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-parse5/node_modules/property-information": { + "version": "6.5.0", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/hast-util-to-text": { + "version": "4.0.2", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "hast-util-is-element": "^3.0.0", + "unist-util-find-after": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hastscript": { + "version": "9.0.1", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-parse-selector": "^4.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/html-escaper": { + "version": "3.0.3", + "license": "MIT" + }, + "node_modules/html-void-elements": { + "version": "3.0.0", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "license": "BSD-2-Clause" + }, + "node_modules/http-errors": { + "version": "2.0.0", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/import-meta-resolve": { + "version": "4.1.0", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "license": "ISC" + }, + "node_modules/inline-style-parser": { + "version": "0.2.4", + "license": "MIT" + }, + "node_modules/iron-webcrypto": { + "version": "1.2.1", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/brc-dd" + } + }, + "node_modules/is-alphabetical": { + "version": "2.0.1", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "2.0.1", + "license": "MIT", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-arrayish": { + "version": "0.3.2", + "license": "MIT", + "optional": true + }, + "node_modules/is-decimal": { + "version": "2.0.1", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-docker": { + "version": "3.0.0", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-hexadecimal": { + "version": "2.0.1", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/is-wsl": { + "version": "3.1.0", + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jiti": { + "version": "2.4.2", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/jose": { + "version": "6.0.11", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsdom": { + "version": "26.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "cssstyle": "^4.2.1", + "data-urls": "^5.0.0", + "decimal.js": "^10.5.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.16", + "parse5": "^7.2.1", + "rrweb-cssom": "^0.8.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^5.1.1", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.1.1", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "license": "MIT", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "1.4.2", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "license": "MIT", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/kleur": { + "version": "4.1.5", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/kysely": { + "version": "0.28.2", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/lightningcss": { + "version": "1.30.1", + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-darwin-arm64": "1.30.1", + "lightningcss-darwin-x64": "1.30.1", + "lightningcss-freebsd-x64": "1.30.1", + "lightningcss-linux-arm-gnueabihf": "1.30.1", + "lightningcss-linux-arm64-gnu": "1.30.1", + "lightningcss-linux-arm64-musl": "1.30.1", + "lightningcss-linux-x64-gnu": "1.30.1", + "lightningcss-linux-x64-musl": "1.30.1", + "lightningcss-win32-arm64-msvc": "1.30.1", + "lightningcss-win32-x64-msvc": "1.30.1" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.30.1", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "license": "MIT" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "license": "MIT" + }, + "node_modules/longest-streak": { + "version": "3.1.0", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/loupe": { + "version": "3.1.4", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "license": "ISC" + }, + "node_modules/lucide-react": { + "version": "0.525.0", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/lz-string": { + "version": "1.5.0", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, + "node_modules/magic-string": { + "version": "0.30.17", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "node_modules/magicast": { + "version": "0.3.5", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.25.4", + "@babel/types": "^7.25.4", + "source-map-js": "^1.2.0" + } + }, + "node_modules/magicast/node_modules/@babel/parser": { + "version": "7.27.3", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.3" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/markdown-extensions": { + "version": "2.0.0", + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/markdown-table": { + "version": "3.0.4", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-definitions": { + "version": "6.0.0", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-find-and-replace": { + "version": "3.0.2", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "escape-string-regexp": "^5.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-from-markdown": { + "version": "2.0.2", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm": { + "version": "3.1.0", + "license": "MIT", + "dependencies": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-gfm-autolink-literal": "^2.0.0", + "mdast-util-gfm-footnote": "^2.0.0", + "mdast-util-gfm-strikethrough": "^2.0.0", + "mdast-util-gfm-table": "^2.0.0", + "mdast-util-gfm-task-list-item": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-autolink-literal": { + "version": "2.0.1", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "ccount": "^2.0.0", + "devlop": "^1.0.0", + "mdast-util-find-and-replace": "^3.0.0", + "micromark-util-character": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-footnote": { + "version": "2.1.0", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-strikethrough": { + "version": "2.0.0", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-table": { + "version": "2.0.0", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "markdown-table": "^3.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-task-list-item": { + "version": "2.0.0", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx": { + "version": "3.0.0", + "license": "MIT", + "dependencies": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-expression": { + "version": "2.0.1", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-jsx": { + "version": "3.2.0", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdxjs-esm": { + "version": "2.0.1", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-phrasing": { + "version": "4.1.0", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.0", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown": { + "version": "2.1.2", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "4.0.0", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdn-data": { + "version": "2.12.2", + "license": "CC0-1.0" + }, + "node_modules/merge2": { + "version": "1.4.1", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromark": { + "version": "4.0.2", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.3", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-gfm": { + "version": "3.0.0", + "license": "MIT", + "dependencies": { + "micromark-extension-gfm-autolink-literal": "^2.0.0", + "micromark-extension-gfm-footnote": "^2.0.0", + "micromark-extension-gfm-strikethrough": "^2.0.0", + "micromark-extension-gfm-table": "^2.0.0", + "micromark-extension-gfm-tagfilter": "^2.0.0", + "micromark-extension-gfm-task-list-item": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-autolink-literal": { + "version": "2.1.0", + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-footnote": { + "version": "2.1.0", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-strikethrough": { + "version": "2.1.0", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-table": { + "version": "2.1.1", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-tagfilter": { + "version": "2.0.0", + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-task-list-item": { + "version": "2.1.0", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-mdx-expression": { + "version": "3.0.1", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-mdx-expression": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-mdx-jsx": { + "version": "3.0.2", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "micromark-factory-mdx-expression": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-mdx-md": { + "version": "2.0.0", + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-mdxjs": { + "version": "3.0.0", + "license": "MIT", + "dependencies": { + "acorn": "^8.0.0", + "acorn-jsx": "^5.0.0", + "micromark-extension-mdx-expression": "^3.0.0", + "micromark-extension-mdx-jsx": "^3.0.0", + "micromark-extension-mdx-md": "^2.0.0", + "micromark-extension-mdxjs-esm": "^3.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-mdxjs-esm": { + "version": "3.0.0", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-position-from-estree": "^2.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.1", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "2.0.1", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-mdx-expression": { + "version": "2.0.3", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-position-from-estree": "^2.0.0", + "vfile-message": "^4.0.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "2.0.1", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "2.0.1", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.1", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.1", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.1", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.1", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "2.0.1", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-events-to-acorn": { + "version": "2.0.3", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/unist": "^3.0.0", + "devlop": "^1.0.0", + "estree-util-visit": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "vfile-message": "^4.0.0" + } + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.1", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.1", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.1", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-subtokenize": { + "version": "2.1.0", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromatch": { + "version": "4.0.8", + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.1", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.1", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minizlib": { + "version": "3.0.2", + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/mkdirp": { + "version": "3.0.1", + "license": "MIT", + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/mrmime": { + "version": "2.0.1", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "license": "MIT" + }, + "node_modules/muggle-string": { + "version": "0.4.1", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/nanostores": { + "version": "0.11.4", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/neotraverse": { + "version": "0.6.18", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/next-themes": { + "version": "0.4.6", + "license": "MIT", + "peerDependencies": { + "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" + } + }, + "node_modules/nlcst-to-string": { + "version": "4.0.0", + "license": "MIT", + "dependencies": { + "@types/nlcst": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-fetch-native": { + "version": "1.6.6", + "license": "MIT" + }, + "node_modules/node-fetch/node_modules/whatwg-url": { + "version": "5.0.0", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/node-fetch/node_modules/whatwg-url/node_modules/tr46": { + "version": "0.0.3", + "license": "MIT" + }, + "node_modules/node-fetch/node_modules/whatwg-url/node_modules/webidl-conversions": { + "version": "3.0.1", + "license": "BSD-2-Clause" + }, + "node_modules/node-mock-http": { + "version": "1.0.0", + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.19", + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nwsapi": { + "version": "2.2.20", + "dev": true, + "license": "MIT" + }, + "node_modules/ofetch": { + "version": "1.4.1", + "license": "MIT", + "dependencies": { + "destr": "^2.0.3", + "node-fetch-native": "^1.6.4", + "ufo": "^1.5.4" + } + }, + "node_modules/ohash": { + "version": "2.0.11", + "license": "MIT" + }, + "node_modules/on-finished": { + "version": "2.4.1", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/oniguruma-parser": { + "version": "0.12.1", + "license": "MIT" + }, + "node_modules/oniguruma-to-es": { + "version": "4.3.3", + "license": "MIT", + "dependencies": { + "oniguruma-parser": "^0.12.1", + "regex": "^6.0.1", + "regex-recursion": "^6.0.2" + } + }, + "node_modules/p-limit": { + "version": "6.2.0", + "license": "MIT", + "dependencies": { + "yocto-queue": "^1.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-queue": { + "version": "8.1.0", + "license": "MIT", + "dependencies": { + "eventemitter3": "^5.0.1", + "p-timeout": "^6.1.2" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-timeout": { + "version": "6.1.4", + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-manager-detector": { + "version": "1.3.0", + "license": "MIT" + }, + "node_modules/pako": { + "version": "0.2.9", + "license": "MIT" + }, + "node_modules/parse-entities": { + "version": "4.0.2", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse-entities/node_modules/@types/unist": { + "version": "2.0.11", + "license": "MIT" + }, + "node_modules/parse-latin": { + "version": "7.0.0", + "license": "MIT", + "dependencies": { + "@types/nlcst": "^2.0.0", + "@types/unist": "^3.0.0", + "nlcst-to-string": "^4.0.0", + "unist-util-modify-children": "^4.0.0", + "unist-util-visit-children": "^3.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse5": { + "version": "7.3.0", + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "license": "MIT" + }, + "node_modules/pathe": { + "version": "2.0.3", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.2", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.3", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.8", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prettier": { + "version": "3.6.2", + "license": "MIT", + "optional": true, + "peer": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/pretty-format": { + "version": "27.5.1", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/prismjs": { + "version": "1.30.0", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/prompts/node_modules/kleur": { + "version": "3.0.3", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/property-information": { + "version": "7.1.0", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pvtsutils": { + "version": "1.3.6", + "license": "MIT", + "dependencies": { + "tslib": "^2.8.1" + } + }, + "node_modules/pvutils": { + "version": "1.1.3", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/radix3": { + "version": "1.1.2", + "license": "MIT" + }, + "node_modules/range-parser": { + "version": "1.2.1", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/react": { + "version": "19.1.0", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.1.0", + "license": "MIT", + "dependencies": { + "scheduler": "^0.26.0" + }, + "peerDependencies": { + "react": "^19.1.0" + } + }, + "node_modules/react-icons": { + "version": "5.5.0", + "license": "MIT", + "peerDependencies": { + "react": "*" + } + }, + "node_modules/react-is": { + "version": "17.0.2", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-remove-scroll": { + "version": "2.7.0", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "license": "MIT", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "license": "MIT", + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/recma-build-jsx": { + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-util-build-jsx": "^3.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/recma-jsx": { + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "acorn-jsx": "^5.0.0", + "estree-util-to-js": "^2.0.0", + "recma-parse": "^1.0.0", + "recma-stringify": "^1.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/recma-parse": { + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "esast-util-from-js": "^2.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/recma-stringify": { + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-util-to-js": "^2.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/redent": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/regex": { + "version": "6.0.1", + "license": "MIT", + "dependencies": { + "regex-utilities": "^2.3.0" + } + }, + "node_modules/regex-recursion": { + "version": "6.0.2", + "license": "MIT", + "dependencies": { + "regex-utilities": "^2.3.0" + } + }, + "node_modules/regex-utilities": { + "version": "2.3.0", + "license": "MIT" + }, + "node_modules/rehype": { + "version": "13.0.2", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "rehype-parse": "^9.0.0", + "rehype-stringify": "^10.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-parse": { + "version": "9.0.1", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-from-html": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-raw": { + "version": "7.0.0", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-raw": "^9.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-recma": { + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "hast-util-to-estree": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-stringify": { + "version": "10.0.1", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-to-html": "^9.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-gfm": { + "version": "4.0.1", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-gfm": "^3.0.0", + "micromark-extension-gfm": "^3.0.0", + "remark-parse": "^11.0.0", + "remark-stringify": "^11.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-mdx": { + "version": "3.1.0", + "license": "MIT", + "dependencies": { + "mdast-util-mdx": "^3.0.0", + "micromark-extension-mdxjs": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-parse": { + "version": "11.0.0", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-rehype": { + "version": "11.1.2", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "mdast-util-to-hast": "^13.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-smartypants": { + "version": "3.0.2", + "license": "MIT", + "dependencies": { + "retext": "^9.0.0", + "retext-smartypants": "^6.0.0", + "unified": "^11.0.4", + "unist-util-visit": "^5.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/remark-stringify": { + "version": "11.0.0", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-to-markdown": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/request-light": { + "version": "0.7.0", + "license": "MIT" + }, + "node_modules/require-directory": { + "version": "2.1.1", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "devOptional": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/restructure": { + "version": "3.0.2", + "license": "MIT" + }, + "node_modules/retext": { + "version": "9.0.0", + "license": "MIT", + "dependencies": { + "@types/nlcst": "^2.0.0", + "retext-latin": "^4.0.0", + "retext-stringify": "^4.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/retext-latin": { + "version": "4.0.0", + "license": "MIT", + "dependencies": { + "@types/nlcst": "^2.0.0", + "parse-latin": "^7.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/retext-smartypants": { + "version": "6.2.0", + "license": "MIT", + "dependencies": { + "@types/nlcst": "^2.0.0", + "nlcst-to-string": "^4.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/retext-stringify": { + "version": "4.0.0", + "license": "MIT", + "dependencies": { + "@types/nlcst": "^2.0.0", + "nlcst-to-string": "^4.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.41.1", + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.7" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.41.1", + "@rollup/rollup-android-arm64": "4.41.1", + "@rollup/rollup-darwin-arm64": "4.41.1", + "@rollup/rollup-darwin-x64": "4.41.1", + "@rollup/rollup-freebsd-arm64": "4.41.1", + "@rollup/rollup-freebsd-x64": "4.41.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.41.1", + "@rollup/rollup-linux-arm-musleabihf": "4.41.1", + "@rollup/rollup-linux-arm64-gnu": "4.41.1", + "@rollup/rollup-linux-arm64-musl": "4.41.1", + "@rollup/rollup-linux-loongarch64-gnu": "4.41.1", + "@rollup/rollup-linux-powerpc64le-gnu": "4.41.1", + "@rollup/rollup-linux-riscv64-gnu": "4.41.1", + "@rollup/rollup-linux-riscv64-musl": "4.41.1", + "@rollup/rollup-linux-s390x-gnu": "4.41.1", + "@rollup/rollup-linux-x64-gnu": "4.41.1", + "@rollup/rollup-linux-x64-musl": "4.41.1", + "@rollup/rollup-win32-arm64-msvc": "4.41.1", + "@rollup/rollup-win32-ia32-msvc": "4.41.1", + "@rollup/rollup-win32-x64-msvc": "4.41.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/rou3": { + "version": "0.5.1", + "license": "MIT" + }, + "node_modules/rrweb-cssom": { + "version": "0.8.0", + "dev": true, + "license": "MIT" + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "dev": true, + "license": "MIT" + }, + "node_modules/saxes": { + "version": "6.0.0", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/scheduler": { + "version": "0.26.0", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.2", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "1.2.0", + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "mime-types": "^3.0.1", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/server-destroy": { + "version": "1.0.1", + "license": "ISC" + }, + "node_modules/set-cookie-parser": { + "version": "2.7.1", + "license": "MIT" + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "license": "ISC" + }, + "node_modules/sharp": { + "version": "0.33.5", + "hasInstallScript": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "color": "^4.2.3", + "detect-libc": "^2.0.3", + "semver": "^7.6.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.33.5", + "@img/sharp-darwin-x64": "0.33.5", + "@img/sharp-libvips-darwin-arm64": "1.0.4", + "@img/sharp-libvips-darwin-x64": "1.0.4", + "@img/sharp-libvips-linux-arm": "1.0.5", + "@img/sharp-libvips-linux-arm64": "1.0.4", + "@img/sharp-libvips-linux-s390x": "1.0.4", + "@img/sharp-libvips-linux-x64": "1.0.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", + "@img/sharp-libvips-linuxmusl-x64": "1.0.4", + "@img/sharp-linux-arm": "0.33.5", + "@img/sharp-linux-arm64": "0.33.5", + "@img/sharp-linux-s390x": "0.33.5", + "@img/sharp-linux-x64": "0.33.5", + "@img/sharp-linuxmusl-arm64": "0.33.5", + "@img/sharp-linuxmusl-x64": "0.33.5", + "@img/sharp-wasm32": "0.33.5", + "@img/sharp-win32-ia32": "0.33.5", + "@img/sharp-win32-x64": "0.33.5" + } + }, + "node_modules/shiki": { + "version": "3.4.2", + "license": "MIT", + "dependencies": { + "@shikijs/core": "3.4.2", + "@shikijs/engine-javascript": "3.4.2", + "@shikijs/engine-oniguruma": "3.4.2", + "@shikijs/langs": "3.4.2", + "@shikijs/themes": "3.4.2", + "@shikijs/types": "3.4.2", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "dev": true, + "license": "ISC" + }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "license": "MIT", + "optional": true, + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "license": "MIT" + }, + "node_modules/smol-toml": { + "version": "1.3.4", + "license": "BSD-3-Clause", + "engines": { + "node": ">= 18" + }, + "funding": { + "url": "https://github.com/sponsors/cyyynthia" + } + }, + "node_modules/sonner": { + "version": "2.0.5", + "license": "MIT", + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, + "node_modules/source-map": { + "version": "0.7.4", + "license": "BSD-3-Clause", + "engines": { + "node": ">= 8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/statuses": { + "version": "2.0.1", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/std-env": { + "version": "3.9.0", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width": { + "version": "4.2.3", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-literal": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/strip-literal/node_modules/js-tokens": { + "version": "9.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/style-to-js": { + "version": "1.1.16", + "license": "MIT", + "dependencies": { + "style-to-object": "1.0.8" + } + }, + "node_modules/style-to-object": { + "version": "1.0.8", + "license": "MIT", + "dependencies": { + "inline-style-parser": "0.2.4" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "dev": true, + "license": "MIT" + }, + "node_modules/tailwind-merge": { + "version": "3.3.1", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwindcss": { + "version": "4.1.11", + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.2.2", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/tar": { + "version": "7.4.3", + "license": "ISC", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.0.1", + "mkdirp": "^3.0.1", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tiny-inflate": { + "version": "1.0.3", + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.14", + "license": "MIT", + "dependencies": { + "fdir": "^6.4.4", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinypool": { + "version": "1.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "6.1.86", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^6.1.86" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.86", + "dev": true, + "license": "MIT" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tough-cookie": { + "version": "5.1.2", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "5.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/trim-lines": { + "version": "3.0.1", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/trough": { + "version": "2.2.0", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/tsconfck": { + "version": "3.1.6", + "license": "MIT", + "bin": { + "tsconfck": "bin/tsconfck.js" + }, + "engines": { + "node": "^18 || >=20" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "license": "0BSD" + }, + "node_modules/tsx": { + "version": "4.20.3", + "devOptional": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.25.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/tw-animate-css": { + "version": "1.3.5", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Wombosvideo" + } + }, + "node_modules/type-fest": { + "version": "4.41.0", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typesafe-path": { + "version": "0.2.2", + "license": "MIT" + }, + "node_modules/typescript": { + "version": "5.8.3", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-auto-import-cache": { + "version": "0.3.6", + "license": "MIT", + "dependencies": { + "semver": "^7.3.8" + } + }, + "node_modules/ufo": { + "version": "1.6.1", + "license": "MIT" + }, + "node_modules/ultrahtml": { + "version": "1.6.0", + "license": "MIT" + }, + "node_modules/uncrypto": { + "version": "0.1.3", + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "6.21.0", + "license": "MIT" + }, + "node_modules/unicode-properties": { + "version": "1.4.1", + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.0", + "unicode-trie": "^2.0.0" + } + }, + "node_modules/unicode-trie": { + "version": "2.0.0", + "license": "MIT", + "dependencies": { + "pako": "^0.2.5", + "tiny-inflate": "^1.0.0" + } + }, + "node_modules/unified": { + "version": "11.0.5", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unifont": { + "version": "0.5.0", + "license": "MIT", + "dependencies": { + "css-tree": "^3.0.0", + "ohash": "^2.0.0" + } + }, + "node_modules/unist-util-find-after": { + "version": "5.0.0", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.0", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-modify-children": { + "version": "4.0.0", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "array-iterate": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position-from-estree": { + "version": "2.0.0", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-remove-position": { + "version": "5.0.0", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.0.0", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-children": { + "version": "3.0.0", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.1", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/universal-user-agent": { + "version": "7.0.3", + "license": "ISC" + }, + "node_modules/unstorage": { + "version": "1.16.0", + "license": "MIT", + "dependencies": { + "anymatch": "^3.1.3", + "chokidar": "^4.0.3", + "destr": "^2.0.5", + "h3": "^1.15.2", + "lru-cache": "^10.4.3", + "node-fetch-native": "^1.6.6", + "ofetch": "^1.4.1", + "ufo": "^1.6.1" + }, + "peerDependencies": { + "@azure/app-configuration": "^1.8.0", + "@azure/cosmos": "^4.2.0", + "@azure/data-tables": "^13.3.0", + "@azure/identity": "^4.6.0", + "@azure/keyvault-secrets": "^4.9.0", + "@azure/storage-blob": "^12.26.0", + "@capacitor/preferences": "^6.0.3 || ^7.0.0", + "@deno/kv": ">=0.9.0", + "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0", + "@planetscale/database": "^1.19.0", + "@upstash/redis": "^1.34.3", + "@vercel/blob": ">=0.27.1", + "@vercel/kv": "^1.0.1", + "aws4fetch": "^1.0.20", + "db0": ">=0.2.1", + "idb-keyval": "^6.2.1", + "ioredis": "^5.4.2", + "uploadthing": "^7.4.4" + }, + "peerDependenciesMeta": { + "@azure/app-configuration": { + "optional": true + }, + "@azure/cosmos": { + "optional": true + }, + "@azure/data-tables": { + "optional": true + }, + "@azure/identity": { + "optional": true + }, + "@azure/keyvault-secrets": { + "optional": true + }, + "@azure/storage-blob": { + "optional": true + }, + "@capacitor/preferences": { + "optional": true + }, + "@deno/kv": { + "optional": true + }, + "@netlify/blobs": { + "optional": true + }, + "@planetscale/database": { + "optional": true + }, + "@upstash/redis": { + "optional": true + }, + "@vercel/blob": { + "optional": true + }, + "@vercel/kv": { + "optional": true + }, + "aws4fetch": { + "optional": true + }, + "db0": { + "optional": true + }, + "idb-keyval": { + "optional": true + }, + "ioredis": { + "optional": true + }, + "uploadthing": { + "optional": true + } + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/use-callback-ref": { + "version": "1.3.3", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "license": "MIT", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sync-external-store": { + "version": "1.5.0", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/uuid": { + "version": "11.1.0", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, + "node_modules/vaul": { + "version": "1.1.2", + "license": "MIT", + "dependencies": { + "@radix-ui/react-dialog": "^1.1.1" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" + } + }, + "node_modules/vfile": { + "version": "6.0.3", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-location": { + "version": "5.0.3", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.2", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vite": { + "version": "6.3.5", + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "3.2.4", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitefu": { + "version": "1.0.6", + "license": "MIT", + "workspaces": [ + "tests/deps/*", + "tests/projects/*" + ], + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "3.2.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.4", + "@vitest/mocker": "3.2.4", + "@vitest/pretty-format": "^3.2.4", + "@vitest/runner": "3.2.4", + "@vitest/snapshot": "3.2.4", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.4", + "@vitest/ui": "3.2.4", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/volar-service-css": { + "version": "0.0.62", + "license": "MIT", + "dependencies": { + "vscode-css-languageservice": "^6.3.0", + "vscode-languageserver-textdocument": "^1.0.11", + "vscode-uri": "^3.0.8" + }, + "peerDependencies": { + "@volar/language-service": "~2.4.0" + }, + "peerDependenciesMeta": { + "@volar/language-service": { + "optional": true + } + } + }, + "node_modules/volar-service-emmet": { + "version": "0.0.62", + "license": "MIT", + "dependencies": { + "@emmetio/css-parser": "^0.4.0", + "@emmetio/html-matcher": "^1.3.0", + "@vscode/emmet-helper": "^2.9.3", + "vscode-uri": "^3.0.8" + }, + "peerDependencies": { + "@volar/language-service": "~2.4.0" + }, + "peerDependenciesMeta": { + "@volar/language-service": { + "optional": true + } + } + }, + "node_modules/volar-service-html": { + "version": "0.0.62", + "license": "MIT", + "dependencies": { + "vscode-html-languageservice": "^5.3.0", + "vscode-languageserver-textdocument": "^1.0.11", + "vscode-uri": "^3.0.8" + }, + "peerDependencies": { + "@volar/language-service": "~2.4.0" + }, + "peerDependenciesMeta": { + "@volar/language-service": { + "optional": true + } + } + }, + "node_modules/volar-service-prettier": { + "version": "0.0.62", + "license": "MIT", + "dependencies": { + "vscode-uri": "^3.0.8" + }, + "peerDependencies": { + "@volar/language-service": "~2.4.0", + "prettier": "^2.2 || ^3.0" + }, + "peerDependenciesMeta": { + "@volar/language-service": { + "optional": true + }, + "prettier": { + "optional": true + } + } + }, + "node_modules/volar-service-typescript": { + "version": "0.0.62", + "license": "MIT", + "dependencies": { + "path-browserify": "^1.0.1", + "semver": "^7.6.2", + "typescript-auto-import-cache": "^0.3.3", + "vscode-languageserver-textdocument": "^1.0.11", + "vscode-nls": "^5.2.0", + "vscode-uri": "^3.0.8" + }, + "peerDependencies": { + "@volar/language-service": "~2.4.0" + }, + "peerDependenciesMeta": { + "@volar/language-service": { + "optional": true + } + } + }, + "node_modules/volar-service-typescript-twoslash-queries": { + "version": "0.0.62", + "license": "MIT", + "dependencies": { + "vscode-uri": "^3.0.8" + }, + "peerDependencies": { + "@volar/language-service": "~2.4.0" + }, + "peerDependenciesMeta": { + "@volar/language-service": { + "optional": true + } + } + }, + "node_modules/volar-service-yaml": { + "version": "0.0.62", + "license": "MIT", + "dependencies": { + "vscode-uri": "^3.0.8", + "yaml-language-server": "~1.15.0" + }, + "peerDependencies": { + "@volar/language-service": "~2.4.0" + }, + "peerDependenciesMeta": { + "@volar/language-service": { + "optional": true + } + } + }, + "node_modules/vscode-css-languageservice": { + "version": "6.3.6", + "license": "MIT", + "dependencies": { + "@vscode/l10n": "^0.0.18", + "vscode-languageserver-textdocument": "^1.0.12", + "vscode-languageserver-types": "3.17.5", + "vscode-uri": "^3.1.0" + } + }, + "node_modules/vscode-html-languageservice": { + "version": "5.5.0", + "license": "MIT", + "dependencies": { + "@vscode/l10n": "^0.0.18", + "vscode-languageserver-textdocument": "^1.0.12", + "vscode-languageserver-types": "^3.17.5", + "vscode-uri": "^3.1.0" + } + }, + "node_modules/vscode-json-languageservice": { + "version": "4.1.8", + "license": "MIT", + "dependencies": { + "jsonc-parser": "^3.0.0", + "vscode-languageserver-textdocument": "^1.0.1", + "vscode-languageserver-types": "^3.16.0", + "vscode-nls": "^5.0.0", + "vscode-uri": "^3.0.2" + }, + "engines": { + "npm": ">=7.0.0" + } + }, + "node_modules/vscode-json-languageservice/node_modules/jsonc-parser": { + "version": "3.3.1", + "license": "MIT" + }, + "node_modules/vscode-jsonrpc": { + "version": "8.2.0", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/vscode-languageserver": { + "version": "9.0.1", + "license": "MIT", + "dependencies": { + "vscode-languageserver-protocol": "3.17.5" + }, + "bin": { + "installServerIntoExtension": "bin/installServerIntoExtension" + } + }, + "node_modules/vscode-languageserver-protocol": { + "version": "3.17.5", + "license": "MIT", + "dependencies": { + "vscode-jsonrpc": "8.2.0", + "vscode-languageserver-types": "3.17.5" + } + }, + "node_modules/vscode-languageserver-textdocument": { + "version": "1.0.12", + "license": "MIT" + }, + "node_modules/vscode-languageserver-types": { + "version": "3.17.5", + "license": "MIT" + }, + "node_modules/vscode-nls": { + "version": "5.2.0", + "license": "MIT" + }, + "node_modules/vscode-uri": { + "version": "3.1.0", + "license": "MIT" + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/web-namespaces": { + "version": "2.0.1", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/which-pm-runs": { + "version": "1.1.0", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/widest-line": { + "version": "5.0.0", + "license": "MIT", + "dependencies": { + "string-width": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/widest-line/node_modules/string-width": { + "version": "7.2.0", + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/widest-line/node_modules/string-width/node_modules/emoji-regex": { + "version": "10.4.0", + "license": "MIT" + }, + "node_modules/widest-line/node_modules/string-width/node_modules/strip-ansi": { + "version": "7.1.0", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/widest-line/node_modules/string-width/node_modules/strip-ansi/node_modules/ansi-regex": { + "version": "6.1.0", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/wrap-ansi": { + "version": "9.0.0", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "7.2.0", + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/wrap-ansi/node_modules/string-width/node_modules/emoji-regex": { + "version": "10.4.0", + "license": "MIT" + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "7.1.0", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi/node_modules/ansi-regex": { + "version": "6.1.0", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ws": { + "version": "8.18.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "dev": true, + "license": "MIT" + }, + "node_modules/xxhash-wasm": { + "version": "1.1.0", + "license": "MIT" + }, + "node_modules/y18n": { + "version": "5.0.8", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "5.0.0", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/yaml": { + "version": "2.8.0", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + } + }, + "node_modules/yaml-language-server": { + "version": "1.15.0", + "license": "MIT", + "dependencies": { + "ajv": "^8.11.0", + "lodash": "4.17.21", + "request-light": "^0.5.7", + "vscode-json-languageservice": "4.1.8", + "vscode-languageserver": "^7.0.0", + "vscode-languageserver-textdocument": "^1.0.1", + "vscode-languageserver-types": "^3.16.0", + "vscode-nls": "^5.0.0", + "vscode-uri": "^3.0.2", + "yaml": "2.2.2" + }, + "bin": { + "yaml-language-server": "bin/yaml-language-server" + }, + "optionalDependencies": { + "prettier": "2.8.7" + } + }, + "node_modules/yaml-language-server/node_modules/prettier": { + "version": "2.8.7", + "license": "MIT", + "optional": true, + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/yaml-language-server/node_modules/request-light": { + "version": "0.5.8", + "license": "MIT" + }, + "node_modules/yaml-language-server/node_modules/vscode-languageserver": { + "version": "7.0.0", + "license": "MIT", + "dependencies": { + "vscode-languageserver-protocol": "3.16.0" + }, + "bin": { + "installServerIntoExtension": "bin/installServerIntoExtension" + } + }, + "node_modules/yaml-language-server/node_modules/vscode-languageserver/node_modules/vscode-languageserver-protocol": { + "version": "3.16.0", + "license": "MIT", + "dependencies": { + "vscode-jsonrpc": "6.0.0", + "vscode-languageserver-types": "3.16.0" + } + }, + "node_modules/yaml-language-server/node_modules/vscode-languageserver/node_modules/vscode-languageserver-protocol/node_modules/vscode-jsonrpc": { + "version": "6.0.0", + "license": "MIT", + "engines": { + "node": ">=8.0.0 || >=10.0.0" + } + }, + "node_modules/yaml-language-server/node_modules/vscode-languageserver/node_modules/vscode-languageserver-protocol/node_modules/vscode-languageserver-types": { + "version": "3.16.0", + "license": "MIT" + }, + "node_modules/yaml-language-server/node_modules/yaml": { + "version": "2.2.2", + "license": "ISC", + "engines": { + "node": ">= 14" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "1.2.1", + "license": "MIT", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yocto-spinner": { + "version": "0.2.3", + "license": "MIT", + "dependencies": { + "yoctocolors": "^2.1.1" + }, + "engines": { + "node": ">=18.19" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yoctocolors": { + "version": "2.1.1", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "3.25.75", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.24.5", + "license": "ISC", + "peerDependencies": { + "zod": "^3.24.1" + } + }, + "node_modules/zod-to-ts": { + "version": "1.2.0", + "peerDependencies": { + "typescript": "^4.9.4 || ^5.0.2", + "zod": "^3" + } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + } + } +} diff --git a/package.json b/package.json index bd80b70..ca0fc82 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ }, "scripts": { "setup": "bun install && bun run manage-db init", - "dev": "bunx --bun astro dev", + "dev": "bunx --bun astro dev --port 4567", "dev:clean": "bun run cleanup-db && bun run manage-db init && bunx --bun astro dev", "build": "bunx --bun astro build", "cleanup-db": "rm -f gitea-mirror.db data/gitea-mirror.db", @@ -16,6 +16,14 @@ "check-db": "bun scripts/manage-db.ts check", "fix-db": "bun scripts/manage-db.ts fix", "reset-users": "bun scripts/manage-db.ts reset-users", + "db:generate": "bun drizzle-kit generate", + "db:migrate": "bun drizzle-kit migrate", + "db:push": "bun drizzle-kit push", + "db:pull": "bun drizzle-kit pull", + "db:check": "bun drizzle-kit check", + "db:studio": "bun drizzle-kit studio", + "migrate:better-auth": "bun scripts/migrate-to-better-auth.ts", + "migrate:encrypt-tokens": "bun scripts/migrate-tokens-encryption.ts", "startup-recovery": "bun scripts/startup-recovery.ts", "startup-recovery-force": "bun scripts/startup-recovery.ts --force", "test-recovery": "bun scripts/test-recovery.ts", @@ -60,6 +68,7 @@ "@types/react-dom": "^19.1.6", "astro": "5.11.0", "bcryptjs": "^3.0.2", + "better-auth": "^1.2.12", "canvas-confetti": "^1.9.3", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -85,9 +94,11 @@ "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.3.0", "@types/bcryptjs": "^3.0.0", + "@types/bun": "^1.2.18", "@types/jsonwebtoken": "^9.0.10", "@types/uuid": "^10.0.0", "@vitejs/plugin-react": "^4.6.0", + "drizzle-kit": "^0.31.4", "jsdom": "^26.1.0", "tsx": "^4.20.3", "vitest": "^3.2.4" diff --git a/scripts/fix-interrupted-jobs.ts b/scripts/fix-interrupted-jobs.ts index 7ab358a..21d7860 100644 --- a/scripts/fix-interrupted-jobs.ts +++ b/scripts/fix-interrupted-jobs.ts @@ -10,7 +10,7 @@ */ import { db, mirrorJobs } from "../src/lib/db"; -import { eq } from "drizzle-orm"; +import { eq, and } from "drizzle-orm"; // Parse command line arguments const args = process.argv.slice(2); @@ -21,18 +21,19 @@ async function fixInterruptedJobs() { console.log("Checking for interrupted jobs..."); // Build the query - let query = db - .select() - .from(mirrorJobs) - .where(eq(mirrorJobs.inProgress, true)); + const whereConditions = userId + ? and(eq(mirrorJobs.inProgress, true), eq(mirrorJobs.userId, userId)) + : eq(mirrorJobs.inProgress, true); if (userId) { console.log(`Filtering for user: ${userId}`); - query = query.where(eq(mirrorJobs.userId, userId)); } // Find all in-progress jobs - const inProgressJobs = await query; + const inProgressJobs = await db + .select() + .from(mirrorJobs) + .where(whereConditions); if (inProgressJobs.length === 0) { console.log("No interrupted jobs found."); @@ -45,7 +46,7 @@ async function fixInterruptedJobs() { }); // Mark all in-progress jobs as failed - let updateQuery = db + await db .update(mirrorJobs) .set({ inProgress: false, @@ -53,13 +54,7 @@ async function fixInterruptedJobs() { status: "failed", message: "Job interrupted and marked as failed by cleanup script" }) - .where(eq(mirrorJobs.inProgress, true)); - - if (userId) { - updateQuery = updateQuery.where(eq(mirrorJobs.userId, userId)); - } - - await updateQuery; + .where(whereConditions); console.log(`โœ… Successfully marked ${inProgressJobs.length} interrupted jobs as failed.`); console.log("These jobs can now be deleted through the normal cleanup process."); diff --git a/scripts/generate-better-auth-schema.ts b/scripts/generate-better-auth-schema.ts new file mode 100644 index 0000000..5f5cef1 --- /dev/null +++ b/scripts/generate-better-auth-schema.ts @@ -0,0 +1,110 @@ +#!/usr/bin/env bun + +import { betterAuth } from "better-auth"; +import { drizzleAdapter } from "better-auth/adapters/drizzle"; +import Database from "bun:sqlite"; +import { drizzle } from "drizzle-orm/bun-sqlite"; +import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core"; + +// Create a minimal auth instance just for schema generation +const tempDb = new Database(":memory:"); +const db = drizzle({ client: tempDb }); + +// Minimal auth config for schema generation +const auth = betterAuth({ + database: drizzleAdapter(db, { + provider: "sqlite", + usePlural: true, + }), + emailAndPassword: { + enabled: true, + }, +}); + +// Generate the schema +// Note: $internal API is not available in current better-auth version +// const schema = auth.$internal.schema; + +console.log("Better Auth Tables Required:"); +console.log("============================"); + +// Convert Better Auth schema to Drizzle schema definitions +const drizzleSchemaCode = `// Better Auth Tables - Generated Schema +import { sqliteTable, text, integer, index } from "drizzle-orm/sqlite-core"; +import { sql } from "drizzle-orm"; + +// Sessions table +export const sessions = sqliteTable("sessions", { + id: text("id").primaryKey(), + token: text("token").notNull().unique(), + userId: text("user_id").notNull().references(() => users.id), + expiresAt: integer("expires_at", { mode: "timestamp" }).notNull(), + ipAddress: text("ip_address"), + userAgent: text("user_agent"), + createdAt: integer("created_at", { mode: "timestamp" }) + .notNull() + .default(sql\`(unixepoch())\`), + updatedAt: integer("updated_at", { mode: "timestamp" }) + .notNull() + .default(sql\`(unixepoch())\`), +}, (table) => { + return { + userIdIdx: index("idx_sessions_user_id").on(table.userId), + tokenIdx: index("idx_sessions_token").on(table.token), + expiresAtIdx: index("idx_sessions_expires_at").on(table.expiresAt), + }; +}); + +// Accounts table (for OAuth providers and credentials) +export const accounts = sqliteTable("accounts", { + id: text("id").primaryKey(), + userId: text("user_id").notNull().references(() => users.id), + providerId: text("provider_id").notNull(), + providerUserId: text("provider_user_id").notNull(), + accessToken: text("access_token"), + refreshToken: text("refresh_token"), + expiresAt: integer("expires_at", { mode: "timestamp" }), + password: text("password"), // For credential provider + createdAt: integer("created_at", { mode: "timestamp" }) + .notNull() + .default(sql\`(unixepoch())\`), + updatedAt: integer("updated_at", { mode: "timestamp" }) + .notNull() + .default(sql\`(unixepoch())\`), +}, (table) => { + return { + userIdIdx: index("idx_accounts_user_id").on(table.userId), + providerIdx: index("idx_accounts_provider").on(table.providerId, table.providerUserId), + }; +}); + +// Verification tokens table +export const verificationTokens = sqliteTable("verification_tokens", { + id: text("id").primaryKey(), + token: text("token").notNull().unique(), + identifier: text("identifier").notNull(), + type: text("type").notNull(), // email, password-reset, etc + expiresAt: integer("expires_at", { mode: "timestamp" }).notNull(), + createdAt: integer("created_at", { mode: "timestamp" }) + .notNull() + .default(sql\`(unixepoch())\`), +}, (table) => { + return { + tokenIdx: index("idx_verification_tokens_token").on(table.token), + identifierIdx: index("idx_verification_tokens_identifier").on(table.identifier), + }; +}); + +// Future: SSO and OIDC Provider tables will be added when we enable those plugins +`; + +console.log(drizzleSchemaCode); + +// Output information about the schema +console.log("\n\nSummary:"); +console.log("========="); +console.log("- Better Auth will modify the existing 'users' table"); +console.log("- New tables required: sessions, accounts, verification_tokens"); +console.log("\nNote: The 'users' table needs emailVerified field added"); + +tempDb.close(); \ No newline at end of file diff --git a/scripts/gitea-mirror-lxc-local.sh b/scripts/gitea-mirror-lxc-local.sh index 339b62a..2168c1b 100755 --- a/scripts/gitea-mirror-lxc-local.sh +++ b/scripts/gitea-mirror-lxc-local.sh @@ -7,7 +7,7 @@ CONTAINER="gitea-test" IMAGE="ubuntu:22.04" INSTALL_DIR="/opt/gitea-mirror" PORT=4321 -JWT_SECRET="$(openssl rand -hex 32)" +BETTER_AUTH_SECRET="$(openssl rand -hex 32)" BUN_ZIP="/tmp/bun-linux-x64.zip" BUN_URL="https://github.com/oven-sh/bun/releases/latest/download/bun-linux-x64.zip" @@ -73,7 +73,7 @@ Environment=NODE_ENV=production Environment=HOST=0.0.0.0 Environment=PORT=$PORT Environment=DATABASE_URL=file:data/gitea-mirror.db -Environment=JWT_SECRET=$JWT_SECRET +Environment=BETTER_AUTH_SECRET=$BETTER_AUTH_SECRET [Install] WantedBy=multi-user.target SERVICE diff --git a/scripts/investigate-repo.ts b/scripts/investigate-repo.ts index 8eeab35..b40678b 100644 --- a/scripts/investigate-repo.ts +++ b/scripts/investigate-repo.ts @@ -79,11 +79,11 @@ async function investigateRepository() { if (config.length > 0) { const userConfig = config[0]; console.log(` User ID: ${userConfig.userId}`); - console.log(` GitHub Username: ${userConfig.githubConfig?.username || "Not set"}`); + console.log(` GitHub Owner: ${userConfig.githubConfig?.owner || "Not set"}`); console.log(` Gitea URL: ${userConfig.giteaConfig?.url || "Not set"}`); - console.log(` Gitea Username: ${userConfig.giteaConfig?.username || "Not set"}`); - console.log(` Preserve Org Structure: ${userConfig.githubConfig?.preserveOrgStructure || false}`); - console.log(` Mirror Issues: ${userConfig.githubConfig?.mirrorIssues || false}`); + console.log(` Gitea Default Owner: ${userConfig.giteaConfig?.defaultOwner || "Not set"}`); + console.log(` Mirror Strategy: ${userConfig.githubConfig?.mirrorStrategy || "preserve"}`); + console.log(` Include Starred: ${userConfig.githubConfig?.includeStarred || false}`); } // Check for any active jobs @@ -123,7 +123,7 @@ async function investigateRepository() { try { const giteaUrl = userConfig.giteaConfig?.url; const giteaToken = userConfig.giteaConfig?.token; - const giteaUsername = userConfig.giteaConfig?.username; + const giteaUsername = userConfig.giteaConfig?.defaultOwner; if (giteaUrl && giteaToken && giteaUsername) { const checkUrl = `${giteaUrl}/api/v1/repos/${giteaUsername}/${repo.name}`; diff --git a/scripts/manage-db.ts b/scripts/manage-db.ts index f7fa1a3..ae858de 100644 --- a/scripts/manage-db.ts +++ b/scripts/manage-db.ts @@ -1,7 +1,12 @@ import fs from "fs"; import path from "path"; import { Database } from "bun:sqlite"; +import { drizzle } from "drizzle-orm/bun-sqlite"; +import { migrate } from "drizzle-orm/bun-sqlite/migrator"; import { v4 as uuidv4 } from "uuid"; +import { users, configs, repositories, organizations, mirrorJobs, events } from "../src/lib/db/schema"; +import bcrypt from "bcryptjs"; +import { eq } from "drizzle-orm"; // Command line arguments const args = process.argv.slice(2); @@ -13,750 +18,222 @@ if (!fs.existsSync(dataDir)) { fs.mkdirSync(dataDir, { recursive: true }); } -// Database paths -const rootDbFile = path.join(process.cwd(), "gitea-mirror.db"); -const rootDevDbFile = path.join(process.cwd(), "gitea-mirror-dev.db"); -const dataDbFile = path.join(dataDir, "gitea-mirror.db"); -const dataDevDbFile = path.join(dataDir, "gitea-mirror-dev.db"); - // Database path - ensure we use absolute path const dbPath = path.join(dataDir, "gitea-mirror.db"); /** - * Ensure all required tables exist + * Initialize database with migrations */ -async function ensureTablesExist() { - // Create or open the database - const db = new Database(dbPath); - - const requiredTables = [ - "users", - "configs", - "repositories", - "organizations", - "mirror_jobs", - "events", - ]; - - for (const table of requiredTables) { - try { - // Check if table exists - const result = db.query(`SELECT name FROM sqlite_master WHERE type='table' AND name='${table}'`).get(); - - if (!result) { - console.warn(`โš ๏ธ Table '${table}' is missing. Creating it now...`); - - switch (table) { - case "users": - db.exec(` - CREATE TABLE users ( - id TEXT PRIMARY KEY, - username TEXT NOT NULL, - password TEXT NOT NULL, - email TEXT NOT NULL, - created_at INTEGER NOT NULL, - updated_at INTEGER NOT NULL - ) - `); - break; - case "configs": - db.exec(` - CREATE TABLE configs ( - id TEXT PRIMARY KEY, - user_id TEXT NOT NULL, - name TEXT NOT NULL, - is_active INTEGER NOT NULL DEFAULT 1, - github_config TEXT NOT NULL, - gitea_config TEXT NOT NULL, - include TEXT NOT NULL DEFAULT '["*"]', - exclude TEXT NOT NULL DEFAULT '[]', - schedule_config TEXT NOT NULL, - created_at INTEGER NOT NULL DEFAULT (strftime('%s','now')), - updated_at INTEGER NOT NULL DEFAULT (strftime('%s','now')), - FOREIGN KEY (user_id) REFERENCES users(id) - ) - `); - break; - case "repositories": - db.exec(` - CREATE TABLE repositories ( - id TEXT PRIMARY KEY, - user_id TEXT NOT NULL, - config_id TEXT NOT NULL, - name TEXT NOT NULL, - full_name TEXT NOT NULL, - url TEXT NOT NULL, - clone_url TEXT NOT NULL, - owner TEXT NOT NULL, - organization TEXT, - mirrored_location TEXT DEFAULT '', - is_private INTEGER NOT NULL DEFAULT 0, - is_fork INTEGER NOT NULL DEFAULT 0, - forked_from TEXT, - has_issues INTEGER NOT NULL DEFAULT 0, - is_starred INTEGER NOT NULL DEFAULT 0, - is_archived INTEGER NOT NULL DEFAULT 0, - size INTEGER NOT NULL DEFAULT 0, - has_lfs INTEGER NOT NULL DEFAULT 0, - has_submodules INTEGER NOT NULL DEFAULT 0, - default_branch TEXT NOT NULL, - visibility TEXT NOT NULL DEFAULT 'public', - status TEXT NOT NULL DEFAULT 'imported', - last_mirrored INTEGER, - error_message TEXT, - created_at INTEGER NOT NULL DEFAULT (strftime('%s','now')), - updated_at INTEGER NOT NULL DEFAULT (strftime('%s','now')), - FOREIGN KEY (user_id) REFERENCES users(id), - FOREIGN KEY (config_id) REFERENCES configs(id) - ) - `); - break; - case "organizations": - db.exec(` - CREATE TABLE organizations ( - id TEXT PRIMARY KEY, - user_id TEXT NOT NULL, - config_id TEXT NOT NULL, - name TEXT NOT NULL, - avatar_url TEXT NOT NULL, - membership_role TEXT NOT NULL DEFAULT 'member', - is_included INTEGER NOT NULL DEFAULT 1, - status TEXT NOT NULL DEFAULT 'imported', - last_mirrored INTEGER, - error_message TEXT, - repository_count INTEGER NOT NULL DEFAULT 0, - created_at INTEGER NOT NULL DEFAULT (strftime('%s','now')), - updated_at INTEGER NOT NULL DEFAULT (strftime('%s','now')), - FOREIGN KEY (user_id) REFERENCES users(id), - FOREIGN KEY (config_id) REFERENCES configs(id) - ) - `); - break; - case "mirror_jobs": - db.exec(` - CREATE TABLE mirror_jobs ( - id TEXT PRIMARY KEY, - user_id TEXT NOT NULL, - repository_id TEXT, - repository_name TEXT, - organization_id TEXT, - organization_name TEXT, - details TEXT, - status TEXT NOT NULL DEFAULT 'imported', - message TEXT NOT NULL, - timestamp TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - - -- New fields for job resilience - job_type TEXT NOT NULL DEFAULT 'mirror', - batch_id TEXT, - total_items INTEGER, - completed_items INTEGER DEFAULT 0, - item_ids TEXT, -- JSON array as text - completed_item_ids TEXT DEFAULT '[]', -- JSON array as text - in_progress INTEGER NOT NULL DEFAULT 0, -- Boolean as integer - started_at TIMESTAMP, - completed_at TIMESTAMP, - last_checkpoint TIMESTAMP, - - FOREIGN KEY (user_id) REFERENCES users(id) - ) - `); - - // Create indexes for better performance - db.exec(` - CREATE INDEX IF NOT EXISTS idx_mirror_jobs_user_id ON mirror_jobs(user_id); - CREATE INDEX IF NOT EXISTS idx_mirror_jobs_batch_id ON mirror_jobs(batch_id); - CREATE INDEX IF NOT EXISTS idx_mirror_jobs_in_progress ON mirror_jobs(in_progress); - CREATE INDEX IF NOT EXISTS idx_mirror_jobs_job_type ON mirror_jobs(job_type); - CREATE INDEX IF NOT EXISTS idx_mirror_jobs_timestamp ON mirror_jobs(timestamp); - `); - break; - case "events": - db.exec(` - CREATE TABLE events ( - id TEXT PRIMARY KEY, - user_id TEXT NOT NULL, - channel TEXT NOT NULL, - payload TEXT NOT NULL, - read INTEGER NOT NULL DEFAULT 0, - created_at INTEGER NOT NULL DEFAULT (strftime('%s','now')), - FOREIGN KEY (user_id) REFERENCES users(id) - ) - `); - db.exec(` - CREATE INDEX idx_events_user_channel ON events(user_id, channel); - CREATE INDEX idx_events_created_at ON events(created_at); - CREATE INDEX idx_events_read ON events(read); - `); - break; - } - console.log(`โœ… Table '${table}' created successfully.`); - } - } catch (error) { - console.error(`โŒ Error checking table '${table}':`, error); - process.exit(1); - } +async function initDatabase() { + console.log("๐Ÿ“ฆ Initializing database..."); + + // Create an empty database file if it doesn't exist + if (!fs.existsSync(dbPath)) { + fs.writeFileSync(dbPath, ""); } - // Migration: Add cleanup_config column to existing configs table + // Create SQLite instance + const sqlite = new Database(dbPath); + const db = drizzle({ client: sqlite }); + + // Run migrations + console.log("๐Ÿ”„ Running migrations..."); try { - const db = new Database(dbPath); - - // Check if cleanup_config column exists - const tableInfo = db.query(`PRAGMA table_info(configs)`).all(); - const hasCleanupConfig = tableInfo.some((column: any) => column.name === 'cleanup_config'); - - if (!hasCleanupConfig) { - console.log("Adding cleanup_config column to configs table..."); - - // Add the column with a default value - const defaultCleanupConfig = JSON.stringify({ - enabled: false, - retentionDays: 7, - lastRun: null, - nextRun: null, - }); - - db.exec(`ALTER TABLE configs ADD COLUMN cleanup_config TEXT NOT NULL DEFAULT '${defaultCleanupConfig}'`); - console.log("โœ… cleanup_config column added successfully."); - } + migrate(db, { migrationsFolder: "./drizzle" }); + console.log("โœ… Migrations completed successfully"); } catch (error) { - console.error("โŒ Error during cleanup_config migration:", error); - // Don't exit here as this is not critical for basic functionality + console.error("โŒ Error running migrations:", error); + throw error; } + + sqlite.close(); + console.log("โœ… Database initialized successfully"); } /** * Check database status */ async function checkDatabase() { - console.log("Checking database status..."); - - // Check for database files in the root directory (which is incorrect) - if (fs.existsSync(rootDbFile)) { - console.warn( - "โš ๏ธ WARNING: Database file found in root directory: gitea-mirror.db" - ); - console.warn("This file should be in the data directory."); - console.warn( - 'Run "bun run manage-db fix" to fix this issue or "bun run cleanup-db" to remove it.' - ); + console.log("๐Ÿ” Checking database status..."); + + if (!fs.existsSync(dbPath)) { + console.log("โŒ Database does not exist at:", dbPath); + console.log("๐Ÿ’ก Run 'bun run init-db' to create the database"); + process.exit(1); } - // Check if database files exist in the data directory (which is correct) - if (fs.existsSync(dataDbFile)) { - console.log( - "โœ… Database file found in data directory: data/gitea-mirror.db" - ); - - // Check for users - try { - const db = new Database(dbPath); - - // Check for users - const userCountResult = db.query(`SELECT COUNT(*) as count FROM users`).get(); - const userCount = userCountResult?.count || 0; - - if (userCount === 0) { - console.log("โ„น๏ธ No users found in the database."); - console.log( - "When you start the application, you will be directed to the signup page" - ); - console.log("to create an initial admin account."); - } else { - console.log(`โœ… ${userCount} user(s) found in the database.`); - console.log("The application will show the login page on startup."); - } - - // Check for configurations - const configCountResult = db.query(`SELECT COUNT(*) as count FROM configs`).get(); - const configCount = configCountResult?.count || 0; - - if (configCount === 0) { - console.log("โ„น๏ธ No configurations found in the database."); - console.log( - "You will need to set up your GitHub and Gitea configurations after login." - ); - } else { - console.log( - `โœ… ${configCount} configuration(s) found in the database.` - ); - } - } catch (error) { - console.error("โŒ Error connecting to the database:", error); - console.warn( - 'The database file might be corrupted. Consider running "bun run manage-db init" to recreate it.' - ); - } - } else { - console.warn("โš ๏ธ WARNING: Database file not found in data directory."); - console.warn('Run "bun run manage-db init" to create it.'); - } -} - -// Database schema updates and migrations have been removed -// since the application is not used by anyone yet - -/** - * Initialize the database - */ -async function initializeDatabase() { - // Check if database already exists first - if (fs.existsSync(dataDbFile)) { - console.log("โš ๏ธ Database already exists at data/gitea-mirror.db"); - console.log( - 'If you want to recreate the database, run "bun run cleanup-db" first.' - ); - console.log( - 'Or use "bun run manage-db reset-users" to just remove users without recreating tables.' - ); - - // Check if we can connect to it - try { - const db = new Database(dbPath); - db.query(`SELECT COUNT(*) as count FROM users`).get(); - console.log("โœ… Database is valid and accessible."); - return; - } catch (error) { - console.error("โŒ Error connecting to the existing database:", error); - console.log( - "The database might be corrupted. Proceeding with reinitialization..." - ); - } - } - - console.log(`Initializing database at ${dbPath}...`); + const sqlite = new Database(dbPath); + const db = drizzle({ client: sqlite }); try { - const db = new Database(dbPath); + // Check tables + const tables = sqlite.query( + "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name" + ).all() as Array<{name: string}>; - // Create tables if they don't exist - db.exec(` - CREATE TABLE IF NOT EXISTS users ( - id TEXT PRIMARY KEY, - username TEXT NOT NULL, - password TEXT NOT NULL, - email TEXT NOT NULL, - created_at INTEGER NOT NULL, - updated_at INTEGER NOT NULL - ) - `); + console.log("\n๐Ÿ“Š Tables found:"); + for (const table of tables) { + const count = sqlite.query(`SELECT COUNT(*) as count FROM ${table.name}`).get() as {count: number}; + console.log(` - ${table.name}: ${count.count} records`); + } - // NOTE: We no longer create a default admin user - user will create one via signup page + // Check migrations + const migrations = sqlite.query( + "SELECT * FROM __drizzle_migrations ORDER BY created_at DESC LIMIT 5" + ).all() as Array<{hash: string, created_at: number}>; - db.exec(` - CREATE TABLE IF NOT EXISTS configs ( - id TEXT PRIMARY KEY, - user_id TEXT NOT NULL, - name TEXT NOT NULL, - is_active INTEGER NOT NULL DEFAULT 1, - github_config TEXT NOT NULL, - gitea_config TEXT NOT NULL, - include TEXT NOT NULL DEFAULT '["*"]', - exclude TEXT NOT NULL DEFAULT '[]', - schedule_config TEXT NOT NULL, - cleanup_config TEXT NOT NULL, - created_at INTEGER NOT NULL DEFAULT (strftime('%s','now')), - updated_at INTEGER NOT NULL DEFAULT (strftime('%s','now')), - FOREIGN KEY (user_id) REFERENCES users(id) - ) - `); - - db.exec(` - CREATE TABLE IF NOT EXISTS repositories ( - id TEXT PRIMARY KEY, - user_id TEXT NOT NULL, - config_id TEXT NOT NULL, - name TEXT NOT NULL, - full_name TEXT NOT NULL, - url TEXT NOT NULL, - clone_url TEXT NOT NULL, - owner TEXT NOT NULL, - organization TEXT, - mirrored_location TEXT DEFAULT '', - is_private INTEGER NOT NULL DEFAULT 0, - is_fork INTEGER NOT NULL DEFAULT 0, - forked_from TEXT, - has_issues INTEGER NOT NULL DEFAULT 0, - is_starred INTEGER NOT NULL DEFAULT 0, - is_archived INTEGER NOT NULL DEFAULT 0, - size INTEGER NOT NULL DEFAULT 0, - has_lfs INTEGER NOT NULL DEFAULT 0, - has_submodules INTEGER NOT NULL DEFAULT 0, - default_branch TEXT NOT NULL, - visibility TEXT NOT NULL DEFAULT 'public', - status TEXT NOT NULL DEFAULT 'imported', - last_mirrored INTEGER, - error_message TEXT, - created_at INTEGER NOT NULL DEFAULT (strftime('%s','now')), - updated_at INTEGER NOT NULL DEFAULT (strftime('%s','now')), - FOREIGN KEY (user_id) REFERENCES users(id), - FOREIGN KEY (config_id) REFERENCES configs(id) - ) - `); - - db.exec(` - CREATE TABLE IF NOT EXISTS organizations ( - id TEXT PRIMARY KEY, - user_id TEXT NOT NULL, - config_id TEXT NOT NULL, - name TEXT NOT NULL, - avatar_url TEXT NOT NULL, - membership_role TEXT NOT NULL DEFAULT 'member', - is_included INTEGER NOT NULL DEFAULT 1, - status TEXT NOT NULL DEFAULT 'imported', - last_mirrored INTEGER, - error_message TEXT, - repository_count INTEGER NOT NULL DEFAULT 0, - created_at INTEGER NOT NULL DEFAULT (strftime('%s','now')), - updated_at INTEGER NOT NULL DEFAULT (strftime('%s','now')), - FOREIGN KEY (user_id) REFERENCES users(id), - FOREIGN KEY (config_id) REFERENCES configs(id) - ) - `); - - db.exec(` - CREATE TABLE IF NOT EXISTS mirror_jobs ( - id TEXT PRIMARY KEY, - user_id TEXT NOT NULL, - repository_id TEXT, - repository_name TEXT, - organization_id TEXT, - organization_name TEXT, - details TEXT, - status TEXT NOT NULL DEFAULT 'imported', - message TEXT NOT NULL, - timestamp TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (user_id) REFERENCES users(id) - ) - `); - - db.exec(` - CREATE TABLE IF NOT EXISTS events ( - id TEXT PRIMARY KEY, - user_id TEXT NOT NULL, - channel TEXT NOT NULL, - payload TEXT NOT NULL, - read INTEGER NOT NULL DEFAULT 0, - created_at INTEGER NOT NULL DEFAULT (strftime('%s','now')), - FOREIGN KEY (user_id) REFERENCES users(id) - ) - `); - - db.exec(` - CREATE INDEX IF NOT EXISTS idx_events_user_channel ON events(user_id, channel); - CREATE INDEX IF NOT EXISTS idx_events_created_at ON events(created_at); - CREATE INDEX IF NOT EXISTS idx_events_read ON events(read); - `); - - // Insert default config if none exists - const configCountResult = db.query(`SELECT COUNT(*) as count FROM configs`).get(); - const configCount = configCountResult?.count || 0; - - if (configCount === 0) { - // Get the first user - const firstUserResult = db.query(`SELECT id FROM users LIMIT 1`).get(); - - if (firstUserResult) { - const userId = firstUserResult.id; - const configId = uuidv4(); - const githubConfig = JSON.stringify({ - username: process.env.GITHUB_USERNAME || "", - token: process.env.GITHUB_TOKEN || "", - skipForks: false, - privateRepositories: false, - mirrorIssues: false, - mirrorStarred: true, - useSpecificUser: false, - preserveOrgStructure: true, - skipStarredIssues: false, - }); - const giteaConfig = JSON.stringify({ - url: process.env.GITEA_URL || "", - token: process.env.GITEA_TOKEN || "", - username: process.env.GITEA_USERNAME || "", - organization: "", - visibility: "public", - starredReposOrg: "github", - }); - const include = JSON.stringify(["*"]); - const exclude = JSON.stringify([]); - const scheduleConfig = JSON.stringify({ - enabled: false, - interval: 3600, - lastRun: null, - nextRun: null, - }); - const cleanupConfig = JSON.stringify({ - enabled: false, - retentionDays: 7, - lastRun: null, - nextRun: null, - }); - - const stmt = db.prepare(` - INSERT INTO configs (id, user_id, name, is_active, github_config, gitea_config, include, exclude, schedule_config, cleanup_config, created_at, updated_at) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - `); - - stmt.run( - configId, - userId, - "Default Configuration", - 1, - githubConfig, - giteaConfig, - include, - exclude, - scheduleConfig, - cleanupConfig, - Date.now(), - Date.now() - ); + if (migrations.length > 0) { + console.log("\n๐Ÿ“‹ Recent migrations:"); + for (const migration of migrations) { + const date = new Date(migration.created_at); + console.log(` - ${migration.hash} (${date.toLocaleString()})`); } } - console.log("โœ… Database initialization completed successfully."); + sqlite.close(); + console.log("\nโœ… Database check complete"); } catch (error) { - console.error("โŒ Error initializing database:", error); + console.error("โŒ Error checking database:", error); + sqlite.close(); process.exit(1); } } /** - * Reset users in the database + * Reset user accounts (development only) */ async function resetUsers() { - console.log(`Resetting users in database at ${dbPath}...`); - - try { - // Check if the database exists - const doesDbExist = fs.existsSync(dbPath); - - if (!doesDbExist) { - console.log( - "โŒ Database file doesn't exist. Run 'bun run manage-db init' first to create it." - ); - return; - } - - const db = new Database(dbPath); - - // Count existing users - const userCountResult = db.query(`SELECT COUNT(*) as count FROM users`).get(); - const userCount = userCountResult?.count || 0; - - if (userCount === 0) { - console.log("โ„น๏ธ No users found in the database. Nothing to reset."); - return; - } - - // Delete all users - db.exec(`DELETE FROM users`); - console.log(`โœ… Deleted ${userCount} users from the database.`); - - // Check dependent configurations that need to be removed - const configCountResult = db.query(`SELECT COUNT(*) as count FROM configs`).get(); - const configCount = configCountResult?.count || 0; - - if (configCount > 0) { - db.exec(`DELETE FROM configs`); - console.log(`โœ… Deleted ${configCount} configurations.`); - } - - // Check for dependent repositories - const repoCountResult = db.query(`SELECT COUNT(*) as count FROM repositories`).get(); - const repoCount = repoCountResult?.count || 0; - - if (repoCount > 0) { - db.exec(`DELETE FROM repositories`); - console.log(`โœ… Deleted ${repoCount} repositories.`); - } - - // Check for dependent organizations - const orgCountResult = db.query(`SELECT COUNT(*) as count FROM organizations`).get(); - const orgCount = orgCountResult?.count || 0; - - if (orgCount > 0) { - db.exec(`DELETE FROM organizations`); - console.log(`โœ… Deleted ${orgCount} organizations.`); - } - - // Check for dependent mirror jobs - const jobCountResult = db.query(`SELECT COUNT(*) as count FROM mirror_jobs`).get(); - const jobCount = jobCountResult?.count || 0; - - if (jobCount > 0) { - db.exec(`DELETE FROM mirror_jobs`); - console.log(`โœ… Deleted ${jobCount} mirror jobs.`); - } - - console.log( - "โœ… Database has been reset. The application will now prompt for a new admin account setup on next run." - ); - } catch (error) { - console.error("โŒ Error resetting users:", error); + console.log("๐Ÿ—‘๏ธ Resetting all user accounts..."); + + if (!fs.existsSync(dbPath)) { + console.log("โŒ Database does not exist"); process.exit(1); } + + const sqlite = new Database(dbPath); + const db = drizzle({ client: sqlite }); + + try { + // Delete all data in order of foreign key dependencies + await db.delete(events); + await db.delete(mirrorJobs); + await db.delete(repositories); + await db.delete(organizations); + await db.delete(configs); + await db.delete(users); + + console.log("โœ… All user accounts and related data have been removed"); + + sqlite.close(); + } catch (error) { + console.error("โŒ Error resetting users:", error); + sqlite.close(); + process.exit(1); + } +} + +/** + * Clean up database files + */ +async function cleanupDatabase() { + console.log("๐Ÿงน Cleaning up database files..."); + + const filesToRemove = [ + dbPath, + path.join(dataDir, "gitea-mirror-dev.db"), + path.join(process.cwd(), "gitea-mirror.db"), + path.join(process.cwd(), "gitea-mirror-dev.db"), + ]; + + for (const file of filesToRemove) { + if (fs.existsSync(file)) { + fs.unlinkSync(file); + console.log(` - Removed: ${file}`); + } + } + + console.log("โœ… Database cleanup complete"); } /** * Fix database location issues */ -async function fixDatabaseIssues() { - console.log("Checking for database issues..."); +async function fixDatabase() { + console.log("๐Ÿ”ง Fixing database location issues..."); + + // Legacy database paths + const rootDbFile = path.join(process.cwd(), "gitea-mirror.db"); + const rootDevDbFile = path.join(process.cwd(), "gitea-mirror-dev.db"); + const dataDevDbFile = path.join(dataDir, "gitea-mirror-dev.db"); - // Check for database files in the root directory + // Check for databases in wrong locations if (fs.existsSync(rootDbFile)) { - console.log("Found database file in root directory: gitea-mirror.db"); - - // If the data directory doesn't have the file, move it there - if (!fs.existsSync(dataDbFile)) { - console.log("Moving database file to data directory..."); - fs.copyFileSync(rootDbFile, dataDbFile); - console.log("Database file moved successfully."); + console.log("๐Ÿ“ Found database in root directory"); + if (!fs.existsSync(dbPath)) { + console.log(" โ†’ Moving to data directory..."); + fs.renameSync(rootDbFile, dbPath); + console.log("โœ… Database moved successfully"); } else { - console.log( - "Database file already exists in data directory. Checking for differences..." - ); - - // Compare file sizes to see which is newer/larger - const rootStats = fs.statSync(rootDbFile); - const dataStats = fs.statSync(dataDbFile); - - if ( - rootStats.size > dataStats.size || - rootStats.mtime > dataStats.mtime - ) { - console.log( - "Root database file is newer or larger. Backing up data directory file and replacing it..." - ); - fs.copyFileSync(dataDbFile, `${dataDbFile}.backup-${Date.now()}`); - fs.copyFileSync(rootDbFile, dataDbFile); - console.log("Database file replaced successfully."); - } + console.log(" โš ๏ธ Database already exists in data directory"); + console.log(" โ†’ Keeping existing data directory database"); + fs.unlinkSync(rootDbFile); + console.log(" โ†’ Removed root directory database"); } - - // Remove the root file - console.log("Removing database file from root directory..."); - fs.unlinkSync(rootDbFile); - console.log("Root database file removed."); } - // Do the same for dev database + // Clean up dev databases if (fs.existsSync(rootDevDbFile)) { - console.log( - "Found development database file in root directory: gitea-mirror-dev.db" - ); - - // If the data directory doesn't have the file, move it there - if (!fs.existsSync(dataDevDbFile)) { - console.log("Moving development database file to data directory..."); - fs.copyFileSync(rootDevDbFile, dataDevDbFile); - console.log("Development database file moved successfully."); - } else { - console.log( - "Development database file already exists in data directory. Checking for differences..." - ); - - // Compare file sizes to see which is newer/larger - const rootStats = fs.statSync(rootDevDbFile); - const dataStats = fs.statSync(dataDevDbFile); - - if ( - rootStats.size > dataStats.size || - rootStats.mtime > dataStats.mtime - ) { - console.log( - "Root development database file is newer or larger. Backing up data directory file and replacing it..." - ); - fs.copyFileSync(dataDevDbFile, `${dataDevDbFile}.backup-${Date.now()}`); - fs.copyFileSync(rootDevDbFile, dataDevDbFile); - console.log("Development database file replaced successfully."); - } - } - - // Remove the root file - console.log("Removing development database file from root directory..."); fs.unlinkSync(rootDevDbFile); - console.log("Root development database file removed."); + console.log(" โ†’ Removed root dev database"); + } + if (fs.existsSync(dataDevDbFile)) { + fs.unlinkSync(dataDevDbFile); + console.log(" โ†’ Removed data dev database"); } - // Check if database files exist in the data directory - if (!fs.existsSync(dataDbFile)) { - console.warn( - "โš ๏ธ WARNING: Production database file not found in data directory." - ); - console.warn('Run "bun run manage-db init" to create it.'); - } else { - console.log("โœ… Production database file found in data directory."); - - // Check if we can connect to the database - try { - // Try to query the database - const db = new Database(dbPath); - db.query(`SELECT 1 FROM sqlite_master LIMIT 1`).get(); - console.log(`โœ… Successfully connected to the database.`); - } catch (error) { - console.error("โŒ Error connecting to the database:", error); - console.warn( - 'The database file might be corrupted. Consider running "bun run manage-db init" to recreate it.' - ); - } - } - - console.log("Database check completed."); + console.log("โœ… Database location fixed"); } /** - * Main function to handle the command + * Auto mode - check and initialize if needed */ -async function main() { - console.log(`Database Management Tool for Gitea Mirror`); - - // Ensure all required tables exist - console.log("Ensuring all required tables exist..."); - await ensureTablesExist(); - - switch (command) { - case "check": - await checkDatabase(); - break; - case "init": - await initializeDatabase(); - break; - case "fix": - await fixDatabaseIssues(); - break; - case "reset-users": - await resetUsers(); - break; - case "auto": - // Auto mode: check, fix, and initialize if needed - console.log("Running in auto mode: check, fix, and initialize if needed"); - await fixDatabaseIssues(); - - if (!fs.existsSync(dataDbFile)) { - await initializeDatabase(); - } else { - await checkDatabase(); - } - break; - default: - console.log(` -Available commands: - check - Check database status - init - Initialize the database (only if it doesn't exist) - fix - Fix database location issues - reset-users - Remove all users and their data - auto - Automatic mode: check, fix, and initialize if needed - -Usage: bun run manage-db [command] -`); +async function autoMode() { + if (!fs.existsSync(dbPath)) { + console.log("๐Ÿ“ฆ Database not found, initializing..."); + await initDatabase(); + } else { + console.log("โœ… Database already exists"); + await checkDatabase(); } } -main().catch((error) => { - console.error("Error during database management:", error); - process.exit(1); -}); +// Execute command +switch (command) { + case "init": + await initDatabase(); + break; + case "check": + await checkDatabase(); + break; + case "fix": + await fixDatabase(); + break; + case "reset-users": + await resetUsers(); + break; + case "cleanup": + await cleanupDatabase(); + break; + case "auto": + await autoMode(); + break; + default: + console.log("Available commands:"); + console.log(" init - Initialize database with migrations"); + console.log(" check - Check database status"); + console.log(" fix - Fix database location issues"); + console.log(" reset-users - Remove all users and related data"); + console.log(" cleanup - Remove all database files"); + console.log(" auto - Auto initialize if needed"); + process.exit(1); +} \ No newline at end of file diff --git a/scripts/migrate-better-auth.ts b/scripts/migrate-better-auth.ts new file mode 100644 index 0000000..52c99ac --- /dev/null +++ b/scripts/migrate-better-auth.ts @@ -0,0 +1,100 @@ +#!/usr/bin/env bun +import { db } from "../src/lib/db"; +import { accounts } from "../src/lib/db/schema"; +import { sql } from "drizzle-orm"; + +console.log("๐Ÿ”„ Starting Better Auth migration..."); + +async function migrateToBetterAuth() { + try { + // Check if migration is needed + const existingAccounts = await db.select().from(accounts).limit(1); + if (existingAccounts.length > 0) { + console.log("โœ“ Better Auth migration already completed"); + return; + } + + // Check if we have old users table with passwords + // This query checks if password column exists in users table + const hasPasswordColumn = await db.get<{ count: number }>( + sql`SELECT COUNT(*) as count FROM pragma_table_info('users') WHERE name = 'password'` + ); + + if (!hasPasswordColumn || hasPasswordColumn.count === 0) { + console.log("โ„น๏ธ Users table doesn't have password column - migration may have already been done"); + + // Check if we have any users without accounts + const usersWithoutAccounts = await db.all<{ id: string; email: string }>( + sql`SELECT u.id, u.email FROM users u LEFT JOIN accounts a ON u.id = a.user_id WHERE a.id IS NULL` + ); + + if (usersWithoutAccounts.length === 0) { + console.log("โœ“ All users have accounts - migration complete"); + return; + } + + console.log(`โš ๏ธ Found ${usersWithoutAccounts.length} users without accounts - they may need to reset passwords`); + return; + } + + // Get all users with password hashes using raw SQL since the schema doesn't have password + const allUsersWithPasswords = await db.all<{ id: string; email: string; username: string; password: string }>( + sql`SELECT id, email, username, password FROM users WHERE password IS NOT NULL` + ); + + if (allUsersWithPasswords.length === 0) { + console.log("โ„น๏ธ No users with passwords to migrate"); + return; + } + + console.log(`๐Ÿ“Š Found ${allUsersWithPasswords.length} users to migrate`); + + // Migrate each user + for (const user of allUsersWithPasswords) { + try { + // Create Better Auth account entry + await db.insert(accounts).values({ + id: crypto.randomUUID(), + userId: user.id, + accountId: user.email, // Use email as account ID + providerId: "credential", // Better Auth credential provider + providerUserId: null, + accessToken: null, + refreshToken: null, + expiresAt: null, + password: user.password, // Move password hash to accounts table + createdAt: new Date(), + updatedAt: new Date() + }); + + console.log(`โœ“ Migrated user: ${user.email}`); + } catch (error) { + console.error(`โŒ Failed to migrate user ${user.email}:`, error); + // Continue with other users even if one fails + } + } + + // Remove password column from users table if it exists + console.log("๐Ÿ”„ Cleaning up old password column..."); + try { + // SQLite doesn't support DROP COLUMN directly, so we need to recreate the table + // For now, we'll just leave it as is since it's not harmful + console.log("โ„น๏ธ Password column left in users table for compatibility"); + } catch (error) { + console.error("โš ๏ธ Could not remove password column:", error); + } + + console.log("โœ… Better Auth migration completed successfully"); + + // Verify migration + const migratedAccounts = await db.select().from(accounts); + console.log(`๐Ÿ“Š Total accounts after migration: ${migratedAccounts.length}`); + + } catch (error) { + console.error("โŒ Better Auth migration failed:", error); + process.exit(1); + } +} + +// Run migration +migrateToBetterAuth(); \ No newline at end of file diff --git a/scripts/migrate-to-better-auth.ts b/scripts/migrate-to-better-auth.ts new file mode 100644 index 0000000..9790021 --- /dev/null +++ b/scripts/migrate-to-better-auth.ts @@ -0,0 +1,87 @@ +#!/usr/bin/env bun + +import { db, users, accounts } from "../src/lib/db"; +import { eq } from "drizzle-orm"; +import { v4 as uuidv4 } from "uuid"; + +/** + * Migrate existing users to Better Auth schema + * + * This script: + * 1. Moves existing password hashes from users table to accounts table + * 2. Updates user data to match Better Auth requirements + * 3. Creates credential accounts for existing users + */ + +async function migrateUsers() { + console.log("๐Ÿ”„ Starting user migration to Better Auth..."); + + try { + // Get all existing users + const existingUsers = await db.select().from(users); + + if (existingUsers.length === 0) { + console.log("โœ… No users to migrate"); + return; + } + + console.log(`Found ${existingUsers.length} users to migrate`); + + for (const user of existingUsers) { + console.log(`\nMigrating user: ${user.username} (${user.email})`); + + // Check if user already has a credential account + const existingAccount = await db + .select() + .from(accounts) + .where( + eq(accounts.userId, user.id) && + eq(accounts.providerId, "credential") + ) + .limit(1); + + if (existingAccount.length > 0) { + console.log("โœ“ User already migrated"); + continue; + } + + // Create credential account with existing password hash + const accountId = uuidv4(); + await db.insert(accounts).values({ + id: accountId, + accountId: accountId, + userId: user.id, + providerId: "credential", + providerUserId: user.email, // Use email as provider user ID + // password: user.password, // Password is not in users table anymore + createdAt: user.createdAt, + updatedAt: user.updatedAt, + }); + + console.log("โœ“ Created credential account"); + + // Update user name field if it's null (Better Auth uses 'name' field) + // Note: Better Auth expects a 'name' field, but we're using username + // This is handled by our additional fields configuration + } + + console.log("\nโœ… User migration completed successfully!"); + + // Summary + const migratedAccounts = await db + .select() + .from(accounts) + .where(eq(accounts.providerId, "credential")); + + console.log(`\nMigration Summary:`); + console.log(`- Total users: ${existingUsers.length}`); + console.log(`- Migrated accounts: ${migratedAccounts.length}`); + + } catch (error) { + console.error("โŒ Migration failed:", error); + process.exit(1); + } +} + +// Run migration +migrateUsers(); \ No newline at end of file diff --git a/scripts/migrate-tokens-encryption.ts b/scripts/migrate-tokens-encryption.ts new file mode 100644 index 0000000..f9c1c6c --- /dev/null +++ b/scripts/migrate-tokens-encryption.ts @@ -0,0 +1,135 @@ +#!/usr/bin/env bun +/** + * Migration script to encrypt existing GitHub and Gitea tokens in the database + * Run with: bun run scripts/migrate-tokens-encryption.ts + */ + +import { db, configs } from "../src/lib/db"; +import { eq } from "drizzle-orm"; +import { encrypt, isEncrypted, migrateToken } from "../src/lib/utils/encryption"; + +async function migrateTokens() { + console.log("Starting token encryption migration..."); + + try { + // Fetch all configs + const allConfigs = await db.select().from(configs); + + console.log(`Found ${allConfigs.length} configurations to check`); + + let migratedCount = 0; + let skippedCount = 0; + let errorCount = 0; + + for (const config of allConfigs) { + try { + let githubUpdated = false; + let giteaUpdated = false; + + // Parse configs + const githubConfig = typeof config.githubConfig === "string" + ? JSON.parse(config.githubConfig) + : config.githubConfig; + + const giteaConfig = typeof config.giteaConfig === "string" + ? JSON.parse(config.giteaConfig) + : config.giteaConfig; + + // Check and migrate GitHub token + if (githubConfig.token) { + if (!isEncrypted(githubConfig.token)) { + console.log(`Encrypting GitHub token for config ${config.id} (user: ${config.userId})`); + githubConfig.token = encrypt(githubConfig.token); + githubUpdated = true; + } else { + console.log(`GitHub token already encrypted for config ${config.id}`); + } + } + + // Check and migrate Gitea token + if (giteaConfig.token) { + if (!isEncrypted(giteaConfig.token)) { + console.log(`Encrypting Gitea token for config ${config.id} (user: ${config.userId})`); + giteaConfig.token = encrypt(giteaConfig.token); + giteaUpdated = true; + } else { + console.log(`Gitea token already encrypted for config ${config.id}`); + } + } + + // Update config if any tokens were migrated + if (githubUpdated || giteaUpdated) { + await db + .update(configs) + .set({ + githubConfig, + giteaConfig, + updatedAt: new Date(), + }) + .where(eq(configs.id, config.id)); + + migratedCount++; + console.log(`โœ“ Config ${config.id} updated successfully`); + } else { + skippedCount++; + } + + } catch (error) { + errorCount++; + console.error(`โœ— Error processing config ${config.id}:`, error); + } + } + + console.log("\n=== Migration Summary ==="); + console.log(`Total configs: ${allConfigs.length}`); + console.log(`Migrated: ${migratedCount}`); + console.log(`Skipped (already encrypted): ${skippedCount}`); + console.log(`Errors: ${errorCount}`); + + if (errorCount > 0) { + console.error("\nโš ๏ธ Some configs failed to migrate. Please check the errors above."); + process.exit(1); + } else { + console.log("\nโœ… Token encryption migration completed successfully!"); + } + + } catch (error) { + console.error("Fatal error during migration:", error); + process.exit(1); + } +} + +// Verify environment setup +function verifyEnvironment() { + const requiredEnvVars = ["ENCRYPTION_SECRET", "JWT_SECRET", "BETTER_AUTH_SECRET"]; + const availableSecrets = requiredEnvVars.filter(varName => process.env[varName]); + + if (availableSecrets.length === 0) { + console.error("โŒ No encryption secret found!"); + console.error("Please set one of the following environment variables:"); + console.error(" - ENCRYPTION_SECRET (recommended)"); + console.error(" - JWT_SECRET"); + console.error(" - BETTER_AUTH_SECRET"); + process.exit(1); + } + + console.log(`Using encryption secret from: ${availableSecrets[0]}`); +} + +// Main execution +async function main() { + console.log("=== Gitea Mirror Token Encryption Migration ===\n"); + + // Verify environment + verifyEnvironment(); + + // Run migration + await migrateTokens(); + + process.exit(0); +} + +main().catch((error) => { + console.error("Unexpected error:", error); + process.exit(1); +}); \ No newline at end of file diff --git a/scripts/repair-mirrored-repos.ts b/scripts/repair-mirrored-repos.ts index 2b01a07..1a01b3f 100644 --- a/scripts/repair-mirrored-repos.ts +++ b/scripts/repair-mirrored-repos.ts @@ -81,21 +81,23 @@ async function repairMirroredRepositories() { try { // Find repositories that might need repair - let query = db - .select() - .from(repositories) - .where( - or( + const whereConditions = specificRepo + ? and( + or( + eq(repositories.status, "imported"), + eq(repositories.status, "failed") + ), + eq(repositories.name, specificRepo) + ) + : or( eq(repositories.status, "imported"), eq(repositories.status, "failed") - ) - ); + ); - if (specificRepo) { - query = query.where(eq(repositories.name, specificRepo)); - } - - const repos = await query; + const repos = await db + .select() + .from(repositories) + .where(whereConditions); if (repos.length === 0) { if (!isStartupMode) { @@ -137,7 +139,7 @@ async function repairMirroredRepositories() { } const userConfig = config[0]; - const giteaUsername = userConfig.giteaConfig?.username; + const giteaUsername = userConfig.giteaConfig?.defaultOwner; if (!giteaUsername) { if (!isStartupMode) { diff --git a/scripts/run-migration.ts b/scripts/run-migration.ts new file mode 100644 index 0000000..48a7c04 --- /dev/null +++ b/scripts/run-migration.ts @@ -0,0 +1,31 @@ +import { Database } from "bun:sqlite"; +import { readFileSync } from "fs"; +import path from "path"; + +const dbPath = path.join(process.cwd(), "data/gitea-mirror.db"); +const db = new Database(dbPath); + +// Read the migration file +const migrationPath = path.join(process.cwd(), "drizzle/0001_polite_exodus.sql"); +const migration = readFileSync(migrationPath, "utf-8"); + +// Split by statement-breakpoint and execute each statement +const statements = migration.split("--> statement-breakpoint").map(s => s.trim()).filter(s => s); + +try { + db.run("BEGIN TRANSACTION"); + + for (const statement of statements) { + console.log(`Executing: ${statement.substring(0, 50)}...`); + db.run(statement); + } + + db.run("COMMIT"); + console.log("Migration completed successfully!"); +} catch (error) { + db.run("ROLLBACK"); + console.error("Migration failed:", error); + process.exit(1); +} finally { + db.close(); +} \ No newline at end of file diff --git a/scripts/test-graceful-shutdown.ts b/scripts/test-graceful-shutdown.ts index 798a729..07ec2cd 100644 --- a/scripts/test-graceful-shutdown.ts +++ b/scripts/test-graceful-shutdown.ts @@ -47,7 +47,7 @@ async function createTestJob(): Promise { jobType: "mirror", totalItems: 10, itemIds: ['item-1', 'item-2', 'item-3', 'item-4', 'item-5'], - completedItemIds: ['item-1', 'item-2'], // Simulate partial completion + completedItems: 2, // Simulate partial completion inProgress: true, }); diff --git a/src/components/auth/LoginForm.tsx b/src/components/auth/LoginForm.tsx index d7a82a9..82a6884 100644 --- a/src/components/auth/LoginForm.tsx +++ b/src/components/auth/LoginForm.tsx @@ -4,50 +4,70 @@ import * as React from 'react'; import { useState } from 'react'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'; - +import { useAuth } from '@/hooks/useAuth'; +import { useAuthMethods } from '@/hooks/useAuthMethods'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { authClient } from '@/lib/auth-client'; +import { Separator } from '@/components/ui/separator'; import { toast, Toaster } from 'sonner'; import { showErrorToast } from '@/lib/utils'; +import { Loader2, Mail, Globe } from 'lucide-react'; export function LoginForm() { const [isLoading, setIsLoading] = useState(false); + const [ssoEmail, setSsoEmail] = useState(''); + const { login } = useAuth(); + const { authMethods, isLoading: isLoadingMethods } = useAuthMethods(); + + // Determine which tab to show by default + const getDefaultTab = () => { + if (authMethods.emailPassword) return 'email'; + if (authMethods.sso.enabled) return 'sso'; + return 'email'; // fallback + }; async function handleLogin(e: React.FormEvent) { e.preventDefault(); setIsLoading(true); const form = e.currentTarget; const formData = new FormData(form); - const username = formData.get('username') as string | null; + const email = formData.get('email') as string | null; const password = formData.get('password') as string | null; - if (!username || !password) { - toast.error('Please enter both username and password'); + if (!email || !password) { + toast.error('Please enter both email and password'); setIsLoading(false); return; } - const loginData = { username, password }; - try { - const response = await fetch('/api/auth/login', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(loginData), - }); + await login(email, password); + toast.success('Login successful!'); + // Small delay before redirecting to see the success message + setTimeout(() => { + window.location.href = '/'; + }, 1000); + } catch (error) { + showErrorToast(error, toast); + } finally { + setIsLoading(false); + } + } - const data = await response.json(); - - if (response.ok) { - toast.success('Login successful!'); - // Small delay before redirecting to see the success message - setTimeout(() => { - window.location.href = '/'; - }, 1000); - } else { - showErrorToast(data.error || 'Login failed. Please try again.', toast); + async function handleSSOLogin(domain?: string) { + setIsLoading(true); + try { + if (!domain && !ssoEmail) { + toast.error('Please enter your email or select a provider'); + return; } + + await authClient.signIn.sso({ + email: ssoEmail || undefined, + domain: domain, + callbackURL: '/', + }); } catch (error) { showErrorToast(error, toast); } finally { @@ -76,45 +96,182 @@ export function LoginForm() { Log in to manage your GitHub to Gitea mirroring - -
-
-
- - -
-
- - -
+ + {isLoadingMethods ? ( + +
+
- -
- - - + + ) : ( + <> + {/* Show tabs only if multiple auth methods are available */} + {authMethods.sso.enabled && authMethods.emailPassword ? ( + + + + + Email + + + + SSO + + + + + +
+
+
+ + +
+
+ + +
+
+
+
+ + + +
+ + + +
+ {authMethods.sso.providers.length > 0 && ( + <> +
+

+ Sign in with your organization account +

+ {authMethods.sso.providers.map(provider => ( + + ))} +
+ +
+
+ +
+
+ Or +
+
+ + )} + +
+ + setSsoEmail(e.target.value)} + className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring" + placeholder="Enter your work email" + disabled={isLoading} + /> +

+ We'll redirect you to your organization's SSO provider +

+
+
+
+ + + +
+
+ ) : ( + // Single auth method - show email/password only + <> + +
+
+
+ + +
+
+ + +
+
+
+
+ + + + + )} + + )} +

Don't have an account? Contact your administrator. diff --git a/src/components/auth/LoginPage.tsx b/src/components/auth/LoginPage.tsx new file mode 100644 index 0000000..abd8937 --- /dev/null +++ b/src/components/auth/LoginPage.tsx @@ -0,0 +1,10 @@ +import { LoginForm } from './LoginForm'; +import Providers from '@/components/layout/Providers'; + +export function LoginPage() { + return ( + + + + ); +} \ No newline at end of file diff --git a/src/components/auth/SignupForm.tsx b/src/components/auth/SignupForm.tsx index 53f52f1..77ee885 100644 --- a/src/components/auth/SignupForm.tsx +++ b/src/components/auth/SignupForm.tsx @@ -5,21 +5,22 @@ import { Button } from '@/components/ui/button'; import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'; import { toast, Toaster } from 'sonner'; import { showErrorToast } from '@/lib/utils'; +import { useAuth } from '@/hooks/useAuth'; export function SignupForm() { const [isLoading, setIsLoading] = useState(false); + const { register } = useAuth(); async function handleSignup(e: React.FormEvent) { e.preventDefault(); setIsLoading(true); const form = e.currentTarget; const formData = new FormData(form); - const username = formData.get('username') as string | null; const email = formData.get('email') as string | null; const password = formData.get('password') as string | null; const confirmPassword = formData.get('confirmPassword') as string | null; - if (!username || !email || !password || !confirmPassword) { + if (!email || !password || !confirmPassword) { toast.error('Please fill in all fields'); setIsLoading(false); return; @@ -31,28 +32,15 @@ export function SignupForm() { return; } - const signupData = { username, email, password }; - try { - const response = await fetch('/api/auth/register', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(signupData), - }); - - const data = await response.json(); - - if (response.ok) { - toast.success('Account created successfully! Redirecting to dashboard...'); - // Small delay before redirecting to see the success message - setTimeout(() => { - window.location.href = '/'; - }, 1500); - } else { - showErrorToast(data.error || 'Failed to create account. Please try again.', toast); - } + // Derive username from email (part before @) + const username = email.split('@')[0]; + await register(username, email, password); + toast.success('Account created successfully! Redirecting to dashboard...'); + // Small delay before redirecting to see the success message + setTimeout(() => { + window.location.href = '/'; + }, 1500); } catch (error) { showErrorToast(error, toast); } finally { @@ -84,20 +72,6 @@ export function SignupForm() {

-
- - -
diff --git a/src/components/auth/SignupPage.tsx b/src/components/auth/SignupPage.tsx new file mode 100644 index 0000000..c864ea2 --- /dev/null +++ b/src/components/auth/SignupPage.tsx @@ -0,0 +1,10 @@ +import { SignupForm } from './SignupForm'; +import Providers from '@/components/layout/Providers'; + +export function SignupPage() { + return ( + + + + ); +} \ No newline at end of file diff --git a/src/components/config/ConfigTabs.tsx b/src/components/config/ConfigTabs.tsx index d6fdbbf..a373a3f 100644 --- a/src/components/config/ConfigTabs.tsx +++ b/src/components/config/ConfigTabs.tsx @@ -2,6 +2,7 @@ import { useEffect, useState, useCallback, useRef } from 'react'; import { GitHubConfigForm } from './GitHubConfigForm'; import { GiteaConfigForm } from './GiteaConfigForm'; import { AutomationSettings } from './AutomationSettings'; +import { SSOSettings } from './SSOSettings'; import type { ConfigApiResponse, GiteaConfig, @@ -20,6 +21,7 @@ import { RefreshCw } from 'lucide-react'; import { toast } from 'sonner'; import { Skeleton } from '@/components/ui/skeleton'; import { invalidateConfigCache } from '@/hooks/useConfigStatus'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; type ConfigState = { githubConfig: GitHubConfig; @@ -601,65 +603,71 @@ export function ConfigTabs() {
- {/* Content section - Grid layout */} -
- {/* GitHub & Gitea connections - Side by side */} -
- - setConfig(prev => ({ - ...prev, - githubConfig: - typeof update === 'function' - ? update(prev.githubConfig) - : update, - })) - } - mirrorOptions={config.mirrorOptions} - setMirrorOptions={update => - setConfig(prev => ({ - ...prev, - mirrorOptions: - typeof update === 'function' - ? update(prev.mirrorOptions) - : update, - })) - } - advancedOptions={config.advancedOptions} - setAdvancedOptions={update => - setConfig(prev => ({ - ...prev, - advancedOptions: - typeof update === 'function' - ? update(prev.advancedOptions) - : update, - })) - } - onAutoSave={autoSaveGitHubConfig} - onMirrorOptionsAutoSave={autoSaveMirrorOptions} - onAdvancedOptionsAutoSave={autoSaveAdvancedOptions} - isAutoSaving={isAutoSavingGitHub} - /> - - setConfig(prev => ({ - ...prev, - giteaConfig: - typeof update === 'function' - ? update(prev.giteaConfig) - : update, - })) - } - onAutoSave={autoSaveGiteaConfig} - isAutoSaving={isAutoSavingGitea} - githubUsername={config.githubConfig.username} - /> -
+ {/* Content section - Tabs layout */} + + + Connections + Automation + Authentication + - {/* Automation & Maintenance - Full width */} -
+ +
+ + setConfig(prev => ({ + ...prev, + githubConfig: + typeof update === 'function' + ? update(prev.githubConfig) + : update, + })) + } + mirrorOptions={config.mirrorOptions} + setMirrorOptions={update => + setConfig(prev => ({ + ...prev, + mirrorOptions: + typeof update === 'function' + ? update(prev.mirrorOptions) + : update, + })) + } + advancedOptions={config.advancedOptions} + setAdvancedOptions={update => + setConfig(prev => ({ + ...prev, + advancedOptions: + typeof update === 'function' + ? update(prev.advancedOptions) + : update, + })) + } + onAutoSave={autoSaveGitHubConfig} + onMirrorOptionsAutoSave={autoSaveMirrorOptions} + onAdvancedOptionsAutoSave={autoSaveAdvancedOptions} + isAutoSaving={isAutoSavingGitHub} + /> + + setConfig(prev => ({ + ...prev, + giteaConfig: + typeof update === 'function' + ? update(prev.giteaConfig) + : update, + })) + } + onAutoSave={autoSaveGiteaConfig} + isAutoSaving={isAutoSavingGitea} + githubUsername={config.githubConfig.username} + /> +
+
+ + -
-
+ + + + + +
); } diff --git a/src/components/config/SSOSettings.tsx b/src/components/config/SSOSettings.tsx new file mode 100644 index 0000000..0d3b072 --- /dev/null +++ b/src/components/config/SSOSettings.tsx @@ -0,0 +1,426 @@ +import { useState, useEffect } from 'react'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Switch } from '@/components/ui/switch'; +import { Alert, AlertDescription } from '@/components/ui/alert'; +import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; +import { apiRequest, showErrorToast } from '@/lib/utils'; +import { toast } from 'sonner'; +import { Plus, Trash2, ExternalLink, Loader2, AlertCircle, Copy, Shield, Info } from 'lucide-react'; +import { Separator } from '@/components/ui/separator'; +import { Skeleton } from '../ui/skeleton'; +import { Badge } from '../ui/badge'; + +interface SSOProvider { + id: string; + issuer: string; + domain: string; + providerId: string; + organizationId?: string; + oidcConfig: { + clientId: string; + clientSecret: string; + authorizationEndpoint: string; + tokenEndpoint: string; + jwksEndpoint: string; + userInfoEndpoint: string; + mapping: { + id: string; + email: string; + emailVerified: string; + name: string; + image: string; + }; + }; + createdAt: string; + updatedAt: string; +} + +export function SSOSettings() { + const [providers, setProviders] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [showProviderDialog, setShowProviderDialog] = useState(false); + const [isDiscovering, setIsDiscovering] = useState(false); + const [headerAuthEnabled, setHeaderAuthEnabled] = useState(false); + + // Form states for new provider + const [providerForm, setProviderForm] = useState({ + issuer: '', + domain: '', + providerId: '', + clientId: '', + clientSecret: '', + authorizationEndpoint: '', + tokenEndpoint: '', + jwksEndpoint: '', + userInfoEndpoint: '', + }); + + + + useEffect(() => { + loadData(); + }, []); + + const loadData = async () => { + setIsLoading(true); + try { + const [providersRes, headerAuthStatus] = await Promise.all([ + apiRequest('/sso/providers'), + apiRequest<{ enabled: boolean }>('/auth/header-status').catch(() => ({ enabled: false })) + ]); + + setProviders(providersRes); + setHeaderAuthEnabled(headerAuthStatus.enabled); + } catch (error) { + showErrorToast(error, toast); + } finally { + setIsLoading(false); + } + }; + + const discoverOIDC = async () => { + if (!providerForm.issuer) { + toast.error('Please enter an issuer URL'); + return; + } + + setIsDiscovering(true); + try { + const discovered = await apiRequest('/sso/discover', { + method: 'POST', + data: { issuer: providerForm.issuer }, + }); + + setProviderForm(prev => ({ + ...prev, + authorizationEndpoint: discovered.authorizationEndpoint || '', + tokenEndpoint: discovered.tokenEndpoint || '', + jwksEndpoint: discovered.jwksEndpoint || '', + userInfoEndpoint: discovered.userInfoEndpoint || '', + domain: discovered.suggestedDomain || prev.domain, + })); + + toast.success('OIDC configuration discovered successfully'); + } catch (error) { + showErrorToast(error, toast); + } finally { + setIsDiscovering(false); + } + }; + + const createProvider = async () => { + try { + const newProvider = await apiRequest('/sso/providers', { + method: 'POST', + data: { + ...providerForm, + mapping: { + id: 'sub', + email: 'email', + emailVerified: 'email_verified', + name: 'name', + image: 'picture', + }, + }, + }); + + setProviders([...providers, newProvider]); + setShowProviderDialog(false); + setProviderForm({ + issuer: '', + domain: '', + providerId: '', + clientId: '', + clientSecret: '', + authorizationEndpoint: '', + tokenEndpoint: '', + jwksEndpoint: '', + userInfoEndpoint: '', + }); + toast.success('SSO provider created successfully'); + } catch (error) { + showErrorToast(error, toast); + } + }; + + const deleteProvider = async (id: string) => { + try { + await apiRequest(`/sso/providers?id=${id}`, { method: 'DELETE' }); + setProviders(providers.filter(p => p.id !== id)); + toast.success('Provider deleted successfully'); + } catch (error) { + showErrorToast(error, toast); + } + }; + + + const copyToClipboard = (text: string) => { + navigator.clipboard.writeText(text); + toast.success('Copied to clipboard'); + }; + + if (isLoading) { + return ( +
+ + +
+ ); + } + + return ( +
+ {/* Header with status indicators */} +
+
+

Authentication & SSO

+

+ Configure how users authenticate with your application +

+
+
+
0 ? 'bg-green-500' : 'bg-muted'}`} /> + + {providers.length} Provider{providers.length !== 1 ? 's' : ''} configured + +
+
+ + {/* Authentication Methods Overview */} + + + Active Authentication Methods + + +
+ {/* Email & Password - Always enabled */} +
+
+
+ Email & Password + Default +
+ Always enabled +
+ + {/* Header Authentication Status */} + {headerAuthEnabled && ( +
+
+
+ Header Authentication + Auto-login +
+ Via reverse proxy +
+ )} + + {/* SSO Providers Status */} +
+
+
0 ? 'bg-green-500' : 'bg-muted'}`} /> + SSO/OIDC Providers +
+ + {providers.length > 0 ? `${providers.length} provider${providers.length !== 1 ? 's' : ''} configured` : 'Not configured'} + +
+
+ + {/* Header Auth Info */} + {headerAuthEnabled && ( + + + + Header authentication is enabled. Users authenticated by your reverse proxy will be automatically logged in. + + + )} + + + + {/* SSO Providers */} + + +
+
+ External Identity Providers + + Connect external OIDC/OAuth providers (Google, Azure AD, etc.) to allow users to sign in with their existing accounts + +
+ + + + + + + Add SSO Provider + + Configure an external OIDC provider for user authentication + + +
+
+ +
+ setProviderForm(prev => ({ ...prev, issuer: e.target.value }))} + placeholder="https://accounts.google.com" + /> + +
+
+ +
+
+ + setProviderForm(prev => ({ ...prev, domain: e.target.value }))} + placeholder="example.com" + /> +
+
+ + setProviderForm(prev => ({ ...prev, providerId: e.target.value }))} + placeholder="google-sso" + /> +
+
+ +
+
+ + setProviderForm(prev => ({ ...prev, clientId: e.target.value }))} + /> +
+
+ + setProviderForm(prev => ({ ...prev, clientSecret: e.target.value }))} + /> +
+
+ +
+ + setProviderForm(prev => ({ ...prev, authorizationEndpoint: e.target.value }))} + placeholder="https://accounts.google.com/o/oauth2/auth" + /> +
+ +
+ + setProviderForm(prev => ({ ...prev, tokenEndpoint: e.target.value }))} + placeholder="https://oauth2.googleapis.com/token" + /> +
+ + + + + Redirect URL: {window.location.origin}/api/auth/sso/callback/{providerForm.providerId || '{provider-id}'} + + +
+ + + + +
+
+
+
+ + {providers.length === 0 ? ( +
+
+ + + +
+

No SSO providers configured

+

+ Enable Single Sign-On by adding an external identity provider like Google, Azure AD, or any OIDC-compliant service. +

+
+ +
+
+ ) : ( +
+ {providers.map(provider => ( + + +
+
+

{provider.providerId}

+

{provider.domain}

+
+ +
+
+ +
+
+

Issuer

+

{provider.issuer}

+
+
+

Client ID

+

{provider.oidcConfig.clientId}

+
+
+
+
+ ))} +
+ )} +
+
+
+ ); +} \ No newline at end of file diff --git a/src/components/layout/Header.tsx b/src/components/layout/Header.tsx index 96e2ef2..53d1162 100644 --- a/src/components/layout/Header.tsx +++ b/src/components/layout/Header.tsx @@ -129,9 +129,9 @@ export function Header({ currentPage, onNavigate, onMenuClick }: HeaderProps) { diff --git a/src/components/layout/SponsorCard.tsx b/src/components/layout/SponsorCard.tsx new file mode 100644 index 0000000..f738c49 --- /dev/null +++ b/src/components/layout/SponsorCard.tsx @@ -0,0 +1,72 @@ +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Heart, Coffee, Sparkles } from "lucide-react"; +import { isSelfHostedMode } from "@/lib/deployment-mode"; + +export function SponsorCard() { + // Only show in self-hosted mode + if (!isSelfHostedMode()) { + return null; + } + + return ( +
+ + + + + Support Development + + + Help us improve Gitea Mirror + + + +

+ Gitea Mirror is open source and free. Your sponsorship helps us maintain and improve it. +

+ + + +
+

+ + Pro features available in hosted version +

+
+
+
+
+ ); +} \ No newline at end of file diff --git a/src/components/oauth/ConsentPage.tsx b/src/components/oauth/ConsentPage.tsx new file mode 100644 index 0000000..f41a7e8 --- /dev/null +++ b/src/components/oauth/ConsentPage.tsx @@ -0,0 +1,307 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { Alert, AlertDescription } from '@/components/ui/alert'; +import { Checkbox } from '@/components/ui/checkbox'; +import { Label } from '@/components/ui/label'; +import { Separator } from '@/components/ui/separator'; +import { authClient } from '@/lib/auth-client'; +import { apiRequest, showErrorToast } from '@/lib/utils'; +import { toast, Toaster } from 'sonner'; +import { Shield, User, Mail, ChevronRight, AlertTriangle, Loader2 } from 'lucide-react'; +import { isValidRedirectUri, parseRedirectUris } from '@/lib/utils/oauth-validation'; + +interface OAuthApplication { + id: string; + clientId: string; + name: string; + redirectURLs: string; + type: string; +} + +interface ConsentRequest { + clientId: string; + scope: string; + state?: string; + redirectUri?: string; +} + +export default function ConsentPage() { + const [isLoading, setIsLoading] = useState(true); + const [isSubmitting, setIsSubmitting] = useState(false); + const [application, setApplication] = useState(null); + const [scopes, setScopes] = useState([]); + const [selectedScopes, setSelectedScopes] = useState>(new Set()); + const [error, setError] = useState(null); + + useEffect(() => { + loadConsentDetails(); + }, []); + + const loadConsentDetails = async () => { + try { + const params = new URLSearchParams(window.location.search); + const clientId = params.get('client_id'); + const scope = params.get('scope'); + const redirectUri = params.get('redirect_uri'); + + if (!clientId) { + setError('Invalid authorization request: missing client ID'); + return; + } + + // Fetch application details + const apps = await apiRequest('/sso/applications'); + const app = apps.find(a => a.clientId === clientId); + + if (!app) { + setError('Invalid authorization request: unknown application'); + return; + } + + // Validate redirect URI if provided + if (redirectUri) { + const authorizedUris = parseRedirectUris(app.redirectURLs); + + if (!isValidRedirectUri(redirectUri, authorizedUris)) { + setError('Invalid authorization request: unauthorized redirect URI'); + return; + } + } + + setApplication(app); + + // Parse requested scopes + const requestedScopes = scope ? scope.split(' ').filter(s => s) : ['openid']; + setScopes(requestedScopes); + + // By default, select all requested scopes + setSelectedScopes(new Set(requestedScopes)); + } catch (error) { + console.error('Failed to load consent details:', error); + setError('Failed to load authorization details'); + } finally { + setIsLoading(false); + } + }; + + const handleConsent = async (accept: boolean) => { + setIsSubmitting(true); + try { + const result = await authClient.oauth2.consent({ + accept, + }); + + if (result.error) { + throw new Error(result.error.message || 'Consent failed'); + } + + // The consent method should handle the redirect + if (!accept) { + // If denied, redirect back to the application with error + const params = new URLSearchParams(window.location.search); + const redirectUri = params.get('redirect_uri'); + + if (redirectUri && application) { + // Validate redirect URI against authorized URIs + const authorizedUris = parseRedirectUris(application.redirectURLs); + + if (isValidRedirectUri(redirectUri, authorizedUris)) { + try { + // Parse and reconstruct the URL to ensure it's safe + const url = new URL(redirectUri); + url.searchParams.set('error', 'access_denied'); + + // Safe to redirect - URI has been validated and sanitized + window.location.href = url.toString(); + } catch (e) { + console.error('Failed to parse redirect URI:', e); + setError('Invalid redirect URI'); + } + } else { + console.error('Unauthorized redirect URI:', redirectUri); + setError('Invalid redirect URI'); + } + } + } + } catch (error) { + showErrorToast(error, toast); + } finally { + setIsSubmitting(false); + } + }; + + const toggleScope = (scope: string) => { + // openid scope is always required + if (scope === 'openid') return; + + const newSelected = new Set(selectedScopes); + if (newSelected.has(scope)) { + newSelected.delete(scope); + } else { + newSelected.add(scope); + } + setSelectedScopes(newSelected); + }; + + const getScopeDescription = (scope: string): { name: string; description: string; icon: any } => { + const scopeDescriptions: Record = { + openid: { + name: 'Basic Information', + description: 'Your user ID (required)', + icon: User, + }, + profile: { + name: 'Profile Information', + description: 'Your name, username, and profile picture', + icon: User, + }, + email: { + name: 'Email Address', + description: 'Your email address and verification status', + icon: Mail, + }, + }; + + return scopeDescriptions[scope] || { + name: scope, + description: `Access to ${scope} information`, + icon: Shield, + }; + }; + + if (isLoading) { + return ( +
+ +
+ ); + } + + if (error) { + return ( +
+ + +
+ +
+ Authorization Error +
+ + + {error} + + + + + +
+
+ ); + } + + return ( + <> +
+ + +
+ +
+ Authorize {application?.name} + + This application is requesting access to your account + +
+ + +
+

Requested permissions:

+
+ {scopes.map(scope => { + const scopeInfo = getScopeDescription(scope); + const Icon = scopeInfo.icon; + const isRequired = scope === 'openid'; + + return ( +
+ toggleScope(scope)} + disabled={isRequired || isSubmitting} + /> +
+ +

+ {scopeInfo.description} +

+
+
+ ); + })} +
+
+ + + +
+

+ + You'll be redirected to {application?.type === 'web' ? 'the website' : 'the application'} +

+

+ + You can revoke access at any time in your account settings +

+
+
+ + + + + +
+
+ + + ); +} \ No newline at end of file diff --git a/src/components/sponsors/GitHubSponsors.tsx b/src/components/sponsors/GitHubSponsors.tsx new file mode 100644 index 0000000..973fede --- /dev/null +++ b/src/components/sponsors/GitHubSponsors.tsx @@ -0,0 +1,105 @@ +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Heart, Coffee, Zap } from "lucide-react"; +import { isSelfHostedMode } from "@/lib/deployment-mode"; + +export function GitHubSponsors() { + // Only show in self-hosted mode + if (!isSelfHostedMode()) { + return null; + } + + return ( + + + + + Support Gitea Mirror + + + +

+ Gitea Mirror is open source and free to use. If you find it helpful, + consider supporting the project! +

+ + + +
+

+ + Your support helps maintain and improve the project +

+
+
+
+ ); +} + +// Smaller inline sponsor button for headers/navbars +export function SponsorButton() { + if (!isSelfHostedMode()) { + return null; + } + + return ( + + ); +} \ No newline at end of file diff --git a/src/hooks/useAuth-legacy.ts b/src/hooks/useAuth-legacy.ts new file mode 100644 index 0000000..01b9432 --- /dev/null +++ b/src/hooks/useAuth-legacy.ts @@ -0,0 +1,147 @@ +import * as React from "react"; +import { + useState, + useEffect, + createContext, + useContext, + type Context, +} from "react"; +import { authApi } from "@/lib/api"; +import type { ExtendedUser } from "@/types/user"; + +interface AuthContextType { + user: ExtendedUser | null; + isLoading: boolean; + error: string | null; + login: (username: string, password: string) => Promise; + register: ( + username: string, + email: string, + password: string + ) => Promise; + logout: () => Promise; + refreshUser: () => Promise; // Added refreshUser function +} + +const AuthContext: Context = createContext< + AuthContextType | undefined +>(undefined); + +export function AuthProvider({ children }: { children: React.ReactNode }) { + const [user, setUser] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + + // Function to refetch the user data + const refreshUser = async () => { + // not using loading state to keep the ui seamless and refresh the data in bg + // setIsLoading(true); + try { + const user = await authApi.getCurrentUser(); + setUser(user); + } catch (err: any) { + setUser(null); + console.error("Failed to refresh user data", err); + } finally { + // setIsLoading(false); + } + }; + + // Automatically check the user status when the app loads + useEffect(() => { + const checkAuth = async () => { + try { + const user = await authApi.getCurrentUser(); + + console.log("User data fetched:", user); + + setUser(user); + } catch (err: any) { + setUser(null); + + // Redirect user based on error + if (err?.message === "No users found") { + window.location.href = "/signup"; + } else { + window.location.href = "/login"; + } + console.error("Auth check failed", err); + } finally { + setIsLoading(false); + } + }; + + checkAuth(); + }, []); + + const login = async (username: string, password: string) => { + setIsLoading(true); + setError(null); + try { + const user = await authApi.login(username, password); + setUser(user); + } catch (err) { + setError(err instanceof Error ? err.message : "Login failed"); + throw err; + } finally { + setIsLoading(false); + } + }; + + const register = async ( + username: string, + email: string, + password: string + ) => { + setIsLoading(true); + setError(null); + try { + const user = await authApi.register(username, email, password); + setUser(user); + } catch (err) { + setError(err instanceof Error ? err.message : "Registration failed"); + throw err; + } finally { + setIsLoading(false); + } + }; + + const logout = async () => { + setIsLoading(true); + try { + await authApi.logout(); + setUser(null); + window.location.href = "/login"; + } catch (err) { + console.error("Logout error:", err); + } finally { + setIsLoading(false); + } + }; + + // Create the context value with the added refreshUser function + const contextValue = { + user, + isLoading, + error, + login, + register, + logout, + refreshUser, + }; + + // Return the provider with the context value + return React.createElement( + AuthContext.Provider, + { value: contextValue }, + children + ); +} + +export function useAuth() { + const context = useContext(AuthContext); + if (context === undefined) { + throw new Error("useAuth must be used within an AuthProvider"); + } + return context; +} diff --git a/src/hooks/useAuth.ts b/src/hooks/useAuth.ts index 01b9432..7bab564 100644 --- a/src/hooks/useAuth.ts +++ b/src/hooks/useAuth.ts @@ -6,21 +6,22 @@ import { useContext, type Context, } from "react"; -import { authApi } from "@/lib/api"; -import type { ExtendedUser } from "@/types/user"; +import { authClient, useSession as useBetterAuthSession } from "@/lib/auth-client"; +import type { Session, AuthUser } from "@/lib/auth-client"; interface AuthContextType { - user: ExtendedUser | null; + user: AuthUser | null; + session: Session | null; isLoading: boolean; error: string | null; - login: (username: string, password: string) => Promise; + login: (email: string, password: string, username?: string) => Promise; register: ( username: string, email: string, password: string ) => Promise; logout: () => Promise; - refreshUser: () => Promise; // Added refreshUser function + refreshUser: () => Promise; } const AuthContext: Context = createContext< @@ -28,60 +29,32 @@ const AuthContext: Context = createContext< >(undefined); export function AuthProvider({ children }: { children: React.ReactNode }) { - const [user, setUser] = useState(null); - const [isLoading, setIsLoading] = useState(true); + const betterAuthSession = useBetterAuthSession(); const [error, setError] = useState(null); + const [isLoading, setIsLoading] = useState(false); - // Function to refetch the user data - const refreshUser = async () => { - // not using loading state to keep the ui seamless and refresh the data in bg - // setIsLoading(true); - try { - const user = await authApi.getCurrentUser(); - setUser(user); - } catch (err: any) { - setUser(null); - console.error("Failed to refresh user data", err); - } finally { - // setIsLoading(false); - } - }; + // Derive user and session from Better Auth hook + const user = betterAuthSession.data?.user || null; + const session = betterAuthSession.data || null; - // Automatically check the user status when the app loads - useEffect(() => { - const checkAuth = async () => { - try { - const user = await authApi.getCurrentUser(); + // Don't do any redirects here - let the pages handle their own redirect logic - console.log("User data fetched:", user); - - setUser(user); - } catch (err: any) { - setUser(null); - - // Redirect user based on error - if (err?.message === "No users found") { - window.location.href = "/signup"; - } else { - window.location.href = "/login"; - } - console.error("Auth check failed", err); - } finally { - setIsLoading(false); - } - }; - - checkAuth(); - }, []); - - const login = async (username: string, password: string) => { + const login = async (email: string, password: string) => { setIsLoading(true); setError(null); try { - const user = await authApi.login(username, password); - setUser(user); + const result = await authClient.signIn.email({ + email, + password, + callbackURL: "/", + }); + + if (result.error) { + throw new Error(result.error.message || "Login failed"); + } } catch (err) { - setError(err instanceof Error ? err.message : "Login failed"); + const message = err instanceof Error ? err.message : "Login failed"; + setError(message); throw err; } finally { setIsLoading(false); @@ -96,10 +69,19 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { setIsLoading(true); setError(null); try { - const user = await authApi.register(username, email, password); - setUser(user); + const result = await authClient.signUp.email({ + email, + password, + name: username, // Better Auth uses 'name' field for display name + callbackURL: "/", + }); + + if (result.error) { + throw new Error(result.error.message || "Registration failed"); + } } catch (err) { - setError(err instanceof Error ? err.message : "Registration failed"); + const message = err instanceof Error ? err.message : "Registration failed"; + setError(message); throw err; } finally { setIsLoading(false); @@ -109,9 +91,13 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { const logout = async () => { setIsLoading(true); try { - await authApi.logout(); - setUser(null); - window.location.href = "/login"; + await authClient.signOut({ + fetchOptions: { + onSuccess: () => { + window.location.href = "/login"; + }, + }, + }); } catch (err) { console.error("Logout error:", err); } finally { @@ -119,10 +105,17 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { } }; - // Create the context value with the added refreshUser function + const refreshUser = async () => { + // Better Auth automatically handles session refresh + // We can force a refetch if needed + await betterAuthSession.refetch(); + }; + + // Create the context value const contextValue = { - user, - isLoading, + user: user as AuthUser | null, + session, + isLoading: isLoading || betterAuthSession.isPending, error, login, register, @@ -145,3 +138,6 @@ export function useAuth() { } return context; } + +// Export the Better Auth session hook for direct use when needed +export { useBetterAuthSession }; \ No newline at end of file diff --git a/src/hooks/useAuthMethods.ts b/src/hooks/useAuthMethods.ts new file mode 100644 index 0000000..9f77a69 --- /dev/null +++ b/src/hooks/useAuthMethods.ts @@ -0,0 +1,65 @@ +import { useState, useEffect } from 'react'; +import { apiRequest } from '@/lib/utils'; + +interface AuthMethods { + emailPassword: boolean; + sso: { + enabled: boolean; + providers: Array<{ + id: string; + providerId: string; + domain: string; + }>; + }; + oidc: { + enabled: boolean; + }; +} + +export function useAuthMethods() { + const [authMethods, setAuthMethods] = useState({ + emailPassword: true, + sso: { + enabled: false, + providers: [], + }, + oidc: { + enabled: false, + }, + }); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + loadAuthMethods(); + }, []); + + const loadAuthMethods = async () => { + try { + // Check SSO providers + const providers = await apiRequest('/sso/providers').catch(() => []); + const applications = await apiRequest('/sso/applications').catch(() => []); + + setAuthMethods({ + emailPassword: true, // Always enabled + sso: { + enabled: providers.length > 0, + providers: providers.map(p => ({ + id: p.id, + providerId: p.providerId, + domain: p.domain, + })), + }, + oidc: { + enabled: applications.length > 0, + }, + }); + } catch (error) { + // If we can't load auth methods, default to email/password only + console.error('Failed to load auth methods:', error); + } finally { + setIsLoading(false); + } + }; + + return { authMethods, isLoading }; +} \ No newline at end of file diff --git a/src/lib/auth-client.ts b/src/lib/auth-client.ts new file mode 100644 index 0000000..07424cc --- /dev/null +++ b/src/lib/auth-client.ts @@ -0,0 +1,28 @@ +import { createAuthClient } from "better-auth/react"; +import { oidcClient } from "better-auth/client/plugins"; +import { ssoClient } from "better-auth/client/plugins"; + +export const authClient = createAuthClient({ + // The base URL is optional when running on the same domain + // Better Auth will use the current domain by default + plugins: [ + oidcClient(), + ssoClient(), + ], +}); + +// Export commonly used methods for convenience +export const { + signIn, + signUp, + signOut, + useSession, + sendVerificationEmail, + resetPassword, + requestPasswordReset, + getSession +} = authClient; + +// Export types +export type Session = Awaited>["data"]; +export type AuthUser = Session extends { user: infer U } ? U : never; \ No newline at end of file diff --git a/src/lib/auth-config.ts b/src/lib/auth-config.ts new file mode 100644 index 0000000..76b13af --- /dev/null +++ b/src/lib/auth-config.ts @@ -0,0 +1,70 @@ +import { betterAuth } from "better-auth"; +import { drizzleAdapter } from "better-auth/adapters/drizzle"; +import { oidcProvider } from "better-auth/plugins"; +import { sso } from "better-auth/plugins/sso"; +import type { BunSQLiteDatabase } from "drizzle-orm/bun-sqlite"; + +// This function will be called with the actual database instance +export function createAuth(db: BunSQLiteDatabase) { + return betterAuth({ + // Database configuration + database: drizzleAdapter(db, { + provider: "sqlite", + usePlural: true, // Our tables use plural names (users, not user) + }), + + // Base URL configuration + baseURL: process.env.BETTER_AUTH_URL || "http://localhost:3000", + + // Authentication methods + emailAndPassword: { + enabled: true, + requireEmailVerification: false, // We'll enable this later + sendResetPassword: async ({ user, url, token }, request) => { + // TODO: Implement email sending for password reset + console.log("Password reset requested for:", user.email); + console.log("Reset URL:", url); + }, + }, + + // Session configuration + session: { + cookieName: "better-auth-session", + updateSessionCookieAge: true, + expiresIn: 60 * 60 * 24 * 30, // 30 days + }, + + // User configuration + user: { + additionalFields: { + // We can add custom fields here if needed + }, + }, + + // Plugins for OIDC/SSO support + plugins: [ + // SSO plugin for OIDC client support + sso({ + provisionUser: async (data) => { + // Custom user provisioning logic for SSO users + console.log("Provisioning SSO user:", data); + return data; + }, + }), + + // OIDC Provider plugin (for future use when we want to be an OIDC provider) + oidcProvider({ + loginPage: "/signin", + consentPage: "/oauth/consent", + metadata: { + issuer: process.env.BETTER_AUTH_URL || "http://localhost:3000", + }, + }), + ], + + // Trusted origins for CORS + trustedOrigins: [ + process.env.BETTER_AUTH_URL || "http://localhost:3000", + ], + }); +} \ No newline at end of file diff --git a/src/lib/auth-header.ts b/src/lib/auth-header.ts new file mode 100644 index 0000000..cf51926 --- /dev/null +++ b/src/lib/auth-header.ts @@ -0,0 +1,135 @@ +import { db, users } from "./db"; +import { eq } from "drizzle-orm"; +import { nanoid } from "nanoid"; + +export interface HeaderAuthConfig { + enabled: boolean; + userHeader: string; + emailHeader?: string; + nameHeader?: string; + autoProvision: boolean; + allowedDomains?: string[]; +} + +// Default configuration - DISABLED by default +export const defaultHeaderAuthConfig: HeaderAuthConfig = { + enabled: false, + userHeader: "X-Authentik-Username", // Common header name + emailHeader: "X-Authentik-Email", + nameHeader: "X-Authentik-Name", + autoProvision: false, + allowedDomains: [], +}; + +// Get header auth config from environment or database +export function getHeaderAuthConfig(): HeaderAuthConfig { + // Check environment variables for header auth config + const envConfig: Partial = { + enabled: process.env.HEADER_AUTH_ENABLED === "true", + userHeader: process.env.HEADER_AUTH_USER_HEADER || defaultHeaderAuthConfig.userHeader, + emailHeader: process.env.HEADER_AUTH_EMAIL_HEADER || defaultHeaderAuthConfig.emailHeader, + nameHeader: process.env.HEADER_AUTH_NAME_HEADER || defaultHeaderAuthConfig.nameHeader, + autoProvision: process.env.HEADER_AUTH_AUTO_PROVISION === "true", + allowedDomains: process.env.HEADER_AUTH_ALLOWED_DOMAINS?.split(",").map(d => d.trim()), + }; + + return { + ...defaultHeaderAuthConfig, + ...envConfig, + }; +} + +// Check if header authentication is enabled +export function isHeaderAuthEnabled(): boolean { + const config = getHeaderAuthConfig(); + return config.enabled === true; +} + +// Extract user info from headers +export function extractUserFromHeaders(headers: Headers): { + username?: string; + email?: string; + name?: string; +} | null { + const config = getHeaderAuthConfig(); + + if (!config.enabled) { + return null; + } + + const username = headers.get(config.userHeader); + const email = config.emailHeader ? headers.get(config.emailHeader) : undefined; + const name = config.nameHeader ? headers.get(config.nameHeader) : undefined; + + if (!username) { + return null; + } + + // If allowed domains are configured, check email domain + if (config.allowedDomains && config.allowedDomains.length > 0 && email) { + const domain = email.split("@")[1]; + if (!config.allowedDomains.includes(domain)) { + console.warn(`Header auth rejected: email domain ${domain} not in allowed list`); + return null; + } + } + + return { username, email, name }; +} + +// Find or create user from header auth +export async function authenticateWithHeaders(headers: Headers) { + const userInfo = extractUserFromHeaders(headers); + + if (!userInfo || !userInfo.username) { + return null; + } + + const config = getHeaderAuthConfig(); + + // Try to find existing user by username or email + let existingUser = await db + .select() + .from(users) + .where(eq(users.username, userInfo.username)) + .limit(1); + + if (existingUser.length === 0 && userInfo.email) { + existingUser = await db + .select() + .from(users) + .where(eq(users.email, userInfo.email)) + .limit(1); + } + + if (existingUser.length > 0) { + return existingUser[0]; + } + + // If auto-provisioning is disabled, don't create new users + if (!config.autoProvision) { + console.warn(`Header auth: User ${userInfo.username} not found and auto-provisioning is disabled`); + return null; + } + + // Create new user if auto-provisioning is enabled + try { + const newUser = { + id: nanoid(), + username: userInfo.username, + email: userInfo.email || `${userInfo.username}@header-auth.local`, + emailVerified: true, // Trust the auth provider + name: userInfo.name || userInfo.username, + createdAt: new Date(), + updatedAt: new Date(), + }; + + await db.insert(users).values(newUser); + console.log(`Header auth: Auto-provisioned new user ${userInfo.username}`); + + return newUser; + } catch (error) { + console.error("Failed to auto-provision user from header auth:", error); + return null; + } +} \ No newline at end of file diff --git a/src/lib/auth-oidc-config.example.ts b/src/lib/auth-oidc-config.example.ts new file mode 100644 index 0000000..ab8cb97 --- /dev/null +++ b/src/lib/auth-oidc-config.example.ts @@ -0,0 +1,179 @@ +/** + * Example OIDC/SSO Configuration for Better Auth + * + * This file demonstrates how to enable OIDC and SSO features in Gitea Mirror. + * To use: Copy this file to auth-oidc-config.ts and update the auth.ts import. + */ + +import { betterAuth } from "better-auth"; +import { drizzleAdapter } from "better-auth/adapters/drizzle"; +import { sso } from "better-auth/plugins/sso"; +import { oidcProvider } from "better-auth/plugins/oidc"; +import type { BunSQLiteDatabase } from "drizzle-orm/bun-sqlite"; + +export function createAuthWithOIDC(db: BunSQLiteDatabase) { + return betterAuth({ + // Database configuration + database: drizzleAdapter(db, { + provider: "sqlite", + usePlural: true, + }), + + // Base configuration + baseURL: process.env.BETTER_AUTH_URL || "http://localhost:3000", + basePath: "/api/auth", + + // Email/Password authentication + emailAndPassword: { + enabled: true, + requireEmailVerification: false, + }, + + // Session configuration + session: { + cookieName: "better-auth-session", + updateSessionCookieAge: true, + expiresIn: 60 * 60 * 24 * 30, // 30 days + }, + + // User configuration with additional fields + user: { + additionalFields: { + username: { + type: "string", + required: true, + defaultValue: "user", + input: true, + } + }, + }, + + // OAuth2 providers (examples) + socialProviders: { + github: { + enabled: !!process.env.GITHUB_OAUTH_CLIENT_ID, + clientId: process.env.GITHUB_OAUTH_CLIENT_ID!, + clientSecret: process.env.GITHUB_OAUTH_CLIENT_SECRET!, + }, + google: { + enabled: !!process.env.GOOGLE_OAUTH_CLIENT_ID, + clientId: process.env.GOOGLE_OAUTH_CLIENT_ID!, + clientSecret: process.env.GOOGLE_OAUTH_CLIENT_SECRET!, + }, + }, + + // Plugins + plugins: [ + // SSO Plugin - For OIDC/SAML client functionality + sso({ + // Auto-provision users from SSO providers + provisionUser: async (data) => { + console.log("Provisioning SSO user:", data.email); + + // Custom logic to set username from email + const username = data.email.split('@')[0]; + + return { + ...data, + username, + }; + }, + + // Organization provisioning for enterprise SSO + organizationProvisioning: { + disabled: false, + defaultRole: "member", + getRole: async (user) => { + // Custom logic to determine user role + // For admin emails, grant admin role + if (user.email?.endsWith('@admin.example.com')) { + return 'admin'; + } + return 'member'; + }, + }, + }), + + // OIDC Provider Plugin - Makes Gitea Mirror an OIDC provider + oidcProvider({ + // Login page for OIDC authentication flow + loginPage: "/login", + + // Consent page for OAuth2 authorization + consentPage: "/oauth/consent", + + // Allow dynamic client registration + allowDynamicClientRegistration: false, + + // OIDC metadata configuration + metadata: { + issuer: process.env.BETTER_AUTH_URL || "http://localhost:3000", + authorization_endpoint: "/api/auth/oauth2/authorize", + token_endpoint: "/api/auth/oauth2/token", + userinfo_endpoint: "/api/auth/oauth2/userinfo", + jwks_uri: "/api/auth/jwks", + }, + + // Additional user info claims + getAdditionalUserInfoClaim: (user, scopes) => { + const claims: Record = {}; + + // Add custom claims based on scopes + if (scopes.includes('profile')) { + claims.username = user.username; + claims.preferred_username = user.username; + } + + if (scopes.includes('gitea')) { + // Add Gitea-specific claims + claims.gitea_admin = false; // Customize based on your logic + claims.gitea_repos = []; // Could fetch user's repositories + } + + return claims; + }, + }), + ], + + // Trusted origins for CORS + trustedOrigins: [ + process.env.BETTER_AUTH_URL || "http://localhost:3000", + // Add your OIDC client domains here + ], + }); +} + +// Environment variables needed: +/* +# OAuth2 Providers (optional) +GITHUB_OAUTH_CLIENT_ID=your-github-client-id +GITHUB_OAUTH_CLIENT_SECRET=your-github-client-secret +GOOGLE_OAUTH_CLIENT_ID=your-google-client-id +GOOGLE_OAUTH_CLIENT_SECRET=your-google-client-secret + +# SSO Configuration (when registering providers) +SSO_PROVIDER_ISSUER=https://idp.example.com +SSO_PROVIDER_CLIENT_ID=your-client-id +SSO_PROVIDER_CLIENT_SECRET=your-client-secret +*/ + +// Example: Registering an SSO provider programmatically +/* +import { authClient } from "./auth-client"; + +// Register corporate SSO +await authClient.sso.register({ + issuer: "https://login.microsoftonline.com/tenant-id/v2.0", + domain: "company.com", + clientId: process.env.AZURE_CLIENT_ID!, + clientSecret: process.env.AZURE_CLIENT_SECRET!, + providerId: "azure-ad", + mapping: { + id: "sub", + email: "email", + emailVerified: "email_verified", + name: "name", + image: "picture", + }, +}); +*/ \ No newline at end of file diff --git a/src/lib/auth.ts b/src/lib/auth.ts new file mode 100644 index 0000000..a1d38ac --- /dev/null +++ b/src/lib/auth.ts @@ -0,0 +1,99 @@ +import { betterAuth } from "better-auth"; +import { drizzleAdapter } from "better-auth/adapters/drizzle"; +import { oidcProvider } from "better-auth/plugins"; +import { sso } from "better-auth/plugins/sso"; +import { db, users } from "./db"; +import * as schema from "./db/schema"; +import { eq } from "drizzle-orm"; + +export const auth = betterAuth({ + // Database configuration + database: drizzleAdapter(db, { + provider: "sqlite", + usePlural: true, // Our tables use plural names (users, not user) + schema, // Pass the schema explicitly + }), + + // Secret for signing tokens + secret: process.env.BETTER_AUTH_SECRET, + + // Base URL configuration + baseURL: process.env.BETTER_AUTH_URL || "http://localhost:4321", + basePath: "/api/auth", // Specify the base path for auth endpoints + + // Authentication methods + emailAndPassword: { + enabled: true, + requireEmailVerification: false, // We'll enable this later + sendResetPassword: async ({ user, url, token }, request) => { + // TODO: Implement email sending for password reset + console.log("Password reset requested for:", user.email); + console.log("Reset URL:", url); + }, + }, + + + // Session configuration + session: { + cookieName: "better-auth-session", + updateSessionCookieAge: true, + expiresIn: 60 * 60 * 24 * 30, // 30 days + }, + + // User configuration + user: { + additionalFields: { + // Keep the username field from our existing schema + username: { + type: "string", + required: false, + input: false, // Don't show in signup form - we'll derive from email + } + }, + }, + + // Plugins configuration + plugins: [ + // OIDC Provider plugin - allows this app to act as an OIDC provider + oidcProvider({ + loginPage: "/login", + consentPage: "/oauth/consent", + // Allow dynamic client registration for flexibility + allowDynamicClientRegistration: true, + // Customize user info claims based on scopes + getAdditionalUserInfoClaim: (user, scopes) => { + const claims: Record = {}; + if (scopes.includes("profile")) { + claims.username = user.username; + } + return claims; + }, + }), + + // SSO plugin - allows users to authenticate with external OIDC providers + sso({ + // Provision new users when they sign in with SSO + provisionUser: async (user) => { + // Derive username from email if not provided + const username = user.name || user.email?.split('@')[0] || 'user'; + return { + ...user, + username, + }; + }, + // Organization provisioning settings + organizationProvisioning: { + disabled: false, + defaultRole: "member", + }, + }), + ], + + // Trusted origins for CORS + trustedOrigins: [ + process.env.BETTER_AUTH_URL || "http://localhost:4321", + ], +}); + +// Export type for use in other parts of the app +export type Auth = typeof auth; \ No newline at end of file diff --git a/src/lib/config.ts b/src/lib/config.ts index 8968830..3929b64 100644 --- a/src/lib/config.ts +++ b/src/lib/config.ts @@ -18,9 +18,9 @@ export const ENV = { return "sqlite://data/gitea-mirror.db"; }, - // JWT secret for authentication - JWT_SECRET: - process.env.JWT_SECRET || "your-secret-key-change-this-in-production", + // Better Auth secret for authentication + BETTER_AUTH_SECRET: + process.env.BETTER_AUTH_SECRET || "your-secret-key-change-this-in-production", // Server host and port HOST: process.env.HOST || "localhost", diff --git a/src/lib/db/adapter.ts b/src/lib/db/adapter.ts new file mode 100644 index 0000000..20b2538 --- /dev/null +++ b/src/lib/db/adapter.ts @@ -0,0 +1,102 @@ +/** + * Database adapter for SQLite + * For the self-hosted version of Gitea Mirror + */ + +import { drizzle as drizzleSqlite } from 'drizzle-orm/bun-sqlite'; +import { Database } from 'bun:sqlite'; +import * as schema from './schema'; + +export type DatabaseClient = ReturnType; + +/** + * Create SQLite database connection + */ +export function createDatabase() { + const dbPath = process.env.DATABASE_PATH || './data/gitea-mirror.db'; + + // Ensure directory exists + const fs = require('fs'); + const path = require('path'); + const dir = path.dirname(dbPath); + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + + // Create SQLite connection + const sqlite = new Database(dbPath); + + // Enable foreign keys and WAL mode for better performance + sqlite.exec('PRAGMA foreign_keys = ON'); + sqlite.exec('PRAGMA journal_mode = WAL'); + sqlite.exec('PRAGMA synchronous = NORMAL'); + sqlite.exec('PRAGMA cache_size = -2000'); // 2MB cache + sqlite.exec('PRAGMA temp_store = MEMORY'); + + // Create Drizzle instance with SQLite + const db = drizzleSqlite(sqlite, { + schema, + logger: process.env.NODE_ENV === 'development', + }); + + return { + db, + client: sqlite, + type: 'sqlite' as const, + + // Helper methods + async close() { + sqlite.close(); + }, + + async healthCheck() { + try { + sqlite.query('SELECT 1').get(); + return true; + } catch { + return false; + } + }, + + async transaction(fn: (tx: any) => Promise) { + return db.transaction(fn); + }, + }; +} + +// Create singleton instance +let dbInstance: DatabaseClient | null = null; + +/** + * Get database instance (singleton) + */ +export function getDatabase(): DatabaseClient { + if (!dbInstance) { + dbInstance = createDatabase(); + } + return dbInstance; +} + +/** + * Close database connection + */ +export async function closeDatabase() { + if (dbInstance) { + await dbInstance.close(); + dbInstance = null; + } +} + +// Export convenience references +export const { db, client, type: dbType } = getDatabase(); + +// Re-export schema for convenience +export * from './schema'; + +/** + * Database migration utilities + */ +export async function runMigrations() { + const { migrate } = await import('drizzle-orm/bun-sqlite/migrator'); + await migrate(db, { migrationsFolder: './drizzle' }); +} \ No newline at end of file diff --git a/src/lib/db/index.ts b/src/lib/db/index.ts index 66414b3..fa2fff7 100644 --- a/src/lib/db/index.ts +++ b/src/lib/db/index.ts @@ -1,489 +1,85 @@ -import { z } from "zod"; -import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core"; import { Database } from "bun:sqlite"; import { drizzle } from "drizzle-orm/bun-sqlite"; import fs from "fs"; import path from "path"; -import { configSchema } from "./schema"; +import { migrate } from "drizzle-orm/bun-sqlite/migrator"; -// Define the database URL - for development we'll use a local SQLite file -const dataDir = path.join(process.cwd(), "data"); -// Ensure data directory exists -if (!fs.existsSync(dataDir)) { - fs.mkdirSync(dataDir, { recursive: true }); -} +// Skip database initialization in test environment +let db: ReturnType; -const dbPath = path.join(dataDir, "gitea-mirror.db"); +if (process.env.NODE_ENV !== "test") { + // Define the database URL - for development we'll use a local SQLite file + const dataDir = path.join(process.cwd(), "data"); + // Ensure data directory exists + if (!fs.existsSync(dataDir)) { + fs.mkdirSync(dataDir, { recursive: true }); + } -// Create an empty database file if it doesn't exist -if (!fs.existsSync(dbPath)) { - fs.writeFileSync(dbPath, ""); -} + const dbPath = path.join(dataDir, "gitea-mirror.db"); -// Create SQLite database instance using Bun's native driver -let sqlite: Database; -try { - sqlite = new Database(dbPath); - console.log("Successfully connected to SQLite database using Bun's native driver"); + // Create an empty database file if it doesn't exist + if (!fs.existsSync(dbPath)) { + fs.writeFileSync(dbPath, ""); + } - // Ensure all required tables exist - ensureTablesExist(sqlite); - - // Run migrations - runMigrations(sqlite); -} catch (error) { - console.error("Error opening database:", error); - throw error; -} - -/** - * Run database migrations - */ -function runMigrations(db: Database) { + // Create SQLite database instance using Bun's native driver + let sqlite: Database; try { - // Migration 1: Add destination_org column to organizations table - const orgTableInfo = db.query("PRAGMA table_info(organizations)").all() as Array<{name: string}>; - const hasDestinationOrg = orgTableInfo.some(col => col.name === 'destination_org'); - - if (!hasDestinationOrg) { - console.log("๐Ÿ”„ Running migration: Adding destination_org column to organizations table"); - db.exec("ALTER TABLE organizations ADD COLUMN destination_org TEXT"); - console.log("โœ… Migration completed: destination_org column added"); - } - - // Migration 2: Add destination_org column to repositories table - const repoTableInfo = db.query("PRAGMA table_info(repositories)").all() as Array<{name: string}>; - const hasRepoDestinationOrg = repoTableInfo.some(col => col.name === 'destination_org'); - - if (!hasRepoDestinationOrg) { - console.log("๐Ÿ”„ Running migration: Adding destination_org column to repositories table"); - db.exec("ALTER TABLE repositories ADD COLUMN destination_org TEXT"); - console.log("โœ… Migration completed: destination_org column added to repositories"); - } + sqlite = new Database(dbPath); + console.log("Successfully connected to SQLite database using Bun's native driver"); } catch (error) { - console.error("โŒ Error running migrations:", error); - // Don't throw - migrations should be non-breaking + console.error("Error opening database:", error); + throw error; } -} -/** - * Ensure all required tables exist in the database - */ -function ensureTablesExist(db: Database) { - const requiredTables = [ - "users", - "configs", - "repositories", - "organizations", - "mirror_jobs", - "events", - ]; + // Create drizzle instance with the SQLite client + db = drizzle({ client: sqlite }); - for (const table of requiredTables) { + /** + * Run Drizzle migrations + */ + function runDrizzleMigrations() { try { - // Check if table exists - const result = db.query(`SELECT name FROM sqlite_master WHERE type='table' AND name='${table}'`).get(); + console.log("๐Ÿ”„ Checking for pending migrations..."); + + // Check if migrations table exists + const migrationsTableExists = sqlite + .query("SELECT name FROM sqlite_master WHERE type='table' AND name='__drizzle_migrations'") + .get(); - if (!result) { - console.warn(`โš ๏ธ Table '${table}' is missing. Creating it now...`); - createTable(db, table); - console.log(`โœ… Table '${table}' created successfully`); + if (!migrationsTableExists) { + console.log("๐Ÿ“ฆ First time setup - running initial migrations..."); } + + // Run migrations using Drizzle migrate function + migrate(db, { migrationsFolder: "./drizzle" }); + + console.log("โœ… Database migrations completed successfully"); } catch (error) { - console.error(`โŒ Error checking/creating table '${table}':`, error); + console.error("โŒ Error running migrations:", error); throw error; } } + + // Run Drizzle migrations after db is initialized + runDrizzleMigrations(); } -/** - * Create a specific table with its schema - */ -function createTable(db: Database, tableName: string) { - switch (tableName) { - case "users": - db.exec(` - CREATE TABLE users ( - id TEXT PRIMARY KEY, - username TEXT NOT NULL, - password TEXT NOT NULL, - email TEXT NOT NULL, - created_at INTEGER NOT NULL, - updated_at INTEGER NOT NULL - ) - `); - break; +export { db }; - case "configs": - db.exec(` - CREATE TABLE configs ( - id TEXT PRIMARY KEY, - user_id TEXT NOT NULL, - name TEXT NOT NULL, - is_active INTEGER NOT NULL DEFAULT 1, - github_config TEXT NOT NULL, - gitea_config TEXT NOT NULL, - include TEXT NOT NULL DEFAULT '["*"]', - exclude TEXT NOT NULL DEFAULT '[]', - schedule_config TEXT NOT NULL, - cleanup_config TEXT NOT NULL, - created_at INTEGER NOT NULL DEFAULT (strftime('%s','now')), - updated_at INTEGER NOT NULL DEFAULT (strftime('%s','now')), - FOREIGN KEY (user_id) REFERENCES users(id) - ) - `); - break; - - case "repositories": - db.exec(` - CREATE TABLE repositories ( - id TEXT PRIMARY KEY, - user_id TEXT NOT NULL, - config_id TEXT NOT NULL, - name TEXT NOT NULL, - full_name TEXT NOT NULL, - url TEXT NOT NULL, - clone_url TEXT NOT NULL, - owner TEXT NOT NULL, - organization TEXT, - mirrored_location TEXT DEFAULT '', - is_private INTEGER NOT NULL DEFAULT 0, - is_fork INTEGER NOT NULL DEFAULT 0, - forked_from TEXT, - has_issues INTEGER NOT NULL DEFAULT 0, - is_starred INTEGER NOT NULL DEFAULT 0, - language TEXT, - description TEXT, - default_branch TEXT NOT NULL, - visibility TEXT NOT NULL DEFAULT 'public', - status TEXT NOT NULL DEFAULT 'imported', - last_mirrored INTEGER, - error_message TEXT, - created_at INTEGER NOT NULL DEFAULT (strftime('%s','now')), - updated_at INTEGER NOT NULL DEFAULT (strftime('%s','now')), - FOREIGN KEY (user_id) REFERENCES users(id), - FOREIGN KEY (config_id) REFERENCES configs(id) - ) - `); - - // Create indexes for repositories - db.exec(` - CREATE INDEX IF NOT EXISTS idx_repositories_user_id ON repositories(user_id); - CREATE INDEX IF NOT EXISTS idx_repositories_config_id ON repositories(config_id); - CREATE INDEX IF NOT EXISTS idx_repositories_status ON repositories(status); - CREATE INDEX IF NOT EXISTS idx_repositories_owner ON repositories(owner); - CREATE INDEX IF NOT EXISTS idx_repositories_organization ON repositories(organization); - CREATE INDEX IF NOT EXISTS idx_repositories_is_fork ON repositories(is_fork); - CREATE INDEX IF NOT EXISTS idx_repositories_is_starred ON repositories(is_starred); - `); - break; - - case "organizations": - db.exec(` - CREATE TABLE organizations ( - id TEXT PRIMARY KEY, - user_id TEXT NOT NULL, - config_id TEXT NOT NULL, - name TEXT NOT NULL, - avatar_url TEXT NOT NULL, - membership_role TEXT NOT NULL DEFAULT 'member', - is_included INTEGER NOT NULL DEFAULT 1, - status TEXT NOT NULL DEFAULT 'imported', - last_mirrored INTEGER, - error_message TEXT, - repository_count INTEGER NOT NULL DEFAULT 0, - destination_org TEXT, - created_at INTEGER NOT NULL DEFAULT (strftime('%s','now')), - updated_at INTEGER NOT NULL DEFAULT (strftime('%s','now')), - FOREIGN KEY (user_id) REFERENCES users(id), - FOREIGN KEY (config_id) REFERENCES configs(id) - ) - `); - - // Create indexes for organizations - db.exec(` - CREATE INDEX IF NOT EXISTS idx_organizations_user_id ON organizations(user_id); - CREATE INDEX IF NOT EXISTS idx_organizations_config_id ON organizations(config_id); - CREATE INDEX IF NOT EXISTS idx_organizations_status ON organizations(status); - CREATE INDEX IF NOT EXISTS idx_organizations_is_included ON organizations(is_included); - `); - break; - - case "mirror_jobs": - db.exec(` - CREATE TABLE mirror_jobs ( - id TEXT PRIMARY KEY, - user_id TEXT NOT NULL, - repository_id TEXT, - repository_name TEXT, - organization_id TEXT, - organization_name TEXT, - details TEXT, - status TEXT NOT NULL DEFAULT 'imported', - message TEXT NOT NULL, - timestamp TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - - -- New fields for job resilience - job_type TEXT NOT NULL DEFAULT 'mirror', - batch_id TEXT, - total_items INTEGER, - completed_items INTEGER DEFAULT 0, - item_ids TEXT, -- JSON array as text - completed_item_ids TEXT DEFAULT '[]', -- JSON array as text - in_progress INTEGER NOT NULL DEFAULT 0, -- Boolean as integer - started_at TIMESTAMP, - completed_at TIMESTAMP, - last_checkpoint TIMESTAMP, - - FOREIGN KEY (user_id) REFERENCES users(id) - ) - `); - - // Create indexes for mirror_jobs - db.exec(` - CREATE INDEX IF NOT EXISTS idx_mirror_jobs_user_id ON mirror_jobs(user_id); - CREATE INDEX IF NOT EXISTS idx_mirror_jobs_batch_id ON mirror_jobs(batch_id); - CREATE INDEX IF NOT EXISTS idx_mirror_jobs_in_progress ON mirror_jobs(in_progress); - CREATE INDEX IF NOT EXISTS idx_mirror_jobs_job_type ON mirror_jobs(job_type); - CREATE INDEX IF NOT EXISTS idx_mirror_jobs_timestamp ON mirror_jobs(timestamp); - `); - break; - - case "events": - db.exec(` - CREATE TABLE events ( - id TEXT PRIMARY KEY, - user_id TEXT NOT NULL, - channel TEXT NOT NULL, - payload TEXT NOT NULL, - read INTEGER NOT NULL DEFAULT 0, - created_at INTEGER NOT NULL DEFAULT (strftime('%s','now')), - FOREIGN KEY (user_id) REFERENCES users(id) - ) - `); - - // Create indexes for events - db.exec(` - CREATE INDEX IF NOT EXISTS idx_events_user_channel ON events(user_id, channel); - CREATE INDEX IF NOT EXISTS idx_events_created_at ON events(created_at); - CREATE INDEX IF NOT EXISTS idx_events_read ON events(read); - `); - break; - - default: - throw new Error(`Unknown table: ${tableName}`); - } -} - -// Create drizzle instance with the SQLite client -export const db = drizzle({ client: sqlite }); - -// Simple async wrapper around SQLite API for compatibility -// This maintains backward compatibility with existing code -export const client = { - async execute(sql: string, params?: any[]) { - try { - const stmt = sqlite.query(sql); - if (/^\s*select/i.test(sql)) { - const rows = stmt.all(params ?? []); - return { rows } as { rows: any[] }; - } - stmt.run(params ?? []); - return { rows: [] } as { rows: any[] }; - } catch (error) { - console.error(`Error executing SQL: ${sql}`, error); - throw error; - } - }, -}; - -// Define the tables -export const users = sqliteTable("users", { - id: text("id").primaryKey(), - username: text("username").notNull(), - password: text("password").notNull(), - email: text("email").notNull(), - createdAt: integer("created_at", { mode: "timestamp" }) - .notNull() - .default(new Date()), - updatedAt: integer("updated_at", { mode: "timestamp" }) - .notNull() - .default(new Date()), -}); - -// New table for event notifications (replacing Redis pub/sub) -export const events = sqliteTable("events", { - id: text("id").primaryKey(), - userId: text("user_id").notNull().references(() => users.id), - channel: text("channel").notNull(), - payload: text("payload", { mode: "json" }).notNull(), - read: integer("read", { mode: "boolean" }).notNull().default(false), - createdAt: integer("created_at", { mode: "timestamp" }) - .notNull() - .default(new Date()), -}); - -const githubSchema = configSchema.shape.githubConfig; -const giteaSchema = configSchema.shape.giteaConfig; -const scheduleSchema = configSchema.shape.scheduleConfig; -const cleanupSchema = configSchema.shape.cleanupConfig; - -export const configs = sqliteTable("configs", { - id: text("id").primaryKey(), - userId: text("user_id") - .notNull() - .references(() => users.id), - name: text("name").notNull(), - isActive: integer("is_active", { mode: "boolean" }).notNull().default(true), - - githubConfig: text("github_config", { mode: "json" }) - .$type>() - .notNull(), - - giteaConfig: text("gitea_config", { mode: "json" }) - .$type>() - .notNull(), - - include: text("include", { mode: "json" }) - .$type() - .notNull() - .default(["*"]), - - exclude: text("exclude", { mode: "json" }) - .$type() - .notNull() - .default([]), - - scheduleConfig: text("schedule_config", { mode: "json" }) - .$type>() - .notNull(), - - cleanupConfig: text("cleanup_config", { mode: "json" }) - .$type>() - .notNull(), - - createdAt: integer("created_at", { mode: "timestamp" }) - .notNull() - .default(new Date()), - - updatedAt: integer("updated_at", { mode: "timestamp" }) - .notNull() - .default(new Date()), -}); - -export const repositories = sqliteTable("repositories", { - id: text("id").primaryKey(), - userId: text("user_id") - .notNull() - .references(() => users.id), - configId: text("config_id") - .notNull() - .references(() => configs.id), - name: text("name").notNull(), - fullName: text("full_name").notNull(), - url: text("url").notNull(), - cloneUrl: text("clone_url").notNull(), - owner: text("owner").notNull(), - organization: text("organization"), - mirroredLocation: text("mirrored_location").default(""), - - isPrivate: integer("is_private", { mode: "boolean" }) - .notNull() - .default(false), - isForked: integer("is_fork", { mode: "boolean" }).notNull().default(false), - forkedFrom: text("forked_from"), - - hasIssues: integer("has_issues", { mode: "boolean" }) - .notNull() - .default(false), - isStarred: integer("is_starred", { mode: "boolean" }) - .notNull() - .default(false), - isArchived: integer("is_archived", { mode: "boolean" }) - .notNull() - .default(false), - - size: integer("size").notNull().default(0), - hasLFS: integer("has_lfs", { mode: "boolean" }).notNull().default(false), - hasSubmodules: integer("has_submodules", { mode: "boolean" }) - .notNull() - .default(false), - - defaultBranch: text("default_branch").notNull(), - visibility: text("visibility").notNull().default("public"), - - status: text("status").notNull().default("imported"), - lastMirrored: integer("last_mirrored", { mode: "timestamp" }), - errorMessage: text("error_message"), - - createdAt: integer("created_at", { mode: "timestamp" }) - .notNull() - .default(new Date()), - updatedAt: integer("updated_at", { mode: "timestamp" }) - .notNull() - .default(new Date()), -}); - -export const mirrorJobs = sqliteTable("mirror_jobs", { - id: text("id").primaryKey(), - userId: text("user_id") - .notNull() - .references(() => users.id), - repositoryId: text("repository_id"), - repositoryName: text("repository_name"), - organizationId: text("organization_id"), - organizationName: text("organization_name"), - details: text("details"), - status: text("status").notNull().default("imported"), - message: text("message").notNull(), - timestamp: integer("timestamp", { mode: "timestamp" }) - .notNull() - .default(new Date()), - - // New fields for job resilience - jobType: text("job_type").notNull().default("mirror"), - batchId: text("batch_id"), - totalItems: integer("total_items"), - completedItems: integer("completed_items").default(0), - itemIds: text("item_ids", { mode: "json" }).$type(), - completedItemIds: text("completed_item_ids", { mode: "json" }).$type().default([]), - inProgress: integer("in_progress", { mode: "boolean" }).notNull().default(false), - startedAt: integer("started_at", { mode: "timestamp" }), - completedAt: integer("completed_at", { mode: "timestamp" }), - lastCheckpoint: integer("last_checkpoint", { mode: "timestamp" }), -}); - -export const organizations = sqliteTable("organizations", { - id: text("id").primaryKey(), - userId: text("user_id") - .notNull() - .references(() => users.id), - configId: text("config_id") - .notNull() - .references(() => configs.id), - name: text("name").notNull(), - - avatarUrl: text("avatar_url").notNull(), - - membershipRole: text("membership_role").notNull().default("member"), - - isIncluded: integer("is_included", { mode: "boolean" }) - .notNull() - .default(true), - - // Override destination organization for this GitHub org's repos - destinationOrg: text("destination_org"), - - status: text("status").notNull().default("imported"), - lastMirrored: integer("last_mirrored", { mode: "timestamp" }), - errorMessage: text("error_message"), - - repositoryCount: integer("repository_count").notNull().default(0), - - createdAt: integer("created_at", { mode: "timestamp" }) - .notNull() - .default(new Date()), - updatedAt: integer("updated_at", { mode: "timestamp" }) - .notNull() - .default(new Date()), -}); +// Export all table definitions from schema +export { + users, + events, + configs, + repositories, + mirrorJobs, + organizations, + sessions, + accounts, + verificationTokens, + oauthApplications, + oauthAccessTokens, + oauthConsent, + ssoProviders +} from "./schema"; diff --git a/src/lib/db/schema.sql b/src/lib/db/schema.sql deleted file mode 100644 index 264645b..0000000 --- a/src/lib/db/schema.sql +++ /dev/null @@ -1,75 +0,0 @@ --- Users table -CREATE TABLE IF NOT EXISTS users ( - id TEXT PRIMARY KEY, - username TEXT NOT NULL UNIQUE, - password TEXT NOT NULL, - email TEXT NOT NULL, - created_at DATETIME NOT NULL, - updated_at DATETIME NOT NULL -); - --- Configurations table -CREATE TABLE IF NOT EXISTS configs ( - id TEXT PRIMARY KEY, - user_id TEXT NOT NULL, - name TEXT NOT NULL, - is_active BOOLEAN NOT NULL DEFAULT 1, - github_config TEXT NOT NULL, - gitea_config TEXT NOT NULL, - schedule_config TEXT NOT NULL, - include TEXT NOT NULL, - exclude TEXT NOT NULL, - created_at DATETIME NOT NULL, - updated_at DATETIME NOT NULL, - FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE -); - --- Repositories table -CREATE TABLE IF NOT EXISTS repositories ( - id TEXT PRIMARY KEY, - config_id TEXT NOT NULL, - name TEXT NOT NULL, - full_name TEXT NOT NULL, - url TEXT NOT NULL, - is_private BOOLEAN NOT NULL, - is_fork BOOLEAN NOT NULL, - owner TEXT NOT NULL, - organization TEXT, - mirrored_location TEXT DEFAULT '', - has_issues BOOLEAN NOT NULL, - is_starred BOOLEAN NOT NULL, - status TEXT NOT NULL, - error_message TEXT, - last_mirrored DATETIME, - created_at DATETIME NOT NULL, - updated_at DATETIME NOT NULL, - FOREIGN KEY (config_id) REFERENCES configs (id) ON DELETE CASCADE -); - --- Organizations table -CREATE TABLE IF NOT EXISTS organizations ( - id TEXT PRIMARY KEY, - config_id TEXT NOT NULL, - name TEXT NOT NULL, - type TEXT NOT NULL, - is_included BOOLEAN NOT NULL, - repository_count INTEGER NOT NULL, - created_at DATETIME NOT NULL, - updated_at DATETIME NOT NULL, - FOREIGN KEY (config_id) REFERENCES configs (id) ON DELETE CASCADE -); - --- Mirror jobs table -CREATE TABLE IF NOT EXISTS mirror_jobs ( - id TEXT PRIMARY KEY, - config_id TEXT NOT NULL, - repository_id TEXT, - status TEXT NOT NULL, - started_at DATETIME NOT NULL, - completed_at DATETIME, - log TEXT NOT NULL, - created_at DATETIME NOT NULL, - updated_at DATETIME NOT NULL, - FOREIGN KEY (config_id) REFERENCES configs (id) ON DELETE CASCADE, - FOREIGN KEY (repository_id) REFERENCES repositories (id) ON DELETE SET NULL -); diff --git a/src/lib/db/schema.ts b/src/lib/db/schema.ts index b9decb5..35dc62d 100644 --- a/src/lib/db/schema.ts +++ b/src/lib/db/schema.ts @@ -1,182 +1,615 @@ import { z } from "zod"; -import { repositoryVisibilityEnum, repoStatusEnum } from "@/types/Repository"; -import { membershipRoleEnum } from "@/types/organizations"; +import { sqliteTable, text, integer, index } from "drizzle-orm/sqlite-core"; +import { sql } from "drizzle-orm"; -// User schema +// ===== Zod Validation Schemas ===== export const userSchema = z.object({ - id: z.string().uuid().optional(), - username: z.string().min(3), - password: z.string().min(8).optional(), // Hashed password + id: z.string(), + username: z.string(), + password: z.string(), email: z.string().email(), - createdAt: z.date().default(() => new Date()), - updatedAt: z.date().default(() => new Date()), + emailVerified: z.boolean().default(false), + createdAt: z.coerce.date(), + updatedAt: z.coerce.date(), }); -export type User = z.infer; +export const githubConfigSchema = z.object({ + owner: z.string(), + type: z.enum(["personal", "organization"]), + token: z.string(), + includeStarred: z.boolean().default(false), + includeForks: z.boolean().default(true), + includeArchived: z.boolean().default(false), + includePrivate: z.boolean().default(true), + includePublic: z.boolean().default(true), + includeOrganizations: z.array(z.string()).default([]), + starredReposOrg: z.string().optional(), + mirrorStrategy: z.enum(["preserve", "single-org", "flat-user"]).default("preserve"), + defaultOrg: z.string().optional(), +}); + +export const giteaConfigSchema = z.object({ + url: z.string().url(), + token: z.string(), + defaultOwner: z.string(), + mirrorInterval: z.string().default("8h"), + lfs: z.boolean().default(false), + wiki: z.boolean().default(false), + visibility: z + .enum(["public", "private", "limited", "default"]) + .default("default"), + createOrg: z.boolean().default(true), + templateOwner: z.string().optional(), + templateRepo: z.string().optional(), + addTopics: z.boolean().default(true), + topicPrefix: z.string().optional(), + preserveVisibility: z.boolean().default(true), + forkStrategy: z + .enum(["skip", "reference", "full-copy"]) + .default("reference"), +}); + +export const scheduleConfigSchema = z.object({ + enabled: z.boolean().default(false), + interval: z.string().default("0 2 * * *"), + concurrent: z.boolean().default(false), + batchSize: z.number().default(10), + pauseBetweenBatches: z.number().default(5000), + retryAttempts: z.number().default(3), + retryDelay: z.number().default(60000), + timeout: z.number().default(3600000), + autoRetry: z.boolean().default(true), + cleanupBeforeMirror: z.boolean().default(false), + notifyOnFailure: z.boolean().default(true), + notifyOnSuccess: z.boolean().default(false), + logLevel: z.enum(["error", "warn", "info", "debug"]).default("info"), + timezone: z.string().default("UTC"), + onlyMirrorUpdated: z.boolean().default(false), + updateInterval: z.number().default(86400000), + skipRecentlyMirrored: z.boolean().default(true), + recentThreshold: z.number().default(3600000), +}); + +export const cleanupConfigSchema = z.object({ + enabled: z.boolean().default(false), + deleteFromGitea: z.boolean().default(false), + deleteIfNotInGitHub: z.boolean().default(true), + protectedRepos: z.array(z.string()).default([]), + dryRun: z.boolean().default(true), + orphanedRepoAction: z + .enum(["skip", "archive", "delete"]) + .default("archive"), + batchSize: z.number().default(10), + pauseBetweenDeletes: z.number().default(2000), +}); -// Configuration schema export const configSchema = z.object({ - id: z.string().uuid().optional(), - userId: z.string().uuid(), - name: z.string().min(1), + id: z.string(), + userId: z.string(), + name: z.string(), isActive: z.boolean().default(true), - githubConfig: z.object({ - username: z.string().min(1), - token: z.string().optional(), - skipForks: z.boolean().default(false), - privateRepositories: z.boolean().default(false), - mirrorIssues: z.boolean().default(false), - mirrorWiki: z.boolean().default(false), - mirrorStarred: z.boolean().default(false), - useSpecificUser: z.boolean().default(false), - singleRepo: z.string().optional(), - includeOrgs: z.array(z.string()).default([]), - excludeOrgs: z.array(z.string()).default([]), - mirrorPublicOrgs: z.boolean().default(false), - publicOrgs: z.array(z.string()).default([]), - skipStarredIssues: z.boolean().default(false), - }), - giteaConfig: z.object({ - username: z.string().min(1), - url: z.string().url(), - token: z.string().min(1), - organization: z.string().optional(), - visibility: z.enum(["public", "private", "limited"]).default("public"), - starredReposOrg: z.string().default("github"), - preserveOrgStructure: z.boolean().default(false), - mirrorStrategy: z.enum(["preserve", "single-org", "flat-user", "mixed"]).optional(), - personalReposOrg: z.string().optional(), // Override destination for personal repos - }), + githubConfig: githubConfigSchema, + giteaConfig: giteaConfigSchema, include: z.array(z.string()).default(["*"]), exclude: z.array(z.string()).default([]), - scheduleConfig: z.object({ - enabled: z.boolean().default(false), - interval: z.number().min(1).default(3600), // in seconds - lastRun: z.date().optional(), - nextRun: z.date().optional(), - }), - cleanupConfig: z.object({ - enabled: z.boolean().default(false), - retentionDays: z.number().min(1).default(604800), // in seconds (default: 7 days) - lastRun: z.date().optional(), - nextRun: z.date().optional(), - }), - createdAt: z.date().default(() => new Date()), - updatedAt: z.date().default(() => new Date()), + scheduleConfig: scheduleConfigSchema, + cleanupConfig: cleanupConfigSchema, + createdAt: z.coerce.date(), + updatedAt: z.coerce.date(), }); -export type Config = z.infer; - -// Repository schema export const repositorySchema = z.object({ - id: z.string().uuid().optional(), - userId: z.string().uuid().optional(), - configId: z.string().uuid(), - - name: z.string().min(1), - fullName: z.string().min(1), + id: z.string(), + userId: z.string(), + configId: z.string(), + name: z.string(), + fullName: z.string(), url: z.string().url(), cloneUrl: z.string().url(), - - owner: z.string().min(1), - organization: z.string().optional(), - + owner: z.string(), + organization: z.string().optional().nullable(), + mirroredLocation: z.string().default(""), isPrivate: z.boolean().default(false), isForked: z.boolean().default(false), - forkedFrom: z.string().optional(), - + forkedFrom: z.string().optional().nullable(), hasIssues: z.boolean().default(false), isStarred: z.boolean().default(false), isArchived: z.boolean().default(false), - - size: z.number(), + size: z.number().default(0), hasLFS: z.boolean().default(false), hasSubmodules: z.boolean().default(false), - + language: z.string().optional().nullable(), + description: z.string().optional().nullable(), defaultBranch: z.string(), - visibility: repositoryVisibilityEnum.default("public"), - - status: repoStatusEnum.default("imported"), - lastMirrored: z.date().optional(), - errorMessage: z.string().optional(), - - mirroredLocation: z.string().default(""), // Store the full Gitea path where repo was mirrored - destinationOrg: z.string().optional(), // Custom destination organization override - - createdAt: z.date().default(() => new Date()), - updatedAt: z.date().default(() => new Date()), + visibility: z.enum(["public", "private", "internal"]).default("public"), + status: z + .enum([ + "imported", + "mirroring", + "mirrored", + "failed", + "skipped", + "deleting", + "deleted", + "syncing", + "synced", + ]) + .default("imported"), + lastMirrored: z.coerce.date().optional().nullable(), + errorMessage: z.string().optional().nullable(), + destinationOrg: z.string().optional().nullable(), + createdAt: z.coerce.date(), + updatedAt: z.coerce.date(), }); -export type Repository = z.infer; - -// Mirror job schema export const mirrorJobSchema = z.object({ - id: z.string().uuid().optional(), - userId: z.string().uuid().optional(), - repositoryId: z.string().uuid().optional(), - repositoryName: z.string().optional(), - organizationId: z.string().uuid().optional(), - organizationName: z.string().optional(), - details: z.string().optional(), - status: repoStatusEnum.default("imported"), + id: z.string(), + userId: z.string(), + repositoryId: z.string().optional().nullable(), + repositoryName: z.string().optional().nullable(), + organizationId: z.string().optional().nullable(), + organizationName: z.string().optional().nullable(), + details: z.string().optional().nullable(), + status: z + .enum([ + "imported", + "mirroring", + "mirrored", + "failed", + "skipped", + "deleting", + "deleted", + "syncing", + "synced", + ]) + .default("imported"), message: z.string(), - timestamp: z.date().default(() => new Date()), - - // New fields for job resilience - jobType: z.enum(["mirror", "sync", "retry"]).default("mirror"), - batchId: z.string().uuid().optional(), // Group related jobs together - totalItems: z.number().optional(), // Total number of items to process - completedItems: z.number().optional(), // Number of items completed - itemIds: z.array(z.string()).optional(), // IDs of items to process - completedItemIds: z.array(z.string()).optional(), // IDs of completed items - inProgress: z.boolean().default(false), // Whether the job is currently running - startedAt: z.date().optional(), // When the job started - completedAt: z.date().optional(), // When the job completed - lastCheckpoint: z.date().optional(), // Last time progress was saved + timestamp: z.coerce.date(), + jobType: z.enum(["mirror", "cleanup", "import"]).default("mirror"), + batchId: z.string().optional().nullable(), + totalItems: z.number().optional().nullable(), + completedItems: z.number().default(0), + itemIds: z.array(z.string()).optional().nullable(), + completedItemIds: z.array(z.string()).default([]), + inProgress: z.boolean().default(false), + startedAt: z.coerce.date().optional().nullable(), + completedAt: z.coerce.date().optional().nullable(), + lastCheckpoint: z.coerce.date().optional().nullable(), }); -export type MirrorJob = z.infer; - -// Organization schema export const organizationSchema = z.object({ - id: z.string().uuid().optional(), - userId: z.string().uuid().optional(), - configId: z.string().uuid(), - - avatarUrl: z.string().url(), - - name: z.string().min(1), - - membershipRole: membershipRoleEnum.default("member"), - - isIncluded: z.boolean().default(false), - - status: repoStatusEnum.default("imported"), - lastMirrored: z.date().optional(), - errorMessage: z.string().optional(), - + id: z.string(), + userId: z.string(), + configId: z.string(), + name: z.string(), + avatarUrl: z.string(), + membershipRole: z.enum(["admin", "member", "owner"]).default("member"), + isIncluded: z.boolean().default(true), + destinationOrg: z.string().optional().nullable(), + status: z + .enum([ + "imported", + "mirroring", + "mirrored", + "failed", + "skipped", + "deleting", + "deleted", + "syncing", + "synced", + ]) + .default("imported"), + lastMirrored: z.coerce.date().optional().nullable(), + errorMessage: z.string().optional().nullable(), repositoryCount: z.number().default(0), - publicRepositoryCount: z.number().optional(), - privateRepositoryCount: z.number().optional(), - forkRepositoryCount: z.number().optional(), - - // Override destination organization for this GitHub org's repos - destinationOrg: z.string().optional(), - - createdAt: z.date().default(() => new Date()), - updatedAt: z.date().default(() => new Date()), + createdAt: z.coerce.date(), + updatedAt: z.coerce.date(), }); -export type Organization = z.infer; - -// Event schema (for SQLite-based pub/sub) export const eventSchema = z.object({ - id: z.string().uuid().optional(), - userId: z.string().uuid(), - channel: z.string().min(1), + id: z.string(), + userId: z.string(), + channel: z.string(), payload: z.any(), read: z.boolean().default(false), - createdAt: z.date().default(() => new Date()), + createdAt: z.coerce.date(), }); -export type Event = z.infer; +// ===== Drizzle Table Definitions ===== + +export const users = sqliteTable("users", { + id: text("id").primaryKey(), + name: text("name"), + email: text("email").notNull().unique(), + emailVerified: integer("email_verified", { mode: "boolean" }).notNull().default(false), + image: text("image"), + createdAt: integer("created_at", { mode: "timestamp" }) + .notNull() + .default(sql`(unixepoch())`), + updatedAt: integer("updated_at", { mode: "timestamp" }) + .notNull() + .default(sql`(unixepoch())`), + // Custom fields + username: text("username"), +}); + +export const events = sqliteTable("events", { + id: text("id").primaryKey(), + userId: text("user_id") + .notNull() + .references(() => users.id), + channel: text("channel").notNull(), + payload: text("payload", { mode: "json" }).notNull(), + read: integer("read", { mode: "boolean" }).notNull().default(false), + createdAt: integer("created_at", { mode: "timestamp" }) + .notNull() + .default(sql`(unixepoch())`), +}, (table) => { + return { + userChannelIdx: index("idx_events_user_channel").on(table.userId, table.channel), + createdAtIdx: index("idx_events_created_at").on(table.createdAt), + readIdx: index("idx_events_read").on(table.read), + }; +}); + +export const configs = sqliteTable("configs", { + id: text("id").primaryKey(), + userId: text("user_id") + .notNull() + .references(() => users.id), + name: text("name").notNull(), + isActive: integer("is_active", { mode: "boolean" }).notNull().default(true), + + githubConfig: text("github_config", { mode: "json" }) + .$type>() + .notNull(), + + giteaConfig: text("gitea_config", { mode: "json" }) + .$type>() + .notNull(), + + include: text("include", { mode: "json" }) + .$type() + .notNull() + .default(sql`'["*"]'`), + + exclude: text("exclude", { mode: "json" }) + .$type() + .notNull() + .default(sql`'[]'`), + + scheduleConfig: text("schedule_config", { mode: "json" }) + .$type>() + .notNull(), + + cleanupConfig: text("cleanup_config", { mode: "json" }) + .$type>() + .notNull(), + + createdAt: integer("created_at", { mode: "timestamp" }) + .notNull() + .default(sql`(unixepoch())`), + + updatedAt: integer("updated_at", { mode: "timestamp" }) + .notNull() + .default(sql`(unixepoch())`), +}); + +export const repositories = sqliteTable("repositories", { + id: text("id").primaryKey(), + userId: text("user_id") + .notNull() + .references(() => users.id), + configId: text("config_id") + .notNull() + .references(() => configs.id), + name: text("name").notNull(), + fullName: text("full_name").notNull(), + url: text("url").notNull(), + cloneUrl: text("clone_url").notNull(), + owner: text("owner").notNull(), + organization: text("organization"), + mirroredLocation: text("mirrored_location").default(""), + + isPrivate: integer("is_private", { mode: "boolean" }) + .notNull() + .default(false), + isForked: integer("is_fork", { mode: "boolean" }).notNull().default(false), + forkedFrom: text("forked_from"), + + hasIssues: integer("has_issues", { mode: "boolean" }) + .notNull() + .default(false), + isStarred: integer("is_starred", { mode: "boolean" }) + .notNull() + .default(false), + isArchived: integer("is_archived", { mode: "boolean" }) + .notNull() + .default(false), + + size: integer("size").notNull().default(0), + hasLFS: integer("has_lfs", { mode: "boolean" }).notNull().default(false), + hasSubmodules: integer("has_submodules", { mode: "boolean" }) + .notNull() + .default(false), + + language: text("language"), + description: text("description"), + defaultBranch: text("default_branch").notNull(), + visibility: text("visibility").notNull().default("public"), + + status: text("status").notNull().default("imported"), + lastMirrored: integer("last_mirrored", { mode: "timestamp" }), + errorMessage: text("error_message"), + + destinationOrg: text("destination_org"), + + createdAt: integer("created_at", { mode: "timestamp" }) + .notNull() + .default(sql`(unixepoch())`), + updatedAt: integer("updated_at", { mode: "timestamp" }) + .notNull() + .default(sql`(unixepoch())`), +}, (table) => { + return { + userIdIdx: index("idx_repositories_user_id").on(table.userId), + configIdIdx: index("idx_repositories_config_id").on(table.configId), + statusIdx: index("idx_repositories_status").on(table.status), + ownerIdx: index("idx_repositories_owner").on(table.owner), + organizationIdx: index("idx_repositories_organization").on(table.organization), + isForkedIdx: index("idx_repositories_is_fork").on(table.isForked), + isStarredIdx: index("idx_repositories_is_starred").on(table.isStarred), + }; +}); + +export const mirrorJobs = sqliteTable("mirror_jobs", { + id: text("id").primaryKey(), + userId: text("user_id") + .notNull() + .references(() => users.id), + repositoryId: text("repository_id"), + repositoryName: text("repository_name"), + organizationId: text("organization_id"), + organizationName: text("organization_name"), + details: text("details"), + status: text("status").notNull().default("imported"), + message: text("message").notNull(), + timestamp: integer("timestamp", { mode: "timestamp" }) + .notNull() + .default(sql`(unixepoch())`), + + // Job resilience fields + jobType: text("job_type").notNull().default("mirror"), + batchId: text("batch_id"), + totalItems: integer("total_items"), + completedItems: integer("completed_items").default(0), + itemIds: text("item_ids", { mode: "json" }).$type(), + completedItemIds: text("completed_item_ids", { mode: "json" }) + .$type() + .default(sql`'[]'`), + inProgress: integer("in_progress", { mode: "boolean" }) + .notNull() + .default(false), + startedAt: integer("started_at", { mode: "timestamp" }), + completedAt: integer("completed_at", { mode: "timestamp" }), + lastCheckpoint: integer("last_checkpoint", { mode: "timestamp" }), +}, (table) => { + return { + userIdIdx: index("idx_mirror_jobs_user_id").on(table.userId), + batchIdIdx: index("idx_mirror_jobs_batch_id").on(table.batchId), + inProgressIdx: index("idx_mirror_jobs_in_progress").on(table.inProgress), + jobTypeIdx: index("idx_mirror_jobs_job_type").on(table.jobType), + timestampIdx: index("idx_mirror_jobs_timestamp").on(table.timestamp), + }; +}); + +export const organizations = sqliteTable("organizations", { + id: text("id").primaryKey(), + userId: text("user_id") + .notNull() + .references(() => users.id), + configId: text("config_id") + .notNull() + .references(() => configs.id), + name: text("name").notNull(), + + avatarUrl: text("avatar_url").notNull(), + + membershipRole: text("membership_role").notNull().default("member"), + + isIncluded: integer("is_included", { mode: "boolean" }) + .notNull() + .default(true), + + destinationOrg: text("destination_org"), + + status: text("status").notNull().default("imported"), + lastMirrored: integer("last_mirrored", { mode: "timestamp" }), + errorMessage: text("error_message"), + + repositoryCount: integer("repository_count").notNull().default(0), + + createdAt: integer("created_at", { mode: "timestamp" }) + .notNull() + .default(sql`(unixepoch())`), + updatedAt: integer("updated_at", { mode: "timestamp" }) + .notNull() + .default(sql`(unixepoch())`), +}, (table) => { + return { + userIdIdx: index("idx_organizations_user_id").on(table.userId), + configIdIdx: index("idx_organizations_config_id").on(table.configId), + statusIdx: index("idx_organizations_status").on(table.status), + isIncludedIdx: index("idx_organizations_is_included").on(table.isIncluded), + }; +}); + +// ===== Better Auth Tables ===== + +// Sessions table +export const sessions = sqliteTable("sessions", { + id: text("id").primaryKey(), + token: text("token").notNull().unique(), + userId: text("user_id").notNull().references(() => users.id), + expiresAt: integer("expires_at", { mode: "timestamp" }).notNull(), + ipAddress: text("ip_address"), + userAgent: text("user_agent"), + createdAt: integer("created_at", { mode: "timestamp" }) + .notNull() + .default(sql`(unixepoch())`), + updatedAt: integer("updated_at", { mode: "timestamp" }) + .notNull() + .default(sql`(unixepoch())`), +}, (table) => { + return { + userIdIdx: index("idx_sessions_user_id").on(table.userId), + tokenIdx: index("idx_sessions_token").on(table.token), + expiresAtIdx: index("idx_sessions_expires_at").on(table.expiresAt), + }; +}); + +// Accounts table (for OAuth providers and credentials) +export const accounts = sqliteTable("accounts", { + id: text("id").primaryKey(), + accountId: text("account_id").notNull(), + userId: text("user_id").notNull().references(() => users.id), + providerId: text("provider_id").notNull(), + providerUserId: text("provider_user_id"), // Make nullable for email/password auth + accessToken: text("access_token"), + refreshToken: text("refresh_token"), + expiresAt: integer("expires_at", { mode: "timestamp" }), + password: text("password"), // For credential provider + createdAt: integer("created_at", { mode: "timestamp" }) + .notNull() + .default(sql`(unixepoch())`), + updatedAt: integer("updated_at", { mode: "timestamp" }) + .notNull() + .default(sql`(unixepoch())`), +}, (table) => { + return { + accountIdIdx: index("idx_accounts_account_id").on(table.accountId), + userIdIdx: index("idx_accounts_user_id").on(table.userId), + providerIdx: index("idx_accounts_provider").on(table.providerId, table.providerUserId), + }; +}); + +// Verification tokens table +export const verificationTokens = sqliteTable("verification_tokens", { + id: text("id").primaryKey(), + token: text("token").notNull().unique(), + identifier: text("identifier").notNull(), + type: text("type").notNull(), // email, password-reset, etc + expiresAt: integer("expires_at", { mode: "timestamp" }).notNull(), + createdAt: integer("created_at", { mode: "timestamp" }) + .notNull() + .default(sql`(unixepoch())`), +}, (table) => { + return { + tokenIdx: index("idx_verification_tokens_token").on(table.token), + identifierIdx: index("idx_verification_tokens_identifier").on(table.identifier), + }; +}); + +// ===== OIDC Provider Tables ===== + +// OAuth Applications table +export const oauthApplications = sqliteTable("oauth_applications", { + id: text("id").primaryKey(), + clientId: text("client_id").notNull().unique(), + clientSecret: text("client_secret").notNull(), + name: text("name").notNull(), + redirectURLs: text("redirect_urls").notNull(), // Comma-separated list + metadata: text("metadata"), // JSON string + type: text("type").notNull(), // web, mobile, etc + disabled: integer("disabled", { mode: "boolean" }).notNull().default(false), + userId: text("user_id"), // Optional - owner of the application + createdAt: integer("created_at", { mode: "timestamp" }) + .notNull() + .default(sql`(unixepoch())`), + updatedAt: integer("updated_at", { mode: "timestamp" }) + .notNull() + .default(sql`(unixepoch())`), +}, (table) => { + return { + clientIdIdx: index("idx_oauth_applications_client_id").on(table.clientId), + userIdIdx: index("idx_oauth_applications_user_id").on(table.userId), + }; +}); + +// OAuth Access Tokens table +export const oauthAccessTokens = sqliteTable("oauth_access_tokens", { + id: text("id").primaryKey(), + accessToken: text("access_token").notNull(), + refreshToken: text("refresh_token"), + accessTokenExpiresAt: integer("access_token_expires_at", { mode: "timestamp" }).notNull(), + refreshTokenExpiresAt: integer("refresh_token_expires_at", { mode: "timestamp" }), + clientId: text("client_id").notNull(), + userId: text("user_id").notNull().references(() => users.id), + scopes: text("scopes").notNull(), // Comma-separated list + createdAt: integer("created_at", { mode: "timestamp" }) + .notNull() + .default(sql`(unixepoch())`), + updatedAt: integer("updated_at", { mode: "timestamp" }) + .notNull() + .default(sql`(unixepoch())`), +}, (table) => { + return { + accessTokenIdx: index("idx_oauth_access_tokens_access_token").on(table.accessToken), + userIdIdx: index("idx_oauth_access_tokens_user_id").on(table.userId), + clientIdIdx: index("idx_oauth_access_tokens_client_id").on(table.clientId), + }; +}); + +// OAuth Consent table +export const oauthConsent = sqliteTable("oauth_consent", { + id: text("id").primaryKey(), + userId: text("user_id").notNull().references(() => users.id), + clientId: text("client_id").notNull(), + scopes: text("scopes").notNull(), // Comma-separated list + consentGiven: integer("consent_given", { mode: "boolean" }).notNull(), + createdAt: integer("created_at", { mode: "timestamp" }) + .notNull() + .default(sql`(unixepoch())`), + updatedAt: integer("updated_at", { mode: "timestamp" }) + .notNull() + .default(sql`(unixepoch())`), +}, (table) => { + return { + userIdIdx: index("idx_oauth_consent_user_id").on(table.userId), + clientIdIdx: index("idx_oauth_consent_client_id").on(table.clientId), + userClientIdx: index("idx_oauth_consent_user_client").on(table.userId, table.clientId), + }; +}); + +// ===== SSO Provider Tables ===== + +// SSO Providers table +export const ssoProviders = sqliteTable("sso_providers", { + id: text("id").primaryKey(), + issuer: text("issuer").notNull(), + domain: text("domain").notNull(), + oidcConfig: text("oidc_config").notNull(), // JSON string with OIDC configuration + userId: text("user_id").notNull(), // Admin who created this provider + providerId: text("provider_id").notNull().unique(), // Unique identifier for the provider + organizationId: text("organization_id"), // Optional - if provider is linked to an organization + createdAt: integer("created_at", { mode: "timestamp" }) + .notNull() + .default(sql`(unixepoch())`), + updatedAt: integer("updated_at", { mode: "timestamp" }) + .notNull() + .default(sql`(unixepoch())`), +}, (table) => { + return { + providerIdIdx: index("idx_sso_providers_provider_id").on(table.providerId), + domainIdx: index("idx_sso_providers_domain").on(table.domain), + issuerIdx: index("idx_sso_providers_issuer").on(table.issuer), + }; +}); + +// Export type definitions +export type User = z.infer; +export type Config = z.infer; +export type Repository = z.infer; +export type MirrorJob = z.infer; +export type Organization = z.infer; +export type Event = z.infer; \ No newline at end of file diff --git a/src/lib/deployment-mode.ts b/src/lib/deployment-mode.ts new file mode 100644 index 0000000..4f5db45 --- /dev/null +++ b/src/lib/deployment-mode.ts @@ -0,0 +1,22 @@ +/** + * Deployment mode utilities + * Supports both self-hosted and hosted versions + */ + +export const DEPLOYMENT_MODE = process.env.DEPLOYMENT_MODE || 'selfhosted'; + +export const isSelfHostedMode = () => DEPLOYMENT_MODE === 'selfhosted'; +export const isHostedMode = () => DEPLOYMENT_MODE === 'hosted'; + +/** + * Feature flags for self-hosted version + */ +export const features = { + // Core features available + githubSync: true, + giteaMirroring: true, + scheduling: true, + multiUser: true, + githubSponsors: true, + unlimitedRepos: true, +}; \ No newline at end of file diff --git a/src/lib/events/realtime.ts b/src/lib/events/realtime.ts new file mode 100644 index 0000000..cf322bc --- /dev/null +++ b/src/lib/events/realtime.ts @@ -0,0 +1,256 @@ +/** + * Real-time event system using EventEmitter + * For the self-hosted version + */ + +import { EventEmitter } from 'events'; + +export interface RealtimeEvent { + type: string; + userId?: string; + data: any; + timestamp: number; +} + +/** + * Real-time event bus for local instance + */ +export class RealtimeEventBus extends EventEmitter { + private channels = new Map void>>(); + private userChannels = new Map(); + + constructor() { + super(); + } + + /** + * Handle incoming events + */ + private handleIncomingEvent(channel: string, event: RealtimeEvent) { + // Emit to local listeners + this.emit(channel, event); + + // Call channel-specific handlers + const handlers = this.channels.get(channel); + if (handlers) { + handlers.forEach(handler => { + try { + handler(event); + } catch (error) { + console.error('Error in event handler:', error); + } + }); + } + } + + /** + * Subscribe to a channel + */ + async subscribe(channel: string, handler?: (event: RealtimeEvent) => void) { + // Add handler if provided + if (handler) { + if (!this.channels.has(channel)) { + this.channels.set(channel, new Set()); + } + this.channels.get(channel)!.add(handler); + } + + // Add local listener + if (!this.listenerCount(channel)) { + this.on(channel, (event) => this.handleIncomingEvent(channel, event)); + } + } + + /** + * Subscribe to user-specific channels + */ + async subscribeUser(userId: string) { + const channels = [ + `user:${userId}`, + `user:${userId}:notifications`, + `user:${userId}:updates`, + ]; + + this.userChannels.set(userId, channels); + + for (const channel of channels) { + await this.subscribe(channel); + } + } + + /** + * Unsubscribe from a channel + */ + async unsubscribe(channel: string, handler?: (event: RealtimeEvent) => void) { + // Remove handler if provided + if (handler) { + this.channels.get(channel)?.delete(handler); + + // Remove channel if no handlers left + if (this.channels.get(channel)?.size === 0) { + this.channels.delete(channel); + } + } + + // Remove local listener if no handlers + if (!this.channels.has(channel)) { + this.removeAllListeners(channel); + } + } + + /** + * Unsubscribe from user channels + */ + async unsubscribeUser(userId: string) { + const channels = this.userChannels.get(userId) || []; + + for (const channel of channels) { + await this.unsubscribe(channel); + } + + this.userChannels.delete(userId); + } + + /** + * Publish an event + */ + async publish(channel: string, event: Omit) { + const fullEvent: RealtimeEvent = { + ...event, + timestamp: Date.now(), + }; + + // Emit locally + this.handleIncomingEvent(channel, fullEvent); + } + + /** + * Broadcast to all users + */ + async broadcast(event: Omit) { + await this.publish('broadcast', event); + } + + /** + * Send event to specific user + */ + async sendToUser(userId: string, event: Omit) { + await this.publish(`user:${userId}`, { + ...event, + userId, + }); + } + + /** + * Send activity update + */ + async sendActivity(activity: { + userId: string; + action: string; + resource: string; + resourceId: string; + details?: any; + }) { + const event = { + type: 'activity', + data: activity, + }; + + // Send to user + await this.sendToUser(activity.userId, event); + + // Also publish to activity channel + await this.publish('activity', { + ...event, + userId: activity.userId, + }); + } + + /** + * Get event statistics + */ + getStats() { + return { + channels: this.channels.size, + listeners: Array.from(this.channels.values()).reduce( + (sum, handlers) => sum + handlers.size, + 0 + ), + userChannels: this.userChannels.size, + }; + } +} + +// Global event bus instance +export const eventBus = new RealtimeEventBus(); + +/** + * React hook for subscribing to events + */ +export function useRealtimeEvents( + channel: string, + handler: (event: RealtimeEvent) => void, + deps: any[] = [] +) { + if (typeof window !== 'undefined') { + const { useEffect } = require('react'); + + useEffect(() => { + eventBus.subscribe(channel, handler); + + return () => { + eventBus.unsubscribe(channel, handler); + }; + }, deps); + } +} + +/** + * Server-sent events endpoint handler + */ +export async function createSSEHandler(userId: string) { + const encoder = new TextEncoder(); + + // Create a readable stream for SSE + const stream = new ReadableStream({ + async start(controller) { + // Send initial connection event + controller.enqueue( + encoder.encode(`data: ${JSON.stringify({ type: 'connected' })}\n\n`) + ); + + // Subscribe to user channels + await eventBus.subscribeUser(userId); + + // Create event handler + const handleEvent = (event: RealtimeEvent) => { + controller.enqueue( + encoder.encode(`data: ${JSON.stringify(event)}\n\n`) + ); + }; + + // Subscribe to channels + eventBus.on(`user:${userId}`, handleEvent); + + // Keep connection alive with heartbeat + const heartbeat = setInterval(() => { + controller.enqueue(encoder.encode(': heartbeat\n\n')); + }, 30000); + + // Cleanup on close + return () => { + clearInterval(heartbeat); + eventBus.off(`user:${userId}`, handleEvent); + eventBus.unsubscribeUser(userId); + }; + }, + }); + + return new Response(stream, { + headers: { + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache', + 'Connection': 'keep-alive', + }, + }); +} \ No newline at end of file diff --git a/src/lib/gitea.ts b/src/lib/gitea.ts index e8a115c..d10a195 100644 --- a/src/lib/gitea.ts +++ b/src/lib/gitea.ts @@ -11,6 +11,7 @@ import { httpPost, httpGet } from "./http-client"; import { createMirrorJob } from "./helpers"; import { db, organizations, repositories } from "./db"; import { eq, and } from "drizzle-orm"; +import { decryptConfigTokens } from "./utils/config-encryption"; /** * Helper function to get organization configuration including destination override @@ -183,12 +184,15 @@ export const isRepoPresentInGitea = async ({ throw new Error("Gitea config is required."); } + // Decrypt config tokens for API usage + const decryptedConfig = decryptConfigTokens(config as Config); + // Check if the repository exists at the specified owner location const response = await fetch( `${config.giteaConfig.url}/api/v1/repos/${owner}/${repoName}`, { headers: { - Authorization: `token ${config.giteaConfig.token}`, + Authorization: `token ${decryptedConfig.giteaConfig.token}`, }, } ); @@ -371,7 +375,7 @@ export const mirrorGithubRepoToGitea = async ({ service: "git", }, { - Authorization: `token ${config.giteaConfig.token}`, + Authorization: `token ${decryptedConfig.giteaConfig.token}`, } ); @@ -392,7 +396,7 @@ export const mirrorGithubRepoToGitea = async ({ config, octokit, repository, - isRepoInOrg: false, + giteaOwner: repoOwner, }); } @@ -476,11 +480,14 @@ export async function getOrCreateGiteaOrg({ try { console.log(`Attempting to get or create Gitea organization: ${orgName}`); + // Decrypt config tokens for API usage + const decryptedConfig = decryptConfigTokens(config as Config); + const orgRes = await fetch( `${config.giteaConfig.url}/api/v1/orgs/${orgName}`, { headers: { - Authorization: `token ${config.giteaConfig.token}`, + Authorization: `token ${decryptedConfig.giteaConfig.token}`, "Content-Type": "application/json", }, } @@ -533,7 +540,7 @@ export async function getOrCreateGiteaOrg({ const createRes = await fetch(`${config.giteaConfig.url}/api/v1/orgs`, { method: "POST", headers: { - Authorization: `token ${config.giteaConfig.token}`, + Authorization: `token ${decryptedConfig.giteaConfig.token}`, "Content-Type": "application/json", }, body: JSON.stringify({ @@ -720,7 +727,7 @@ export async function mirrorGitHubRepoToGiteaOrg({ private: repository.isPrivate, }, { - Authorization: `token ${config.giteaConfig.token}`, + Authorization: `token ${decryptedConfig.giteaConfig.token}`, } ); @@ -741,7 +748,7 @@ export async function mirrorGitHubRepoToGiteaOrg({ config, octokit, repository, - isRepoInOrg: true, + giteaOwner: orgName, }); } @@ -1074,6 +1081,9 @@ export const syncGiteaRepo = async ({ throw new Error("Gitea config is required."); } + // Decrypt config tokens for API usage + const decryptedConfig = decryptConfigTokens(config as Config); + console.log(`Syncing repository ${repository.name}`); // Mark repo as "syncing" in DB @@ -1183,12 +1193,12 @@ export const mirrorGitRepoIssuesToGitea = async ({ config, octokit, repository, - isRepoInOrg, + giteaOwner, }: { config: Partial; octokit: Octokit; repository: Repository; - isRepoInOrg: boolean; + giteaOwner: string; }) => { //things covered here are- issue, title, body, labels, comments and assignees if ( @@ -1200,9 +1210,8 @@ export const mirrorGitRepoIssuesToGitea = async ({ throw new Error("Missing GitHub or Gitea configuration."); } - const repoOrigin = isRepoInOrg - ? repository.organization - : config.githubConfig.username; + // Decrypt config tokens for API usage + const decryptedConfig = decryptConfigTokens(config as Config); const [owner, repo] = repository.fullName.split("/"); @@ -1232,7 +1241,7 @@ export const mirrorGitRepoIssuesToGitea = async ({ // Get existing labels from Gitea const giteaLabelsRes = await httpGet( - `${config.giteaConfig.url}/api/v1/repos/${repoOrigin}/${repository.name}/labels`, + `${config.giteaConfig.url}/api/v1/repos/${giteaOwner}/${repository.name}/labels`, { Authorization: `token ${config.giteaConfig.token}`, } @@ -1264,7 +1273,7 @@ export const mirrorGitRepoIssuesToGitea = async ({ } else { try { const created = await httpPost( - `${config.giteaConfig!.url}/api/v1/repos/${repoOrigin}/${ + `${config.giteaConfig!.url}/api/v1/repos/${giteaOwner}/${ repository.name }/labels`, { name, color: "#ededed" }, // Default color @@ -1301,7 +1310,7 @@ export const mirrorGitRepoIssuesToGitea = async ({ // Create the issue in Gitea const createdIssue = await httpPost( - `${config.giteaConfig!.url}/api/v1/repos/${repoOrigin}/${ + `${config.giteaConfig!.url}/api/v1/repos/${giteaOwner}/${ repository.name }/issues`, issuePayload, @@ -1328,7 +1337,7 @@ export const mirrorGitRepoIssuesToGitea = async ({ comments, async (comment) => { await httpPost( - `${config.giteaConfig!.url}/api/v1/repos/${repoOrigin}/${ + `${config.giteaConfig!.url}/api/v1/repos/${giteaOwner}/${ repository.name }/issues/${createdIssue.data.number}/comments`, { diff --git a/src/lib/modules/registry.ts b/src/lib/modules/registry.ts new file mode 100644 index 0000000..0299e0d --- /dev/null +++ b/src/lib/modules/registry.ts @@ -0,0 +1,184 @@ +/** + * Module registry implementation + * Manages loading and access to modular features + */ + +import type { + Module, + ModuleRegistry, + AppContext, + RouteHandler, + Middleware, + DatabaseAdapter, + EventEmitter +} from './types'; +// Module registry for extensibility + +/** + * Simple event emitter implementation + */ +class SimpleEventEmitter implements EventEmitter { + private events: Map> = new Map(); + + on(event: string, handler: (...args: any[]) => void): void { + if (!this.events.has(event)) { + this.events.set(event, new Set()); + } + this.events.get(event)!.add(handler); + } + + off(event: string, handler: (...args: any[]) => void): void { + this.events.get(event)?.delete(handler); + } + + emit(event: string, ...args: any[]): void { + this.events.get(event)?.forEach(handler => { + try { + handler(...args); + } catch (error) { + console.error(`Error in event handler for ${event}:`, error); + } + }); + } +} + +/** + * Module manager class + */ +export class ModuleManager { + private modules: Map = new Map(); + private routes: Map = new Map(); + private middlewares: Middleware[] = []; + private events = new SimpleEventEmitter(); + private initialized = false; + + /** + * Get app context for modules + */ + private getAppContext(): AppContext { + return { + addRoute: (path, handler) => this.addRoute(path, handler), + addMiddleware: (middleware) => this.middlewares.push(middleware), + db: this.getDatabaseAdapter(), + events: this.events, + modules: this.getRegistry(), + }; + } + + /** + * Get database adapter based on deployment mode + */ + private getDatabaseAdapter(): DatabaseAdapter { + // This would be implemented to use SQLite or PostgreSQL + // based on deployment mode + return { + query: async (sql, params) => [], + execute: async (sql, params) => {}, + transaction: async (fn) => fn(), + }; + } + + /** + * Register a module + */ + async register(module: Module): Promise { + if (this.modules.has(module.name)) { + console.warn(`Module ${module.name} is already registered`); + return; + } + + try { + await module.init(this.getAppContext()); + this.modules.set(module.name, module); + console.log(`Module ${module.name} registered successfully`); + } catch (error) { + console.error(`Failed to register module ${module.name}:`, error); + throw error; + } + } + + /** + * Unregister a module + */ + async unregister(moduleName: string): Promise { + const module = this.modules.get(moduleName); + if (!module) return; + + if (module.cleanup) { + await module.cleanup(); + } + + this.modules.delete(moduleName); + // Remove routes registered by this module + // This would need to track which module registered which routes + } + + /** + * Add a route handler + */ + private addRoute(path: string, handler: RouteHandler): void { + this.routes.set(path, handler); + } + + /** + * Get route handler for a path + */ + getRouteHandler(path: string): RouteHandler | null { + return this.routes.get(path) || null; + } + + /** + * Get all middleware + */ + getMiddleware(): Middleware[] { + return [...this.middlewares]; + } + + /** + * Get module registry + */ + getRegistry(): ModuleRegistry { + const registry: ModuleRegistry = {}; + + // Copy all modules to registry + for (const [name, module] of this.modules) { + registry[name] = module; + } + + return registry; + } + + + /** + * Get a specific module + */ + get(name: K): ModuleRegistry[K] | null { + return this.getRegistry()[name] || null; + } + + /** + * Check if a module is loaded + */ + has(name: string): boolean { + return this.modules.has(name); + } + + /** + * Emit an event to all modules + */ + emit(event: string, ...args: any[]): void { + this.events.emit(event, ...args); + } +} + +// Global module manager instance +export const modules = new ModuleManager(); + + +// Initialize modules on app start +export async function initializeModules() { + // Load core modules here if any + + // Emit initialization complete event + modules.emit('modules:initialized'); +} \ No newline at end of file diff --git a/src/lib/modules/types.d.ts b/src/lib/modules/types.d.ts new file mode 100644 index 0000000..001c833 --- /dev/null +++ b/src/lib/modules/types.d.ts @@ -0,0 +1,86 @@ +/** + * Module system type definitions + * These interfaces allow for extensibility and plugins + */ +import type { APIContext } from 'astro'; +import type { ComponentType, LazyExoticComponent } from 'react'; +/** + * Base module interface that all modules must implement + */ +export interface Module { + /** Unique module identifier */ + name: string; + /** Module version */ + version: string; + /** Initialize the module with app context */ + init(app: AppContext): Promise; + /** Cleanup when module is unloaded */ + cleanup?(): Promise; +} +/** + * Application context passed to modules + */ +export interface AppContext { + /** Register API routes */ + addRoute(path: string, handler: RouteHandler): void; + /** Register middleware */ + addMiddleware(middleware: Middleware): void; + /** Access to database (abstracted) */ + db: DatabaseAdapter; + /** Event emitter for cross-module communication */ + events: EventEmitter; + /** Access to other modules */ + modules: ModuleRegistry; +} +/** + * Route handler type + */ +export type RouteHandler = (context: APIContext) => Promise | Response; +/** + * Middleware type + */ +export type Middleware = (context: APIContext, next: () => Promise) => Promise; +/** + * Database adapter interface (abstract away implementation) + */ +export interface DatabaseAdapter { + query(sql: string, params?: any[]): Promise; + execute(sql: string, params?: any[]): Promise; + transaction(fn: () => Promise): Promise; +} +/** + * Event emitter for cross-module communication + */ +export interface EventEmitter { + on(event: string, handler: (...args: any[]) => void): void; + off(event: string, handler: (...args: any[]) => void): void; + emit(event: string, ...args: any[]): void; +} +/** + * Example module interfaces + * These are examples of how modules can be structured + */ +export interface FeatureModule extends Module { + /** React components provided by the module */ + components?: Record>>; + /** API methods provided by the module */ + api?: Record Promise>; + /** Lifecycle hooks */ + hooks?: { + onInit?: () => Promise; + onUserAction?: (action: string, data: any) => Promise; + }; +} +/** + * Module registry interface + */ +export interface ModuleRegistry { + [key: string]: Module | undefined; +} +export interface User { + id: string; + email: string; + name?: string; + username?: string; +} +//# sourceMappingURL=types.d.ts.map \ No newline at end of file diff --git a/src/lib/modules/types.d.ts.map b/src/lib/modules/types.d.ts.map new file mode 100644 index 0000000..9cf8369 --- /dev/null +++ b/src/lib/modules/types.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["types.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,OAAO,CAAC;AACxC,OAAO,KAAK,EAAE,aAAa,EAAE,mBAAmB,EAAE,MAAM,OAAO,CAAC;AAEhE;;GAEG;AACH,MAAM,WAAW,MAAM;IACrB,+BAA+B;IAC/B,IAAI,EAAE,MAAM,CAAC;IAEb,qBAAqB;IACrB,OAAO,EAAE,MAAM,CAAC;IAEhB,6CAA6C;IAC7C,IAAI,CAAC,GAAG,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAErC,sCAAsC;IACtC,OAAO,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC3B;AAED;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,0BAA0B;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,GAAG,IAAI,CAAC;IAEpD,0BAA0B;IAC1B,aAAa,CAAC,UAAU,EAAE,UAAU,GAAG,IAAI,CAAC;IAE5C,sCAAsC;IACtC,EAAE,EAAE,eAAe,CAAC;IAEpB,mDAAmD;IACnD,MAAM,EAAE,YAAY,CAAC;IAErB,8BAA8B;IAC9B,OAAO,EAAE,cAAc,CAAC;CACzB;AAED;;GAEG;AACH,MAAM,MAAM,YAAY,GAAG,CAAC,OAAO,EAAE,UAAU,KAAK,OAAO,CAAC,QAAQ,CAAC,GAAG,QAAQ,CAAC;AAEjF;;GAEG;AACH,MAAM,MAAM,UAAU,GAAG,CACvB,OAAO,EAAE,UAAU,EACnB,IAAI,EAAE,MAAM,OAAO,CAAC,QAAQ,CAAC,KAC1B,OAAO,CAAC,QAAQ,CAAC,CAAC;AAEvB;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,KAAK,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,GAAG,EAAE,GAAG,OAAO,CAAC,CAAC,EAAE,CAAC,CAAC;IACpD,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,GAAG,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACpD,WAAW,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;CAClD;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,GAAG,IAAI,CAAC;IAC3D,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,GAAG,IAAI,CAAC;IAC5D,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;CAC3C;AAED;;;GAGG;AAGH,MAAM,WAAW,aAAc,SAAQ,MAAM;IAC3C,8CAA8C;IAC9C,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,mBAAmB,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;IAErE,yCAAyC;IACzC,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC;IAEvD,sBAAsB;IACtB,KAAK,CAAC,EAAE;QACN,MAAM,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;QAC7B,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;KAC7D,CAAC;CACH;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;CACnC;AAGD,MAAM,WAAW,IAAI;IACnB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB"} \ No newline at end of file diff --git a/src/lib/modules/types.js b/src/lib/modules/types.js new file mode 100644 index 0000000..4ceb2c0 --- /dev/null +++ b/src/lib/modules/types.js @@ -0,0 +1,5 @@ +/** + * Module system type definitions + * These interfaces allow for extensibility and plugins + */ +export {}; diff --git a/src/lib/modules/types.ts b/src/lib/modules/types.ts new file mode 100644 index 0000000..d8469e6 --- /dev/null +++ b/src/lib/modules/types.ts @@ -0,0 +1,110 @@ +/** + * Module system type definitions + * These interfaces allow for extensibility and plugins + */ + +import type { APIContext } from 'astro'; +import type { ComponentType, LazyExoticComponent } from 'react'; + +/** + * Base module interface that all modules must implement + */ +export interface Module { + /** Unique module identifier */ + name: string; + + /** Module version */ + version: string; + + /** Initialize the module with app context */ + init(app: AppContext): Promise; + + /** Cleanup when module is unloaded */ + cleanup?(): Promise; +} + +/** + * Application context passed to modules + */ +export interface AppContext { + /** Register API routes */ + addRoute(path: string, handler: RouteHandler): void; + + /** Register middleware */ + addMiddleware(middleware: Middleware): void; + + /** Access to database (abstracted) */ + db: DatabaseAdapter; + + /** Event emitter for cross-module communication */ + events: EventEmitter; + + /** Access to other modules */ + modules: ModuleRegistry; +} + +/** + * Route handler type + */ +export type RouteHandler = (context: APIContext) => Promise | Response; + +/** + * Middleware type + */ +export type Middleware = ( + context: APIContext, + next: () => Promise +) => Promise; + +/** + * Database adapter interface (abstract away implementation) + */ +export interface DatabaseAdapter { + query(sql: string, params?: any[]): Promise; + execute(sql: string, params?: any[]): Promise; + transaction(fn: () => Promise): Promise; +} + +/** + * Event emitter for cross-module communication + */ +export interface EventEmitter { + on(event: string, handler: (...args: any[]) => void): void; + off(event: string, handler: (...args: any[]) => void): void; + emit(event: string, ...args: any[]): void; +} + +/** + * Example module interfaces + * These are examples of how modules can be structured + */ + +// Example: Feature module with components +export interface FeatureModule extends Module { + /** React components provided by the module */ + components?: Record>>; + + /** API methods provided by the module */ + api?: Record Promise>; + + /** Lifecycle hooks */ + hooks?: { + onInit?: () => Promise; + onUserAction?: (action: string, data: any) => Promise; + }; +} + +/** + * Module registry interface + */ +export interface ModuleRegistry { + [key: string]: Module | undefined; +} + +// Generic types that modules might use +export interface User { + id: string; + email: string; + name?: string; + username?: string; +} \ No newline at end of file diff --git a/src/lib/recovery.ts b/src/lib/recovery.ts index 5129ec1..0d00aa8 100644 --- a/src/lib/recovery.ts +++ b/src/lib/recovery.ts @@ -11,6 +11,7 @@ import { createGitHubClient } from './github'; import { processWithResilience } from './utils/concurrency'; import { repositoryVisibilityEnum, repoStatusEnum } from '@/types/Repository'; import type { Repository } from './db/schema'; +import { getDecryptedGitHubToken } from './utils/config-encryption'; // Recovery state tracking let recoveryInProgress = false; @@ -262,7 +263,8 @@ async function recoverMirrorJob(job: any, remainingItemIds: string[]) { // Create GitHub client with error handling let octokit; try { - octokit = createGitHubClient(config.githubConfig.token); + const decryptedToken = getDecryptedGitHubToken(config); + octokit = createGitHubClient(decryptedToken); } catch (error) { throw new Error(`Failed to create GitHub client: ${error instanceof Error ? error.message : String(error)}`); } diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 4f37651..eae92a1 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -9,6 +9,15 @@ export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); } +export function generateRandomString(length: number): string { + const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + let result = ''; + for (let i = 0; i < length; i++) { + result += chars.charAt(Math.floor(Math.random() * chars.length)); + } + return result; +} + export function formatDate(date?: Date | string | null): string { if (!date) return "Never"; return new Intl.DateTimeFormat("en-US", { @@ -185,7 +194,7 @@ export async function apiRequest( } } -export const getStatusColor = (status: RepoStatus): string => { +export const getStatusColor = (status: string): string => { switch (status) { case "imported": return "bg-blue-500"; // Info/primary-like @@ -199,6 +208,12 @@ export const getStatusColor = (status: RepoStatus): string => { return "bg-indigo-500"; // Sync in progress case "synced": return "bg-teal-500"; // Sync complete + case "skipped": + return "bg-gray-500"; // Skipped + case "deleting": + return "bg-orange-500"; // Deleting + case "deleted": + return "bg-gray-600"; // Deleted default: return "bg-gray-400"; // Unknown/neutral } diff --git a/src/lib/utils/auth-helpers.ts b/src/lib/utils/auth-helpers.ts new file mode 100644 index 0000000..10e2336 --- /dev/null +++ b/src/lib/utils/auth-helpers.ts @@ -0,0 +1,58 @@ +import type { APIRoute, APIContext } from "astro"; +import { auth } from "@/lib/auth"; + +/** + * Get authenticated user from request + * @param request - The request object from Astro API route + * @returns The authenticated user or null if not authenticated + */ +export async function getAuthenticatedUser(request: Request) { + try { + const session = await auth.api.getSession({ + headers: request.headers, + }); + + return session ? session.user : null; + } catch (error) { + console.error("Error getting session:", error); + return null; + } +} + +/** + * Require authentication for API routes + * Returns an error response if user is not authenticated + * @param context - The API context from Astro + * @returns Object with user if authenticated, or error response if not + */ +export async function requireAuth(context: APIContext) { + const user = await getAuthenticatedUser(context.request); + + if (!user) { + return { + user: null, + response: new Response( + JSON.stringify({ + success: false, + error: "Unauthorized - Please log in", + }), + { + status: 401, + headers: { "Content-Type": "application/json" }, + } + ), + }; + } + + return { user, response: null }; +} + +/** + * Get user ID from authenticated session + * @param request - The request object from Astro API route + * @returns The user ID or null if not authenticated + */ +export async function getAuthenticatedUserId(request: Request): Promise { + const user = await getAuthenticatedUser(request); + return user?.id || null; +} \ No newline at end of file diff --git a/src/lib/utils/config-encryption.ts b/src/lib/utils/config-encryption.ts new file mode 100644 index 0000000..a223c18 --- /dev/null +++ b/src/lib/utils/config-encryption.ts @@ -0,0 +1,52 @@ +import { decrypt } from "./encryption"; +import type { Config } from "@/types/config"; + +/** + * Decrypts tokens in a config object for use in API calls + * @param config The config object with potentially encrypted tokens + * @returns Config object with decrypted tokens + */ +export function decryptConfigTokens(config: Config): Config { + const decryptedConfig = { ...config }; + + // Deep clone the config objects + if (config.githubConfig) { + decryptedConfig.githubConfig = { ...config.githubConfig }; + if (config.githubConfig.token) { + decryptedConfig.githubConfig.token = decrypt(config.githubConfig.token); + } + } + + if (config.giteaConfig) { + decryptedConfig.giteaConfig = { ...config.giteaConfig }; + if (config.giteaConfig.token) { + decryptedConfig.giteaConfig.token = decrypt(config.giteaConfig.token); + } + } + + return decryptedConfig; +} + +/** + * Gets a decrypted GitHub token from config + * @param config The config object + * @returns Decrypted GitHub token + */ +export function getDecryptedGitHubToken(config: Config): string { + if (!config.githubConfig?.token) { + throw new Error("GitHub token not found in config"); + } + return decrypt(config.githubConfig.token); +} + +/** + * Gets a decrypted Gitea token from config + * @param config The config object + * @returns Decrypted Gitea token + */ +export function getDecryptedGiteaToken(config: Config): string { + if (!config.giteaConfig?.token) { + throw new Error("Gitea token not found in config"); + } + return decrypt(config.giteaConfig.token); +} \ No newline at end of file diff --git a/src/lib/utils/encryption.ts b/src/lib/utils/encryption.ts new file mode 100644 index 0000000..0c72f83 --- /dev/null +++ b/src/lib/utils/encryption.ts @@ -0,0 +1,169 @@ +import * as crypto from "crypto"; + +// Encryption configuration +const ALGORITHM = "aes-256-gcm"; +const IV_LENGTH = 16; // 128 bits +const SALT_LENGTH = 32; // 256 bits +const TAG_LENGTH = 16; // 128 bits +const KEY_LENGTH = 32; // 256 bits +const ITERATIONS = 100000; // PBKDF2 iterations + +// Get or generate encryption key +function getEncryptionKey(): Buffer { + const secret = process.env.ENCRYPTION_SECRET || process.env.JWT_SECRET || process.env.BETTER_AUTH_SECRET; + + if (!secret) { + throw new Error("No encryption secret found. Please set ENCRYPTION_SECRET environment variable."); + } + + // Use a static salt derived from the secret for consistent key generation + // This ensures the same key is generated across application restarts + const salt = crypto.createHash('sha256').update('gitea-mirror-salt' + secret).digest(); + + return crypto.pbkdf2Sync(secret, salt, ITERATIONS, KEY_LENGTH, 'sha256'); +} + +export interface EncryptedData { + encrypted: string; + iv: string; + salt: string; + tag: string; + version: number; +} + +/** + * Encrypts sensitive data like API tokens + * @param plaintext The data to encrypt + * @returns Encrypted data with metadata + */ +export function encrypt(plaintext: string): string { + if (!plaintext) { + return ''; + } + + try { + const key = getEncryptionKey(); + const iv = crypto.randomBytes(IV_LENGTH); + const salt = crypto.randomBytes(SALT_LENGTH); + + const cipher = crypto.createCipheriv(ALGORITHM, key, iv); + + const encrypted = Buffer.concat([ + cipher.update(plaintext, 'utf8'), + cipher.final() + ]); + + const tag = cipher.getAuthTag(); + + const encryptedData: EncryptedData = { + encrypted: encrypted.toString('base64'), + iv: iv.toString('base64'), + salt: salt.toString('base64'), + tag: tag.toString('base64'), + version: 1 + }; + + // Return as base64 encoded JSON for easy storage + return Buffer.from(JSON.stringify(encryptedData)).toString('base64'); + } catch (error) { + console.error('Encryption error:', error); + throw new Error('Failed to encrypt data'); + } +} + +/** + * Decrypts encrypted data + * @param encryptedString The encrypted data string + * @returns Decrypted plaintext + */ +export function decrypt(encryptedString: string): string { + if (!encryptedString) { + return ''; + } + + try { + // Check if it's already plaintext (for backward compatibility during migration) + if (!isEncrypted(encryptedString)) { + return encryptedString; + } + + const encryptedData: EncryptedData = JSON.parse( + Buffer.from(encryptedString, 'base64').toString('utf8') + ); + + const key = getEncryptionKey(); + const iv = Buffer.from(encryptedData.iv, 'base64'); + const tag = Buffer.from(encryptedData.tag, 'base64'); + const encrypted = Buffer.from(encryptedData.encrypted, 'base64'); + + const decipher = crypto.createDecipheriv(ALGORITHM, key, iv); + decipher.setAuthTag(tag); + + const decrypted = Buffer.concat([ + decipher.update(encrypted), + decipher.final() + ]); + + return decrypted.toString('utf8'); + } catch (error) { + // If decryption fails, check if it's plaintext (backward compatibility) + try { + JSON.parse(Buffer.from(encryptedString, 'base64').toString('utf8')); + throw error; // It was encrypted but failed to decrypt + } catch { + // Not encrypted, return as-is for backward compatibility + console.warn('Token appears to be unencrypted, returning as-is for backward compatibility'); + return encryptedString; + } + } +} + +/** + * Checks if a string is encrypted + * @param value The string to check + * @returns true if encrypted, false otherwise + */ +export function isEncrypted(value: string): boolean { + if (!value) { + return false; + } + + try { + const decoded = Buffer.from(value, 'base64').toString('utf8'); + const data = JSON.parse(decoded); + return data.version === 1 && data.encrypted && data.iv && data.tag; + } catch { + return false; + } +} + +/** + * Migrates unencrypted tokens to encrypted format + * @param token The token to migrate + * @returns Encrypted token if it wasn't already encrypted + */ +export function migrateToken(token: string): string { + if (!token || isEncrypted(token)) { + return token; + } + + return encrypt(token); +} + +/** + * Generates a secure random token + * @param length Token length in bytes (default: 32) + * @returns Hex encoded random token + */ +export function generateSecureToken(length: number = 32): string { + return crypto.randomBytes(length).toString('hex'); +} + +/** + * Hashes a value using SHA-256 (for non-reversible values like API keys for comparison) + * @param value The value to hash + * @returns Hex encoded hash + */ +export function hashValue(value: string): string { + return crypto.createHash('sha256').update(value).digest('hex'); +} \ No newline at end of file diff --git a/src/lib/utils/oauth-validation.test.ts b/src/lib/utils/oauth-validation.test.ts new file mode 100644 index 0000000..1580ef7 --- /dev/null +++ b/src/lib/utils/oauth-validation.test.ts @@ -0,0 +1,85 @@ +import { describe, test, expect } from "bun:test"; +import { isValidRedirectUri, parseRedirectUris } from "./oauth-validation"; + +describe("OAuth Validation", () => { + describe("parseRedirectUris", () => { + test("parses comma-separated URIs", () => { + const result = parseRedirectUris("https://app1.com,https://app2.com, https://app3.com "); + expect(result).toEqual([ + "https://app1.com", + "https://app2.com", + "https://app3.com" + ]); + }); + + test("handles empty string", () => { + expect(parseRedirectUris("")).toEqual([]); + }); + + test("filters out empty values", () => { + const result = parseRedirectUris("https://app1.com,,https://app2.com,"); + expect(result).toEqual(["https://app1.com", "https://app2.com"]); + }); + }); + + describe("isValidRedirectUri", () => { + test("validates exact match", () => { + const authorizedUris = ["https://app.example.com/callback"]; + + expect(isValidRedirectUri("https://app.example.com/callback", authorizedUris)).toBe(true); + expect(isValidRedirectUri("https://app.example.com/other", authorizedUris)).toBe(false); + }); + + test("validates wildcard paths", () => { + const authorizedUris = ["https://app.example.com/*"]; + + expect(isValidRedirectUri("https://app.example.com/", authorizedUris)).toBe(true); + expect(isValidRedirectUri("https://app.example.com/callback", authorizedUris)).toBe(true); + expect(isValidRedirectUri("https://app.example.com/deep/path", authorizedUris)).toBe(true); + + // Different domain should fail + expect(isValidRedirectUri("https://evil.com/callback", authorizedUris)).toBe(false); + }); + + test("validates protocol", () => { + const authorizedUris = ["https://app.example.com/callback"]; + + // HTTP instead of HTTPS should fail + expect(isValidRedirectUri("http://app.example.com/callback", authorizedUris)).toBe(false); + }); + + test("validates host and port", () => { + const authorizedUris = ["https://app.example.com:3000/callback"]; + + // Different port should fail + expect(isValidRedirectUri("https://app.example.com/callback", authorizedUris)).toBe(false); + expect(isValidRedirectUri("https://app.example.com:3000/callback", authorizedUris)).toBe(true); + expect(isValidRedirectUri("https://app.example.com:4000/callback", authorizedUris)).toBe(false); + }); + + test("handles invalid URIs", () => { + const authorizedUris = ["not-a-valid-uri", "https://valid.com"]; + + // Invalid redirect URI + expect(isValidRedirectUri("not-a-valid-uri", authorizedUris)).toBe(false); + + // Valid redirect URI with invalid authorized URI should still work if it matches valid one + expect(isValidRedirectUri("https://valid.com", authorizedUris)).toBe(true); + }); + + test("handles empty inputs", () => { + expect(isValidRedirectUri("", ["https://app.com"])).toBe(false); + expect(isValidRedirectUri("https://app.com", [])).toBe(false); + }); + + test("prevents open redirect attacks", () => { + const authorizedUris = ["https://app.example.com/callback"]; + + // Various attack vectors + expect(isValidRedirectUri("https://app.example.com.evil.com/callback", authorizedUris)).toBe(false); + expect(isValidRedirectUri("https://app.example.com@evil.com/callback", authorizedUris)).toBe(false); + expect(isValidRedirectUri("//evil.com/callback", authorizedUris)).toBe(false); + expect(isValidRedirectUri("https:evil.com/callback", authorizedUris)).toBe(false); + }); + }); +}); \ No newline at end of file diff --git a/src/lib/utils/oauth-validation.ts b/src/lib/utils/oauth-validation.ts new file mode 100644 index 0000000..3a64b55 --- /dev/null +++ b/src/lib/utils/oauth-validation.ts @@ -0,0 +1,59 @@ +/** + * Validates a redirect URI against a list of authorized URIs + * @param redirectUri The redirect URI to validate + * @param authorizedUris List of authorized redirect URIs + * @returns true if the redirect URI is authorized, false otherwise + */ +export function isValidRedirectUri(redirectUri: string, authorizedUris: string[]): boolean { + if (!redirectUri || authorizedUris.length === 0) { + return false; + } + + try { + // Parse the redirect URI to ensure it's valid + const redirectUrl = new URL(redirectUri); + + return authorizedUris.some(authorizedUri => { + try { + // Handle wildcard paths (e.g., https://example.com/*) + if (authorizedUri.endsWith('/*')) { + const baseUri = authorizedUri.slice(0, -2); + const baseUrl = new URL(baseUri); + + // Check protocol, host, and port match + return redirectUrl.protocol === baseUrl.protocol && + redirectUrl.host === baseUrl.host && + redirectUrl.pathname.startsWith(baseUrl.pathname); + } + + // Handle exact match + const authorizedUrl = new URL(authorizedUri); + + // For exact match, everything must match including path and query params + return redirectUrl.href === authorizedUrl.href; + } catch { + // If authorized URI is not a valid URL, treat as invalid + return false; + } + }); + } catch { + // If redirect URI is not a valid URL, it's invalid + return false; + } +} + +/** + * Parses a comma-separated list of redirect URIs and trims whitespace + * @param redirectUrls Comma-separated list of redirect URIs + * @returns Array of trimmed redirect URIs + */ +export function parseRedirectUris(redirectUrls: string): string[] { + if (!redirectUrls) { + return []; + } + + return redirectUrls + .split(',') + .map(uri => uri.trim()) + .filter(uri => uri.length > 0); +} \ No newline at end of file diff --git a/src/middleware.ts b/src/middleware.ts index 7fa984c..d02dbca 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -3,6 +3,8 @@ import { initializeRecovery, hasJobsNeedingRecovery, getRecoveryStatus } from '. import { startCleanupService, stopCleanupService } from './lib/cleanup-service'; import { initializeShutdownManager, registerShutdownCallback } from './lib/shutdown-manager'; import { setupSignalHandlers } from './lib/signal-handlers'; +import { auth } from './lib/auth'; +import { isHeaderAuthEnabled, authenticateWithHeaders } from './lib/auth-header'; // Flag to track if recovery has been initialized let recoveryInitialized = false; @@ -11,6 +13,52 @@ let cleanupServiceStarted = false; let shutdownManagerInitialized = false; export const onRequest = defineMiddleware(async (context, next) => { + // First, try Better Auth session (cookie-based) + try { + const session = await auth.api.getSession({ + headers: context.request.headers, + }); + + if (session) { + context.locals.user = session.user; + context.locals.session = session.session; + } else { + // No cookie session, check for header authentication + if (isHeaderAuthEnabled()) { + const headerUser = await authenticateWithHeaders(context.request.headers); + if (headerUser) { + // Create a session-like object for header auth + context.locals.user = { + id: headerUser.id, + email: headerUser.email, + emailVerified: headerUser.emailVerified, + name: headerUser.name || headerUser.username, + username: headerUser.username, + createdAt: headerUser.createdAt, + updatedAt: headerUser.updatedAt, + }; + context.locals.session = { + id: `header-${headerUser.id}`, + userId: headerUser.id, + expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000), // 1 day + ipAddress: context.request.headers.get('x-forwarded-for') || context.clientAddress, + userAgent: context.request.headers.get('user-agent'), + }; + } else { + context.locals.user = null; + context.locals.session = null; + } + } else { + context.locals.user = null; + context.locals.session = null; + } + } + } catch (error) { + // If there's an error getting the session, set to null + context.locals.user = null; + context.locals.session = null; + } + // Initialize shutdown manager and signal handlers first if (!shutdownManagerInitialized) { try { diff --git a/src/pages/api/auth/[...all].ts b/src/pages/api/auth/[...all].ts new file mode 100644 index 0000000..d4077f4 --- /dev/null +++ b/src/pages/api/auth/[...all].ts @@ -0,0 +1,10 @@ +import { auth } from "@/lib/auth"; +import type { APIRoute } from "astro"; + +export const ALL: APIRoute = async (ctx) => { + // If you want to use rate limiting, make sure to set the 'x-forwarded-for' header + // to the request headers from the context + // ctx.request.headers.set("x-forwarded-for", ctx.clientAddress); + + return auth.handler(ctx.request); +}; \ No newline at end of file diff --git a/src/pages/api/auth/check-users.ts b/src/pages/api/auth/check-users.ts new file mode 100644 index 0000000..f726cdb --- /dev/null +++ b/src/pages/api/auth/check-users.ts @@ -0,0 +1,30 @@ +import type { APIRoute } from "astro"; +import { db, users } from "@/lib/db"; +import { sql } from "drizzle-orm"; + +export const GET: APIRoute = async () => { + try { + const userCountResult = await db + .select({ count: sql`count(*)` }) + .from(users); + + const userCount = userCountResult[0].count; + + if (userCount === 0) { + return new Response(JSON.stringify({ error: "No users found" }), { + status: 404, + headers: { "Content-Type": "application/json" }, + }); + } + + return new Response(JSON.stringify({ userCount }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + } catch (error) { + return new Response(JSON.stringify({ error: "Internal server error" }), { + status: 500, + headers: { "Content-Type": "application/json" }, + }); + } +}; \ No newline at end of file diff --git a/src/pages/api/auth/debug.ts b/src/pages/api/auth/debug.ts new file mode 100644 index 0000000..3267bf2 --- /dev/null +++ b/src/pages/api/auth/debug.ts @@ -0,0 +1,79 @@ +import type { APIRoute } from "astro"; +import { auth } from "@/lib/auth"; +import { db } from "@/lib/db"; +import { users } from "@/lib/db/schema"; +import { nanoid } from "nanoid"; + +export const GET: APIRoute = async ({ request }) => { + try { + // Get Better Auth configuration info + const info = { + baseURL: auth.options.baseURL, + basePath: auth.options.basePath, + trustedOrigins: auth.options.trustedOrigins, + emailPasswordEnabled: auth.options.emailAndPassword?.enabled, + userFields: auth.options.user?.additionalFields, + databaseConfig: { + usePlural: true, + provider: "sqlite" + } + }; + + return new Response(JSON.stringify({ + success: true, + config: info + }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + } catch (error) { + // Log full error details server-side for debugging + console.error("Debug endpoint error:", error); + + // Only return safe error information to the client + return new Response(JSON.stringify({ + success: false, + error: error instanceof Error ? error.message : "An unexpected error occurred" + }), { + status: 500, + headers: { "Content-Type": "application/json" }, + }); + } +}; + +export const POST: APIRoute = async ({ request }) => { + try { + // Test creating a user directly + const userId = nanoid(); + const now = new Date(); + + await db.insert(users).values({ + id: userId, + email: "test2@example.com", + emailVerified: false, + username: "test2", + // Let the database handle timestamps with defaults + }); + + return new Response(JSON.stringify({ + success: true, + userId, + message: "User created successfully" + }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + } catch (error) { + // Log full error details server-side for debugging + console.error("Debug endpoint error:", error); + + // Only return safe error information to the client + return new Response(JSON.stringify({ + success: false, + error: error instanceof Error ? error.message : "An unexpected error occurred" + }), { + status: 500, + headers: { "Content-Type": "application/json" }, + }); + } +}; \ No newline at end of file diff --git a/src/pages/api/auth/header-status.ts b/src/pages/api/auth/header-status.ts new file mode 100644 index 0000000..661eb96 --- /dev/null +++ b/src/pages/api/auth/header-status.ts @@ -0,0 +1,16 @@ +import type { APIRoute } from "astro"; +import { getHeaderAuthConfig } from "@/lib/auth-header"; + +export const GET: APIRoute = async () => { + const config = getHeaderAuthConfig(); + + return new Response(JSON.stringify({ + enabled: config.enabled, + userHeader: config.userHeader, + autoProvision: config.autoProvision, + hasAllowedDomains: config.allowedDomains && config.allowedDomains.length > 0, + }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); +}; \ No newline at end of file diff --git a/src/pages/api/auth/legacy-backup/README.md b/src/pages/api/auth/legacy-backup/README.md new file mode 100644 index 0000000..f0bdf3a --- /dev/null +++ b/src/pages/api/auth/legacy-backup/README.md @@ -0,0 +1,13 @@ +# Legacy Auth Routes Backup + +These files are the original authentication routes before migrating to Better Auth. +They are kept here as a reference during the migration process. + +## Migration Notes + +- `index.ts` - Handled user session validation and getting current user +- `login.ts` - Handled user login with email/password +- `logout.ts` - Handled user logout and session cleanup +- `register.ts` - Handled new user registration + +All these endpoints are now handled by Better Auth through the catch-all route `[...all].ts`. \ No newline at end of file diff --git a/src/pages/api/auth/index.ts b/src/pages/api/auth/legacy-backup/index.ts similarity index 89% rename from src/pages/api/auth/index.ts rename to src/pages/api/auth/legacy-backup/index.ts index 1c2f936..8eeb62b 100644 --- a/src/pages/api/auth/index.ts +++ b/src/pages/api/auth/legacy-backup/index.ts @@ -1,6 +1,6 @@ import type { APIRoute } from "astro"; -import { db, users, configs, client } from "@/lib/db"; -import { eq, and } from "drizzle-orm"; +import { db, users, configs } from "@/lib/db"; +import { eq, and, sql } from "drizzle-orm"; import jwt from "jsonwebtoken"; const JWT_SECRET = process.env.JWT_SECRET || "your-secret-key"; @@ -10,10 +10,10 @@ export const GET: APIRoute = async ({ request, cookies }) => { const token = authHeader?.split(" ")[1] || cookies.get("token")?.value; if (!token) { - const userCountResult = await client.execute( - `SELECT COUNT(*) as count FROM users` - ); - const userCount = userCountResult.rows[0].count; + const userCountResult = await db + .select({ count: sql`count(*)` }) + .from(users); + const userCount = userCountResult[0].count; if (userCount === 0) { return new Response(JSON.stringify({ error: "No users found" }), { diff --git a/src/pages/api/auth/login.ts b/src/pages/api/auth/legacy-backup/login.ts similarity index 100% rename from src/pages/api/auth/login.ts rename to src/pages/api/auth/legacy-backup/login.ts diff --git a/src/pages/api/auth/logout.ts b/src/pages/api/auth/legacy-backup/logout.ts similarity index 100% rename from src/pages/api/auth/logout.ts rename to src/pages/api/auth/legacy-backup/logout.ts diff --git a/src/pages/api/auth/register.ts b/src/pages/api/auth/legacy-backup/register.ts similarity index 100% rename from src/pages/api/auth/register.ts rename to src/pages/api/auth/legacy-backup/register.ts diff --git a/src/pages/api/config/index.ts b/src/pages/api/config/index.ts index a7e7590..0465f43 100644 --- a/src/pages/api/config/index.ts +++ b/src/pages/api/config/index.ts @@ -5,6 +5,7 @@ import { eq } from "drizzle-orm"; import { calculateCleanupInterval } from "@/lib/cleanup-service"; import { createSecureErrorResponse } from "@/lib/utils"; import { mapUiToDbConfig, mapDbToUiConfig } from "@/lib/utils/config-mapper"; +import { encrypt, decrypt, migrateToken } from "@/lib/utils/encryption"; export const POST: APIRoute = async ({ request }) => { try { @@ -55,17 +56,27 @@ export const POST: APIRoute = async ({ request }) => { ? JSON.parse(existingConfig.giteaConfig) : existingConfig.giteaConfig; + // Decrypt existing tokens before preserving if (!mappedGithubConfig.token && existingGithub.token) { - mappedGithubConfig.token = existingGithub.token; + mappedGithubConfig.token = decrypt(existingGithub.token); } if (!mappedGiteaConfig.token && existingGitea.token) { - mappedGiteaConfig.token = existingGitea.token; + mappedGiteaConfig.token = decrypt(existingGitea.token); } } catch (tokenError) { console.error("Failed to preserve tokens:", tokenError); } } + + // Encrypt tokens before saving + if (mappedGithubConfig.token) { + mappedGithubConfig.token = encrypt(mappedGithubConfig.token); + } + + if (mappedGiteaConfig.token) { + mappedGiteaConfig.token = encrypt(mappedGiteaConfig.token); + } // Process schedule config - set/update nextRun if enabled, clear if disabled const processedScheduleConfig = { ...scheduleConfig }; @@ -279,15 +290,54 @@ export const GET: APIRoute = async ({ request }) => { // Map database structure to UI structure const dbConfig = config[0]; - const uiConfig = mapDbToUiConfig(dbConfig); - return new Response(JSON.stringify({ - ...dbConfig, - ...uiConfig, - }), { - status: 200, - headers: { "Content-Type": "application/json" }, - }); + // Decrypt tokens before sending to UI + try { + const githubConfig = typeof dbConfig.githubConfig === "string" + ? JSON.parse(dbConfig.githubConfig) + : dbConfig.githubConfig; + + const giteaConfig = typeof dbConfig.giteaConfig === "string" + ? JSON.parse(dbConfig.giteaConfig) + : dbConfig.giteaConfig; + + // Decrypt tokens + if (githubConfig.token) { + githubConfig.token = decrypt(githubConfig.token); + } + + if (giteaConfig.token) { + giteaConfig.token = decrypt(giteaConfig.token); + } + + // Create modified config with decrypted tokens + const decryptedConfig = { + ...dbConfig, + githubConfig, + giteaConfig + }; + + const uiConfig = mapDbToUiConfig(decryptedConfig); + + return new Response(JSON.stringify({ + ...dbConfig, + ...uiConfig, + }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + } catch (error) { + console.error("Failed to decrypt tokens:", error); + // Return config without decrypting tokens if there's an error + const uiConfig = mapDbToUiConfig(dbConfig); + return new Response(JSON.stringify({ + ...dbConfig, + ...uiConfig, + }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + } } catch (error) { return createSecureErrorResponse(error, "config fetch", 500); } diff --git a/src/pages/api/job/mirror-org.ts b/src/pages/api/job/mirror-org.ts index dc40095..d9328aa 100644 --- a/src/pages/api/job/mirror-org.ts +++ b/src/pages/api/job/mirror-org.ts @@ -9,6 +9,7 @@ import { type MembershipRole } from "@/types/organizations"; import { createSecureErrorResponse } from "@/lib/utils"; import { processWithResilience } from "@/lib/utils/concurrency"; import { v4 as uuidv4 } from "uuid"; +import { getDecryptedGitHubToken } from "@/lib/utils/config-encryption"; export const POST: APIRoute = async ({ request }) => { try { @@ -71,7 +72,8 @@ export const POST: APIRoute = async ({ request }) => { } // Create a single Octokit instance to be reused - const octokit = createGitHubClient(config.githubConfig.token); + const decryptedToken = getDecryptedGitHubToken(config); + const octokit = createGitHubClient(decryptedToken); // Define the concurrency limit - adjust based on API rate limits // Using a lower concurrency for organizations since each org might contain many repos diff --git a/src/pages/api/job/mirror-repo.ts b/src/pages/api/job/mirror-repo.ts index 4e6acea..60bd3af 100644 --- a/src/pages/api/job/mirror-repo.ts +++ b/src/pages/api/job/mirror-repo.ts @@ -9,6 +9,7 @@ import { getGiteaRepoOwnerAsync, } from "@/lib/gitea"; import { createGitHubClient } from "@/lib/github"; +import { getDecryptedGitHubToken } from "@/lib/utils/config-encryption"; import { processWithResilience } from "@/lib/utils/concurrency"; import { createSecureErrorResponse } from "@/lib/utils"; @@ -73,7 +74,8 @@ export const POST: APIRoute = async ({ request }) => { } // Create a single Octokit instance to be reused - const octokit = createGitHubClient(config.githubConfig.token); + const decryptedToken = getDecryptedGitHubToken(config); + const octokit = createGitHubClient(decryptedToken); // Define the concurrency limit - adjust based on API rate limits const CONCURRENCY_LIMIT = 3; diff --git a/src/pages/api/job/retry-repo.ts b/src/pages/api/job/retry-repo.ts index f283629..560295c 100644 --- a/src/pages/api/job/retry-repo.ts +++ b/src/pages/api/job/retry-repo.ts @@ -13,6 +13,7 @@ import type { RetryRepoRequest, RetryRepoResponse } from "@/types/retry"; import { processWithRetry } from "@/lib/utils/concurrency"; import { createMirrorJob } from "@/lib/helpers"; import { createSecureErrorResponse } from "@/lib/utils"; +import { getDecryptedGitHubToken } from "@/lib/utils/config-encryption"; export const POST: APIRoute = async ({ request }) => { try { @@ -71,8 +72,11 @@ export const POST: APIRoute = async ({ request }) => { // Start background retry with parallel processing setTimeout(async () => { // Create a single Octokit instance to be reused if needed - const octokit = config.githubConfig.token - ? createGitHubClient(config.githubConfig.token) + const decryptedToken = config.githubConfig.token + ? getDecryptedGitHubToken(config) + : null; + const octokit = decryptedToken + ? createGitHubClient(decryptedToken) : null; // Define the concurrency limit - adjust based on API rate limits diff --git a/src/pages/api/organizations/[id].ts b/src/pages/api/organizations/[id].ts index 9a3c888..152ccac 100644 --- a/src/pages/api/organizations/[id].ts +++ b/src/pages/api/organizations/[id].ts @@ -2,36 +2,17 @@ import type { APIRoute } from "astro"; import { db, organizations } from "@/lib/db"; import { eq, and } from "drizzle-orm"; import { createSecureErrorResponse } from "@/lib/utils"; -import jwt from "jsonwebtoken"; +import { requireAuth } from "@/lib/utils/auth-helpers"; -const JWT_SECRET = process.env.JWT_SECRET || "your-secret-key"; - -export const PATCH: APIRoute = async ({ request, params, cookies }) => { +export const PATCH: APIRoute = async (context) => { try { - // Get token from Authorization header or cookies - const authHeader = request.headers.get("Authorization"); - const token = authHeader?.split(" ")[1] || cookies.get("token")?.value; + // Check authentication + const { user, response } = await requireAuth(context); + if (response) return response; - if (!token) { - return new Response(JSON.stringify({ error: "Unauthorized" }), { - status: 401, - headers: { "Content-Type": "application/json" }, - }); - } + const userId = user!.id; - // Verify token and get user ID - let userId: string; - try { - const decoded = jwt.verify(token, JWT_SECRET) as { id: string }; - userId = decoded.id; - } catch (error) { - return new Response(JSON.stringify({ error: "Invalid token" }), { - status: 401, - headers: { "Content-Type": "application/json" }, - }); - } - - const orgId = params.id; + const orgId = context.params.id; if (!orgId) { return new Response(JSON.stringify({ error: "Organization ID is required" }), { status: 400, @@ -39,7 +20,7 @@ export const PATCH: APIRoute = async ({ request, params, cookies }) => { }); } - const body = await request.json(); + const body = await context.request.json(); const { destinationOrg } = body; // Validate that the organization belongs to the user diff --git a/src/pages/api/repositories/[id].ts b/src/pages/api/repositories/[id].ts index b79bcce..debbc07 100644 --- a/src/pages/api/repositories/[id].ts +++ b/src/pages/api/repositories/[id].ts @@ -2,36 +2,17 @@ import type { APIRoute } from "astro"; import { db, repositories } from "@/lib/db"; import { eq, and } from "drizzle-orm"; import { createSecureErrorResponse } from "@/lib/utils"; -import jwt from "jsonwebtoken"; +import { requireAuth } from "@/lib/utils/auth-helpers"; -const JWT_SECRET = process.env.JWT_SECRET || "your-secret-key"; - -export const PATCH: APIRoute = async ({ request, params, cookies }) => { +export const PATCH: APIRoute = async (context) => { try { - // Get token from Authorization header or cookies - const authHeader = request.headers.get("Authorization"); - const token = authHeader?.split(" ")[1] || cookies.get("token")?.value; + // Check authentication + const { user, response } = await requireAuth(context); + if (response) return response; - if (!token) { - return new Response(JSON.stringify({ error: "Unauthorized" }), { - status: 401, - headers: { "Content-Type": "application/json" }, - }); - } + const userId = user!.id; - // Verify token and get user ID - let userId: string; - try { - const decoded = jwt.verify(token, JWT_SECRET) as { id: string }; - userId = decoded.id; - } catch (error) { - return new Response(JSON.stringify({ error: "Invalid token" }), { - status: 401, - headers: { "Content-Type": "application/json" }, - }); - } - - const repoId = params.id; + const repoId = context.params.id; if (!repoId) { return new Response(JSON.stringify({ error: "Repository ID is required" }), { status: 400, @@ -39,7 +20,7 @@ export const PATCH: APIRoute = async ({ request, params, cookies }) => { }); } - const body = await request.json(); + const body = await context.request.json(); const { destinationOrg } = body; // Validate that the repository belongs to the user diff --git a/src/pages/api/sso/applications.ts b/src/pages/api/sso/applications.ts new file mode 100644 index 0000000..ef4ee93 --- /dev/null +++ b/src/pages/api/sso/applications.ts @@ -0,0 +1,176 @@ +import type { APIContext } from "astro"; +import { createSecureErrorResponse } from "@/lib/utils"; +import { requireAuth } from "@/lib/utils/auth-helpers"; +import { db, oauthApplications } from "@/lib/db"; +import { nanoid } from "nanoid"; +import { eq } from "drizzle-orm"; +import { generateRandomString } from "@/lib/utils"; + +// GET /api/sso/applications - List all OAuth applications +export async function GET(context: APIContext) { + try { + const { user, response } = await requireAuth(context); + if (response) return response; + + const applications = await db.select().from(oauthApplications); + + // Don't send client secrets in list response + const sanitizedApps = applications.map(app => ({ + ...app, + clientSecret: undefined, + })); + + return new Response(JSON.stringify(sanitizedApps), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + } catch (error) { + return createSecureErrorResponse(error, "SSO applications API"); + } +} + +// POST /api/sso/applications - Create a new OAuth application +export async function POST(context: APIContext) { + try { + const { user, response } = await requireAuth(context); + if (response) return response; + + const body = await context.request.json(); + const { name, redirectURLs, type = "web", metadata } = body; + + // Validate required fields + if (!name || !redirectURLs || redirectURLs.length === 0) { + return new Response( + JSON.stringify({ error: "Name and at least one redirect URL are required" }), + { + status: 400, + headers: { "Content-Type": "application/json" }, + } + ); + } + + // Generate client credentials + const clientId = `client_${generateRandomString(32)}`; + const clientSecret = `secret_${generateRandomString(48)}`; + + // Insert new application + const [newApp] = await db + .insert(oauthApplications) + .values({ + id: nanoid(), + clientId, + clientSecret, + name, + redirectURLs: Array.isArray(redirectURLs) ? redirectURLs.join(",") : redirectURLs, + type, + metadata: metadata ? JSON.stringify(metadata) : null, + userId: user.id, + disabled: false, + }) + .returning(); + + return new Response(JSON.stringify(newApp), { + status: 201, + headers: { "Content-Type": "application/json" }, + }); + } catch (error) { + return createSecureErrorResponse(error, "SSO applications API"); + } +} + +// PUT /api/sso/applications/:id - Update an OAuth application +export async function PUT(context: APIContext) { + try { + const { user, response } = await requireAuth(context); + if (response) return response; + + const url = new URL(context.request.url); + const appId = url.pathname.split("/").pop(); + + if (!appId) { + return new Response( + JSON.stringify({ error: "Application ID is required" }), + { + status: 400, + headers: { "Content-Type": "application/json" }, + } + ); + } + + const body = await context.request.json(); + const { name, redirectURLs, disabled, metadata } = body; + + const updateData: any = {}; + if (name !== undefined) updateData.name = name; + if (redirectURLs !== undefined) { + updateData.redirectURLs = Array.isArray(redirectURLs) + ? redirectURLs.join(",") + : redirectURLs; + } + if (disabled !== undefined) updateData.disabled = disabled; + if (metadata !== undefined) updateData.metadata = JSON.stringify(metadata); + + const [updated] = await db + .update(oauthApplications) + .set({ + ...updateData, + updatedAt: new Date(), + }) + .where(eq(oauthApplications.id, appId)) + .returning(); + + if (!updated) { + return new Response(JSON.stringify({ error: "Application not found" }), { + status: 404, + headers: { "Content-Type": "application/json" }, + }); + } + + return new Response(JSON.stringify({ ...updated, clientSecret: undefined }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + } catch (error) { + return createSecureErrorResponse(error, "SSO applications API"); + } +} + +// DELETE /api/sso/applications/:id - Delete an OAuth application +export async function DELETE(context: APIContext) { + try { + const { user, response } = await requireAuth(context); + if (response) return response; + + const url = new URL(context.request.url); + const appId = url.searchParams.get("id"); + + if (!appId) { + return new Response( + JSON.stringify({ error: "Application ID is required" }), + { + status: 400, + headers: { "Content-Type": "application/json" }, + } + ); + } + + const deleted = await db + .delete(oauthApplications) + .where(eq(oauthApplications.id, appId)) + .returning(); + + if (deleted.length === 0) { + return new Response(JSON.stringify({ error: "Application not found" }), { + status: 404, + headers: { "Content-Type": "application/json" }, + }); + } + + return new Response(JSON.stringify({ success: true }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + } catch (error) { + return createSecureErrorResponse(error, "SSO applications API"); + } +} \ No newline at end of file diff --git a/src/pages/api/sso/discover.ts b/src/pages/api/sso/discover.ts new file mode 100644 index 0000000..acbf94d --- /dev/null +++ b/src/pages/api/sso/discover.ts @@ -0,0 +1,69 @@ +import type { APIContext } from "astro"; +import { createSecureErrorResponse } from "@/lib/utils"; +import { requireAuth } from "@/lib/utils/auth-helpers"; + +// POST /api/sso/discover - Discover OIDC configuration from issuer URL +export async function POST(context: APIContext) { + try { + const { user, response } = await requireAuth(context); + if (response) return response; + + const { issuer } = await context.request.json(); + + if (!issuer) { + return new Response(JSON.stringify({ error: "Issuer URL is required" }), { + status: 400, + headers: { "Content-Type": "application/json" }, + }); + } + + // Ensure issuer URL ends without trailing slash for well-known discovery + const cleanIssuer = issuer.replace(/\/$/, ""); + const discoveryUrl = `${cleanIssuer}/.well-known/openid-configuration`; + + try { + // Fetch OIDC discovery document + const response = await fetch(discoveryUrl); + + if (!response.ok) { + throw new Error(`Failed to fetch discovery document: ${response.status}`); + } + + const config = await response.json(); + + // Extract the essential endpoints + const discoveredConfig = { + issuer: config.issuer || cleanIssuer, + authorizationEndpoint: config.authorization_endpoint, + tokenEndpoint: config.token_endpoint, + userInfoEndpoint: config.userinfo_endpoint, + jwksEndpoint: config.jwks_uri, + // Additional useful fields + scopes: config.scopes_supported || ["openid", "profile", "email"], + responseTypes: config.response_types_supported || ["code"], + grantTypes: config.grant_types_supported || ["authorization_code"], + // Suggested domain from issuer + suggestedDomain: new URL(cleanIssuer).hostname.replace("www.", ""), + }; + + return new Response(JSON.stringify(discoveredConfig), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + } catch (error) { + console.error("OIDC discovery error:", error); + return new Response( + JSON.stringify({ + error: "Failed to discover OIDC configuration", + details: error instanceof Error ? error.message : "Unknown error" + }), + { + status: 400, + headers: { "Content-Type": "application/json" }, + } + ); + } + } catch (error) { + return createSecureErrorResponse(error, "SSO discover API"); + } +} \ No newline at end of file diff --git a/src/pages/api/sso/providers.ts b/src/pages/api/sso/providers.ts new file mode 100644 index 0000000..9c5d523 --- /dev/null +++ b/src/pages/api/sso/providers.ts @@ -0,0 +1,152 @@ +import type { APIContext } from "astro"; +import { createSecureErrorResponse } from "@/lib/utils"; +import { requireAuth } from "@/lib/utils/auth-helpers"; +import { db, ssoProviders } from "@/lib/db"; +import { nanoid } from "nanoid"; +import { eq } from "drizzle-orm"; + +// GET /api/sso/providers - List all SSO providers +export async function GET(context: APIContext) { + try { + const { user, response } = await requireAuth(context); + if (response) return response; + + const providers = await db.select().from(ssoProviders); + + return new Response(JSON.stringify(providers), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + } catch (error) { + return createSecureErrorResponse(error, "SSO providers API"); + } +} + +// POST /api/sso/providers - Create a new SSO provider +export async function POST(context: APIContext) { + try { + const { user, response } = await requireAuth(context); + if (response) return response; + + const body = await context.request.json(); + const { + issuer, + domain, + clientId, + clientSecret, + authorizationEndpoint, + tokenEndpoint, + jwksEndpoint, + userInfoEndpoint, + mapping, + providerId, + organizationId, + } = body; + + // Validate required fields + if (!issuer || !domain || !providerId) { + return new Response( + JSON.stringify({ error: "Missing required fields" }), + { + status: 400, + headers: { "Content-Type": "application/json" }, + } + ); + } + + // Check if provider ID already exists + const existing = await db + .select() + .from(ssoProviders) + .where(eq(ssoProviders.providerId, providerId)) + .limit(1); + + if (existing.length > 0) { + return new Response( + JSON.stringify({ error: "Provider ID already exists" }), + { + status: 409, + headers: { "Content-Type": "application/json" }, + } + ); + } + + // Create OIDC config object + const oidcConfig = { + clientId, + clientSecret, + authorizationEndpoint, + tokenEndpoint, + jwksEndpoint, + userInfoEndpoint, + mapping: mapping || { + id: "sub", + email: "email", + emailVerified: "email_verified", + name: "name", + image: "picture", + }, + }; + + // Insert new provider + const [newProvider] = await db + .insert(ssoProviders) + .values({ + id: nanoid(), + issuer, + domain, + oidcConfig: JSON.stringify(oidcConfig), + userId: user.id, + providerId, + organizationId, + }) + .returning(); + + return new Response(JSON.stringify(newProvider), { + status: 201, + headers: { "Content-Type": "application/json" }, + }); + } catch (error) { + return createSecureErrorResponse(error, "SSO providers API"); + } +} + +// DELETE /api/sso/providers - Delete a provider by ID +export async function DELETE(context: APIContext) { + try { + const { user, response } = await requireAuth(context); + if (response) return response; + + const url = new URL(context.request.url); + const providerId = url.searchParams.get("id"); + + if (!providerId) { + return new Response( + JSON.stringify({ error: "Provider ID is required" }), + { + status: 400, + headers: { "Content-Type": "application/json" }, + } + ); + } + + const deleted = await db + .delete(ssoProviders) + .where(eq(ssoProviders.id, providerId)) + .returning(); + + if (deleted.length === 0) { + return new Response(JSON.stringify({ error: "Provider not found" }), { + status: 404, + headers: { "Content-Type": "application/json" }, + }); + } + + return new Response(JSON.stringify({ success: true }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + } catch (error) { + return createSecureErrorResponse(error, "SSO providers API"); + } +} \ No newline at end of file diff --git a/src/pages/api/sync/index.ts b/src/pages/api/sync/index.ts index dd74473..ba6e00f 100644 --- a/src/pages/api/sync/index.ts +++ b/src/pages/api/sync/index.ts @@ -10,6 +10,7 @@ import { getGithubStarredRepositories, } from "@/lib/github"; import { jsonResponse, createSecureErrorResponse } from "@/lib/utils"; +import { getDecryptedGitHubToken } from "@/lib/utils/config-encryption"; export const POST: APIRoute = async ({ request }) => { const url = new URL(request.url); @@ -33,21 +34,21 @@ export const POST: APIRoute = async ({ request }) => { }); } - const token = config.githubConfig?.token; - - if (!token) { + if (!config.githubConfig?.token) { return jsonResponse({ data: { error: "GitHub token is missing in config" }, status: 400, }); } - const octokit = createGitHubClient(token); + // Decrypt the GitHub token before using it + const decryptedToken = getDecryptedGitHubToken(config); + const octokit = createGitHubClient(decryptedToken); // Fetch GitHub data in parallel const [basicAndForkedRepos, starredRepos, gitOrgs] = await Promise.all([ getGithubRepositories({ octokit, config }), - config.githubConfig?.mirrorStarred + config.githubConfig?.includeStarred ? getGithubStarredRepositories({ octokit, config }) : Promise.resolve([]), getGithubOrganizations({ octokit, config }), diff --git a/src/pages/docs/advanced.astro b/src/pages/docs/advanced.astro new file mode 100644 index 0000000..9212221 --- /dev/null +++ b/src/pages/docs/advanced.astro @@ -0,0 +1,467 @@ +--- +import MainLayout from '../../layouts/main.astro'; +--- + + +
+ + +
+ +
+
+ + + + + Advanced +
+

Advanced Topics

+

+ Advanced configuration options, deployment strategies, troubleshooting, and performance optimization for Gitea Mirror. +

+
+ + +
+

Environment Variables

+ +

+ Gitea Mirror can be configured using environment variables. These are particularly useful for containerized deployments. +

+ +
+ + + + + + + + + + {[ + { var: 'NODE_ENV', desc: 'Application environment', default: 'production' }, + { var: 'PORT', desc: 'Server port', default: '4321' }, + { var: 'HOST', desc: 'Server host', default: '0.0.0.0' }, + { var: 'BETTER_AUTH_SECRET', desc: 'Authentication secret key', default: 'Auto-generated' }, + { var: 'BETTER_AUTH_URL', desc: 'Authentication base URL', default: 'http://localhost:4321' }, + { var: 'NODE_EXTRA_CA_CERTS', desc: 'Path to CA certificate file', default: 'None' }, + { var: 'DATABASE_URL', desc: 'SQLite database path', default: './data/gitea-mirror.db' }, + ].map((item, i) => ( + + + + + + ))} + +
VariableDescriptionDefault
{item.var}{item.desc}{item.default}
+
+
+ +
+ + +
+

Database Management

+ +

+ Gitea Mirror uses SQLite for data storage. The database is automatically created on first run. +

+ +

Database Commands

+ +
+
+

Initialize Database

+
+ bun run init-db +
+

Creates or recreates the database schema

+
+ +
+

Check Database

+
+ bun run check-db +
+

Verifies database integrity and displays statistics

+
+ +
+

Fix Database

+
+ bun run fix-db +
+

Attempts to repair common database issues

+
+ +
+

Backup Database

+
+ cp data/gitea-mirror.db data/gitea-mirror.db.backup +
+

Always backup before major changes

+
+
+ +

Database Schema Management

+ +
+
+
+ + + +
+
+

Drizzle Kit

+

Database schema is managed with Drizzle ORM. Use these commands for schema changes:

+
    +
  • bun run drizzle-kit generate - Generate migration files
  • +
  • bun run drizzle-kit push - Apply schema changes directly
  • +
  • bun run drizzle-kit studio - Open database browser
  • +
+
+
+
+
+ +
+ + +
+

Performance Optimization

+ +

Mirroring Performance

+ +
+ {[ + { + title: 'Batch Operations', + tips: [ + 'Mirror multiple repositories at once', + 'Use organization-level mirroring', + 'Schedule mirroring during off-peak hours' + ] + }, + { + title: 'Network Optimization', + tips: [ + 'Use SSH URLs when possible', + 'Enable Git LFS only when needed', + 'Consider repository size limits' + ] + } + ].map(section => ( +
+

{section.title}

+
    + {section.tips.map(tip => ( +
  • + โ€ข + {tip} +
  • + ))} +
+
+ ))} +
+ +

Database Performance

+ +
+

Regular Maintenance

+
    +
  • + โ€ข + Enable automatic cleanup in Configuration โ†’ Automation +
  • +
  • + โ€ข + Periodically vacuum the SQLite database: sqlite3 data/gitea-mirror.db "VACUUM;" +
  • +
  • + โ€ข + Monitor database size and clean old events regularly +
  • +
+
+
+ +
+ + +
+

Reverse Proxy Configuration

+ +

+ For production deployments, it's recommended to use a reverse proxy like Nginx or Caddy. +

+ +

Nginx Example

+ +
+
{`server {
+    listen 80;
+    server_name gitea-mirror.example.com;
+    return 301 https://$server_name$request_uri;
+}
+
+server {
+    listen 443 ssl http2;
+    server_name gitea-mirror.example.com;
+
+    ssl_certificate /path/to/cert.pem;
+    ssl_certificate_key /path/to/key.pem;
+
+    location / {
+        proxy_pass http://localhost:4321;
+        proxy_http_version 1.1;
+        proxy_set_header Upgrade $http_upgrade;
+        proxy_set_header Connection 'upgrade';
+        proxy_set_header Host $host;
+        proxy_cache_bypass $http_upgrade;
+        proxy_set_header X-Real-IP $remote_addr;
+        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+        proxy_set_header X-Forwarded-Proto $scheme;
+    }
+
+    # SSE endpoint needs special handling
+    location /api/sse {
+        proxy_pass http://localhost:4321;
+        proxy_http_version 1.1;
+        proxy_set_header Connection '';
+        proxy_set_header Cache-Control 'no-cache';
+        proxy_set_header X-Accel-Buffering 'no';
+        proxy_read_timeout 86400;
+    }
+}`}
+
+ +

Caddy Example

+ +
+
{`gitea-mirror.example.com {
+    reverse_proxy localhost:4321
+}`}
+
+
+ +
+ + +
+

Monitoring and Health Checks

+ +

Health Check Endpoint

+ +
+

Monitor application health using the built-in endpoint:

+ +
+ GET /api/health +
+ +

Response:

+
+
{`{
+  "status": "ok",
+  "timestamp": "2024-01-15T10:30:00Z",
+  "database": "connected",
+  "version": "1.0.0"
+}`}
+
+
+ +

Monitoring with Prometheus

+ +

+ While Gitea Mirror doesn't have built-in Prometheus metrics, you can monitor it using: +

+ +
    +
  • + โ€ข + Blackbox exporter for endpoint monitoring +
  • +
  • + โ€ข + Node exporter for system metrics +
  • +
  • + โ€ข + Custom scripts to check database metrics +
  • +
+
+ +
+ + +
+

Backup and Recovery

+ +

What to Backup

+ +
+
+

Essential Files

+
    +
  • โ€ข data/gitea-mirror.db
  • +
  • โ€ข .env (if using)
  • +
  • โ€ข Custom CA certificates
  • +
+
+ +
+

Optional Files

+
    +
  • โ€ข Docker volumes
  • +
  • โ€ข Custom configurations
  • +
  • โ€ข Logs for auditing
  • +
+
+
+ +

Backup Script Example

+ +
+
{`#!/bin/bash
+BACKUP_DIR="/backups/gitea-mirror"
+DATE=$(date +%Y%m%d_%H%M%S)
+
+# Create backup directory
+mkdir -p "$BACKUP_DIR/$DATE"
+
+# Backup database
+cp data/gitea-mirror.db "$BACKUP_DIR/$DATE/"
+
+# Backup environment
+cp .env "$BACKUP_DIR/$DATE/" 2>/dev/null || true
+
+# Create tarball
+tar -czf "$BACKUP_DIR/backup_$DATE.tar.gz" -C "$BACKUP_DIR" "$DATE"
+
+# Clean up
+rm -rf "$BACKUP_DIR/$DATE"
+
+# Keep only last 7 backups
+ls -t "$BACKUP_DIR"/backup_*.tar.gz | tail -n +8 | xargs rm -f`}
+
+
+ +
+ + +
+

Troubleshooting Guide

+ +
+ {[ + { + issue: 'Application won\'t start', + solutions: [ + 'Check port availability: `lsof -i :4321`', + 'Verify environment variables are set correctly', + 'Check database file permissions', + 'Review logs for startup errors' + ] + }, + { + issue: 'Authentication failures', + solutions: [ + 'Ensure BETTER_AUTH_SECRET is set and consistent', + 'Check BETTER_AUTH_URL matches your deployment', + 'Clear browser cookies and try again', + 'Verify database contains user records' + ] + }, + { + issue: 'Mirroring failures', + solutions: [ + 'Test GitHub/Gitea connections individually', + 'Verify access tokens have correct permissions', + 'Check network connectivity and firewall rules', + 'Review Activity Log for detailed error messages' + ] + }, + { + issue: 'Performance issues', + solutions: [ + 'Check database size and run cleanup', + 'Monitor system resources (CPU, memory, disk)', + 'Reduce concurrent mirroring operations', + 'Consider upgrading deployment resources' + ] + } + ].map(item => ( +
+

{item.issue}

+
    + {item.solutions.map(solution => ( +
  • + โ†’ + {solution} +
  • + ))} +
+
+ ))} +
+
+ +
+ + +
+

Migration Guide

+ +

Migrating from JWT to Better Auth

+ +
+

If you're upgrading from an older version using JWT authentication:

+ +
    +
  1. + 1 +
    + Backup your database +

    Always create a backup before migration

    +
    +
  2. +
  3. + 2 +
    + Update environment variables +

    Replace JWT_SECRET with BETTER_AUTH_SECRET

    +
    +
  4. +
  5. + 3 +
    + Run database migrations +

    New auth tables will be created automatically

    +
    +
  6. +
  7. + 4 +
    + Users will need to log in again +

    Previous sessions will be invalidated

    +
    +
  8. +
+
+
+
+
+
\ No newline at end of file diff --git a/src/pages/docs/architecture.astro b/src/pages/docs/architecture.astro index be28c46..27c010b 100644 --- a/src/pages/docs/architecture.astro +++ b/src/pages/docs/architecture.astro @@ -47,7 +47,8 @@ import MainLayout from '../../layouts/main.astro'; { name: 'Shadcn UI', desc: 'UI component library built on Tailwind CSS' }, { name: 'SQLite', desc: 'Database for storing configuration, state, and events' }, { name: 'Bun', desc: 'JavaScript runtime and package manager' }, - { name: 'Drizzle ORM', desc: 'Type-safe ORM for database interactions' } + { name: 'Drizzle ORM', desc: 'Type-safe ORM for database interactions' }, + { name: 'Better Auth', desc: 'Modern authentication library with SSO/OIDC support' } ].map(tech => (
@@ -184,7 +185,8 @@ import MainLayout from '../../layouts/main.astro';
{[ - 'Authentication and user management', + 'Authentication with Better Auth (email/password, SSO, OIDC)', + 'OAuth2/OIDC provider functionality', 'GitHub API integration', 'Gitea API integration', 'Mirroring operations and job queue', @@ -213,11 +215,13 @@ import MainLayout from '../../layouts/main.astro';
{[ - 'User accounts and authentication data', + 'User accounts and authentication data (Better Auth)', + 'OAuth applications and SSO provider configurations', 'GitHub and Gitea configuration', 'Repository and organization information', 'Mirroring job history and status', - 'Event notifications and their read status' + 'Event notifications and their read status', + 'OAuth tokens and consent records' ].map(item => (
โ–ธ @@ -238,7 +242,7 @@ import MainLayout from '../../layouts/main.astro';
    {[ - { title: 'User Authentication', desc: 'Users authenticate through the frontend, which communicates with the backend to validate credentials.' }, + { title: 'User Authentication', desc: 'Users authenticate via Better Auth using email/password, SSO providers, or as OIDC clients.' }, { title: 'Configuration', desc: 'Users configure GitHub and Gitea settings through the UI, which are stored in the SQLite database.' }, { title: 'Repository Discovery', desc: 'The backend queries the GitHub API to discover repositories based on user configuration.' }, { title: 'Mirroring Process', desc: 'When triggered, the backend fetches repository data from GitHub and pushes it to Gitea.' }, diff --git a/src/pages/docs/authentication.astro b/src/pages/docs/authentication.astro new file mode 100644 index 0000000..d87ef96 --- /dev/null +++ b/src/pages/docs/authentication.astro @@ -0,0 +1,535 @@ +--- +import MainLayout from '../../layouts/main.astro'; +--- + + +
    + + +
    + +
    +
    + + + + Authentication +
    +

    Authentication & SSO Configuration

    +

    + Configure authentication methods including email/password, Single Sign-On (SSO), and OIDC provider functionality for Gitea Mirror. +

    +
    + + +
    +

    Authentication Overview

    + +
    +

    + Gitea Mirror uses Better Auth, a modern authentication library that supports multiple authentication methods. + All authentication settings can be configured through the web UI without editing configuration files. +

    +
    + +

    Supported Authentication Methods

    + +
    + {[ + { + icon: 'โœ‰๏ธ', + title: 'Email & Password', + desc: 'Traditional authentication with email and password. Always enabled by default.', + status: 'Always Enabled' + }, + { + icon: '๐ŸŒ', + title: 'Single Sign-On (SSO)', + desc: 'Allow users to sign in using external OIDC providers like Google, Okta, or Azure AD.', + status: 'Optional' + }, + { + icon: '๐Ÿ”‘', + title: 'OIDC Provider', + desc: 'Act as an OIDC provider, allowing other applications to authenticate through Gitea Mirror.', + status: 'Optional' + } + ].map(method => ( +
    +
    {method.icon}
    +

    {method.title}

    +

    {method.desc}

    + + {method.status} + +
    + ))} +
    +
    + +
    + + +
    +

    Accessing Authentication Settings

    + +
      +
    1. + 1 + Navigate to the Configuration page +
    2. +
    3. + 2 + Click on the Authentication tab +
    4. +
    5. + 3 + Configure SSO providers or OAuth applications as needed +
    6. +
    +
    + +
    + + +
    +

    Single Sign-On (SSO) Configuration

    + +

    + SSO allows your users to authenticate using external identity providers. This is useful for organizations that already have centralized authentication systems. +

    + +

    Adding an SSO Provider

    + +
    +

    Required Information

    + +
    + {[ + { name: 'Issuer URL', desc: 'The OIDC issuer URL of your provider', example: 'https://accounts.google.com' }, + { name: 'Domain', desc: 'The email domain for this provider', example: 'example.com' }, + { name: 'Provider ID', desc: 'A unique identifier for this provider', example: 'google-sso' }, + { name: 'Client ID', desc: 'OAuth client ID from your provider', example: '123456789.apps.googleusercontent.com' }, + { name: 'Client Secret', desc: 'OAuth client secret from your provider', example: 'GOCSPX-...' } + ].map(field => ( +
    +
    + {field.name} + Required +
    +

    {field.desc}

    + {field.example} +
    + ))} +
    +
    + +
    +
    +
    + + + +
    +
    +

    Auto-Discovery

    +

    Most OIDC providers support auto-discovery. Simply enter the Issuer URL and click "Discover" to automatically populate the endpoint URLs.

    +
    +
    +
    + +

    Redirect URL Configuration

    + +
    +

    When configuring your SSO provider, use this redirect URL:

    + https://your-domain.com/api/auth/sso/callback/{`{provider-id}`} +

    Replace {`{provider-id}`} with your chosen Provider ID (e.g., google-sso)

    +
    +
    + +
    + + +
    +

    Example SSO Configurations

    + + +
    +

    + Google + Google SSO +

    + +
    +
      +
    1. + 1. Create OAuth Client in Google Cloud Console +
        +
      • โ€ข Go to Google Cloud Console
      • +
      • โ€ข Create a new OAuth 2.0 Client ID
      • +
      • โ€ข Add authorized redirect URI: https://your-domain.com/api/auth/sso/callback/google-sso
      • +
      +
    2. +
    3. + 2. Configure in Gitea Mirror +
      +
      +
      Issuer URL: https://accounts.google.com
      +
      Domain: your-company.com
      +
      Provider ID: google-sso
      +
      Client ID: [Your Google Client ID]
      +
      Client Secret: [Your Google Client Secret]
      +
      +
      +
    4. +
    5. + 3. Use Auto-Discovery +

      Click "Discover" to automatically populate the endpoint URLs

      +
    6. +
    +
    +
    + + +
    +

    + O + Okta SSO +

    + +
    +
      +
    1. + 1. Create OIDC Application in Okta +
        +
      • โ€ข In Okta Admin Console, create a new OIDC Web Application
      • +
      • โ€ข Set Sign-in redirect URI: https://your-domain.com/api/auth/sso/callback/okta-sso
      • +
      • โ€ข Note the Client ID and Client Secret
      • +
      +
    2. +
    3. + 2. Configure in Gitea Mirror +
      +
      +
      Issuer URL: https://your-okta-domain.okta.com
      +
      Domain: your-company.com
      +
      Provider ID: okta-sso
      +
      Client ID: [Your Okta Client ID]
      +
      Client Secret: [Your Okta Client Secret]
      +
      +
      +
    4. +
    +
    +
    + + +
    +

    + M + Azure AD / Microsoft Entra ID +

    + +
    +
      +
    1. + 1. Register Application in Azure Portal +
        +
      • โ€ข Go to Azure Portal โ†’ Azure Active Directory โ†’ App registrations
      • +
      • โ€ข Create a new registration
      • +
      • โ€ข Add redirect URI: https://your-domain.com/api/auth/sso/callback/azure-sso
      • +
      +
    2. +
    3. + 2. Configure in Gitea Mirror +
      +
      +
      Issuer URL: https://login.microsoftonline.com/{`{tenant-id}`}/v2.0
      +
      Domain: your-company.com
      +
      Provider ID: azure-sso
      +
      Client ID: [Your Application ID]
      +
      Client Secret: [Your Client Secret]
      +
      +
      +
    4. +
    +
    +
    +
    + +
    + + +
    +

    OIDC Provider Configuration

    + +

    + The OIDC Provider feature allows Gitea Mirror to act as an authentication provider for other applications. + This is useful when you want to centralize authentication through Gitea Mirror. +

    + +

    Creating OAuth Applications

    + +
    +
      +
    1. + 1 +
      + Navigate to OAuth Applications +

      Go to Configuration โ†’ Authentication โ†’ OAuth Applications

      +
      +
    2. +
    3. + 2 +
      + Create New Application +

      Click "Create Application" and provide:

      +
        +
      • โ€ข Application Name
      • +
      • โ€ข Application Type (Web, Mobile, or Desktop)
      • +
      • โ€ข Redirect URLs (one per line)
      • +
      +
      +
    4. +
    5. + 3 +
      + Save Credentials +

      You'll receive a Client ID and Client Secret. Store these securely!

      +
      +
    6. +
    +
    + +

    OIDC Endpoints

    + +
    +

    Applications can use these standard OIDC endpoints:

    +
    +
    + Discovery: + https://your-domain.com/.well-known/openid-configuration +
    +
    + Authorization: + https://your-domain.com/api/auth/oauth2/authorize +
    +
    + Token: + https://your-domain.com/api/auth/oauth2/token +
    +
    + UserInfo: + https://your-domain.com/api/auth/oauth2/userinfo +
    +
    + JWKS: + https://your-domain.com/api/auth/jwks +
    +
    +
    + +

    Supported Scopes

    + +
    + {[ + { scope: 'openid', desc: 'Required - provides user ID', claims: 'sub' }, + { scope: 'profile', desc: 'User profile information', claims: 'name, username, picture' }, + { scope: 'email', desc: 'Email address', claims: 'email, email_verified' } + ].map(item => ( +
    + {item.scope} +

    {item.desc}

    +

    Claims: {item.claims}

    +
    + ))} +
    +
    + +
    + + +
    +

    User Experience

    + +

    Login Flow with SSO

    + +
    +

    When SSO is configured, users will see authentication options on the login page:

    +
      +
    1. 1. Email & Password tab for traditional login
    2. +
    3. 2. SSO tab with provider buttons or email input
    4. +
    5. 3. Automatic redirect to the appropriate provider
    6. +
    7. 4. Return to Gitea Mirror after successful authentication
    8. +
    +
    + +

    OAuth Consent Flow

    + +
    +

    When an application requests authentication through Gitea Mirror:

    +
      +
    1. 1. User is redirected to Gitea Mirror
    2. +
    3. 2. Login prompt if not already authenticated
    4. +
    5. 3. Consent screen showing requested permissions
    6. +
    7. 4. User approves or denies the request
    8. +
    9. 5. Redirect back to the application with auth code
    10. +
    +
    +
    + +
    + + +
    +

    Security Considerations

    + +
    + {[ + { + icon: '๐Ÿ”’', + title: 'Client Secrets', + items: [ + 'Store OAuth client secrets securely', + 'Never commit secrets to version control', + 'Rotate secrets regularly' + ] + }, + { + icon: '๐Ÿ”—', + title: 'Redirect URLs', + items: [ + 'Only add trusted redirect URLs', + 'Use HTTPS in production', + 'Validate exact URL matches' + ] + }, + { + icon: '๐Ÿ›ก๏ธ', + title: 'Scopes & Permissions', + items: [ + 'Grant minimum required scopes', + 'Review requested permissions', + 'Users can revoke access anytime' + ] + }, + { + icon: 'โฑ๏ธ', + title: 'Token Security', + items: [ + 'Access tokens have expiration', + 'Refresh tokens for long-lived access', + 'Tokens can be revoked' + ] + } + ].map(section => ( +
    +
    + {section.icon} +

    {section.title}

    +
    +
      + {section.items.map(item => ( +
    • + โ€ข + {item} +
    • + ))} +
    +
    + ))} +
    +
    + +
    + + +
    +

    Troubleshooting

    + +
    +
    +

    SSO Login Issues

    +
      +
    • + โ€ข +
      + "Invalid origin" error: Check that your Gitea Mirror URL matches the configured redirect URI +
      +
    • +
    • + โ€ข +
      + "Provider not found" error: Ensure the provider is properly configured and saved +
      +
    • +
    • + โ€ข +
      + Redirect loop: Verify the redirect URI in both Gitea Mirror and the SSO provider match exactly +
      +
    • +
    +
    + +
    +

    OIDC Provider Issues

    +
      +
    • + โ€ข +
      + Application not found: Ensure the client ID is correct and the app is not disabled +
      +
    • +
    • + โ€ข +
      + Invalid redirect URI: The redirect URI must match exactly what's configured +
      +
    • +
    • + โ€ข +
      + Consent not working: Check browser cookies are enabled and not blocked +
      +
    • +
    +
    +
    +
    + +
    + + +
    +

    Migration from JWT Authentication

    + +
    +
    +
    + + + +
    +
    +

    For Existing Users

    +
      +
    • โ€ข Email/password authentication continues to work
    • +
    • โ€ข No action required from existing users
    • +
    • โ€ข SSO can be added as an additional option
    • +
    • โ€ข JWT_SECRET is no longer required in environment variables
    • +
    +
    +
    +
    +
    +
    +
    +
    \ No newline at end of file diff --git a/src/pages/docs/ca-certificates.astro b/src/pages/docs/ca-certificates.astro new file mode 100644 index 0000000..3553867 --- /dev/null +++ b/src/pages/docs/ca-certificates.astro @@ -0,0 +1,475 @@ +--- +import MainLayout from '../../layouts/main.astro'; +--- + + +
    + + +
    + +
    +
    + + + + Security +
    +

    CA Certificates Configuration

    +

    + Configure custom Certificate Authority (CA) certificates for connecting to self-signed or privately signed Gitea instances. +

    +
    + + +
    +

    Overview

    + +
    +

    + When your Gitea instance uses a self-signed certificate or a certificate signed by a private Certificate Authority (CA), + you need to configure Gitea Mirror to trust these certificates. This guide explains how to add custom CA certificates + for different deployment methods. +

    +
    + +
    +
    +
    + + + +
    +
    +

    Important

    +

    Without proper CA certificate configuration, you'll encounter SSL/TLS errors when connecting to Gitea instances with custom certificates.

    +
    +
    +
    +
    + +
    + + +
    +

    Common SSL/TLS Errors

    + +

    If you see any of these errors, you likely need to configure CA certificates:

    + +
    + {[ + 'UNABLE_TO_VERIFY_LEAF_SIGNATURE', + 'SELF_SIGNED_CERT_IN_CHAIN', + 'UNABLE_TO_GET_ISSUER_CERT_LOCALLY', + 'CERT_UNTRUSTED', + 'unable to verify the first certificate' + ].map(error => ( +
    + {error} +
    + ))} +
    +
    + +
    + + +
    +

    Docker Configuration

    + +

    For Docker deployments, you have several options to add custom CA certificates:

    + +

    Method 1: Volume Mount (Recommended)

    + +
    +
      +
    1. + 1. Create a certificates directory +
      +
      mkdir -p ./certs
      +
      +
    2. +
    3. + 2. Copy your CA certificate(s) +
      +
      cp /path/to/your-ca-cert.crt ./certs/
      +
      +
    4. +
    5. + 3. Update docker-compose.yml +
      +
      {`version: '3.8'
      +services:
      +  gitea-mirror:
      +    image: raylabs/gitea-mirror:latest
      +    volumes:
      +      - ./data:/app/data
      +      - ./certs:/usr/local/share/ca-certificates:ro
      +    environment:
      +      - NODE_EXTRA_CA_CERTS=/usr/local/share/ca-certificates/your-ca-cert.crt`}
      +
      +
    6. +
    7. + 4. Restart the container +
      +
      docker-compose down && docker-compose up -d
      +
      +
    8. +
    +
    + +

    Method 2: Custom Docker Image

    + +
    +

    For permanent certificate inclusion, create a custom Docker image:

    + +
    +
    {`FROM raylabs/gitea-mirror:latest
    +
    +# Copy CA certificates
    +COPY ./certs/*.crt /usr/local/share/ca-certificates/
    +
    +# Update CA certificates
    +RUN update-ca-certificates
    +
    +# Set environment variable
    +ENV NODE_EXTRA_CA_CERTS=/usr/local/share/ca-certificates/your-ca-cert.crt`}
    +
    + +

    Build and use your custom image:

    +
    +
    docker build -t my-gitea-mirror .
    +
    +
    +
    + +
    + + +
    +

    Native/Bun Configuration

    + +

    For native Bun deployments, configure CA certificates using environment variables:

    + +

    Method 1: Environment Variable

    + +
    +
      +
    1. + 1. Export the certificate path +
      +
      export NODE_EXTRA_CA_CERTS=/path/to/your-ca-cert.crt
      +
      +
    2. +
    3. + 2. Run Gitea Mirror +
      +
      bun run start
      +
      +
    4. +
    +
    + +

    Method 2: .env File

    + +
    +

    Add to your .env file:

    +
    +
    NODE_EXTRA_CA_CERTS=/path/to/your-ca-cert.crt
    +
    +
    + +

    Method 3: System-wide CA Store

    + +
    +

    Add certificates to your system's CA store:

    + +
    +
    + Ubuntu/Debian: +
    +
    {`sudo cp your-ca-cert.crt /usr/local/share/ca-certificates/
    +sudo update-ca-certificates`}
    +
    +
    + +
    + RHEL/CentOS/Fedora: +
    +
    {`sudo cp your-ca-cert.crt /etc/pki/ca-trust/source/anchors/
    +sudo update-ca-trust`}
    +
    +
    + +
    + macOS: +
    +
    {`sudo security add-trusted-cert -d -r trustRoot \\
    +  -k /Library/Keychains/System.keychain your-ca-cert.crt`}
    +
    +
    +
    +
    +
    + +
    + + +
    +

    LXC Container Configuration

    + +

    For LXC deployments on Proxmox VE:

    + +
    +
      +
    1. + 1. Enter the container +
      +
      pct enter <container-id>
      +
      +
    2. +
    3. + 2. Create certificates directory +
      +
      mkdir -p /usr/local/share/ca-certificates
      +
      +
    4. +
    5. + 3. Copy your CA certificate +
      +
      cat > /usr/local/share/ca-certificates/your-ca.crt
      +
      +

      Paste your certificate content and press Ctrl+D

      +
    6. +
    7. + 4. Update the systemd service +
      +
      {`cat >> /etc/systemd/system/gitea-mirror.service << EOF
      +Environment="NODE_EXTRA_CA_CERTS=/usr/local/share/ca-certificates/your-ca.crt"
      +EOF`}
      +
      +
    8. +
    9. + 5. Reload and restart +
      +
      {`systemctl daemon-reload
      +systemctl restart gitea-mirror`}
      +
      +
    10. +
    +
    +
    + +
    + + +
    +

    Multiple CA Certificates

    + +

    If you need to trust multiple CA certificates:

    + +

    Option 1: Bundle Certificates

    + +
    +

    Combine multiple certificates into one file:

    +
    +
    {`cat ca-cert1.crt ca-cert2.crt ca-cert3.crt > ca-bundle.crt
    +export NODE_EXTRA_CA_CERTS=/path/to/ca-bundle.crt`}
    +
    +
    + +

    Option 2: System CA Store

    + +
    +

    Add all certificates to the system CA store (recommended for production):

    +
    +
    {`# Copy all certificates
    +cp *.crt /usr/local/share/ca-certificates/
    +update-ca-certificates`}
    +
    +
    +
    + +
    + + +
    +

    Verifying Certificate Configuration

    + +

    Test your certificate configuration:

    + +
    +

    1. Test Gitea Connection

    +

    Use the "Test Connection" button in the Gitea configuration section

    + +

    2. Check Logs

    +

    Look for SSL/TLS errors in the application logs:

    + +
    +
    + Docker: +
    + docker logs gitea-mirror +
    +
    +
    + Native: +
    + Check terminal output +
    +
    +
    + LXC: +
    + journalctl -u gitea-mirror -f +
    +
    +
    + +

    3. Manual Certificate Test

    +

    Test SSL connection directly:

    +
    +
    openssl s_client -connect your-gitea-domain.com:443 -CAfile /path/to/ca-cert.crt
    +
    +
    +
    + +
    + + +
    +

    Best Practices

    + +
    + {[ + { + icon: '๐Ÿ”’', + title: 'Certificate Security', + items: [ + 'Keep CA certificates secure', + 'Use read-only mounts in Docker', + 'Limit certificate file permissions', + 'Regularly update certificates' + ] + }, + { + icon: '๐Ÿ“', + title: 'Certificate Management', + items: [ + 'Use descriptive certificate filenames', + 'Document certificate purposes', + 'Track certificate expiration dates', + 'Maintain certificate backups' + ] + }, + { + icon: '๐Ÿข', + title: 'Production Deployment', + items: [ + 'Use proper SSL certificates when possible', + 'Consider Let\'s Encrypt for public instances', + 'Implement certificate rotation procedures', + 'Monitor certificate expiration' + ] + }, + { + icon: '๐Ÿ”', + title: 'Troubleshooting', + items: [ + 'Verify certificate format (PEM)', + 'Check certificate chain completeness', + 'Ensure proper file permissions', + 'Test with openssl commands' + ] + } + ].map(section => ( +
    +
    + {section.icon} +

    {section.title}

    +
    +
      + {section.items.map(item => ( +
    • + โ€ข + {item} +
    • + ))} +
    +
    + ))} +
    +
    + +
    + + +
    +

    Common Issues and Solutions

    + +
    +
    +

    Certificate not being recognized

    +
      +
    • + โ€ข + Ensure the certificate is in PEM format +
    • +
    • + โ€ข + Check that NODE_EXTRA_CA_CERTS points to the correct file +
    • +
    • + โ€ข + Restart the application after adding certificates +
    • +
    +
    + +
    +

    Still getting SSL errors

    +
      +
    • + โ€ข + Verify the complete certificate chain is included +
    • +
    • + โ€ข + Check if intermediate certificates are needed +
    • +
    • + โ€ข + Ensure the certificate matches the server hostname +
    • +
    +
    + +
    +

    Certificate expired

    +
      +
    • + โ€ข + Check certificate validity: openssl x509 -in cert.crt -noout -dates +
    • +
    • + โ€ข + Update with new certificate from your CA +
    • +
    • + โ€ข + Restart Gitea Mirror after updating +
    • +
    +
    +
    +
    +
    +
    +
    \ No newline at end of file diff --git a/src/pages/docs/index.astro b/src/pages/docs/index.astro index 8dbf4a9..e20edbc 100644 --- a/src/pages/docs/index.astro +++ b/src/pages/docs/index.astro @@ -1,16 +1,16 @@ --- import MainLayout from '../../layouts/main.astro'; -import { LuSettings, LuRocket, LuBookOpen } from 'react-icons/lu'; +import { LuSettings, LuRocket, LuBookOpen, LuShield, LuKey, LuNetwork } from 'react-icons/lu'; // Define our documentation pages directly const docs = [ { - slug: 'architecture', - title: 'Architecture', - description: 'Comprehensive overview of the Gitea Mirror application architecture.', + slug: 'quickstart', + title: 'Quick Start Guide', + description: 'Get started with Gitea Mirror quickly.', order: 1, - icon: LuBookOpen, - href: '/docs/architecture' + icon: LuRocket, + href: '/docs/quickstart' }, { slug: 'configuration', @@ -21,12 +21,36 @@ const docs = [ href: '/docs/configuration' }, { - slug: 'quickstart', - title: 'Quick Start Guide', - description: 'Get started with Gitea Mirror quickly.', + slug: 'authentication', + title: 'Authentication & SSO', + description: 'Configure authentication methods, SSO providers, and OIDC.', order: 3, - icon: LuRocket, - href: '/docs/quickstart' + icon: LuKey, + href: '/docs/authentication' + }, + { + slug: 'architecture', + title: 'Architecture', + description: 'Comprehensive overview of the Gitea Mirror application architecture.', + order: 4, + icon: LuBookOpen, + href: '/docs/architecture' + }, + { + slug: 'ca-certificates', + title: 'CA Certificates', + description: 'Configure custom CA certificates for self-signed Gitea instances.', + order: 5, + icon: LuShield, + href: '/docs/ca-certificates' + }, + { + slug: 'advanced', + title: 'Advanced Topics', + description: 'Advanced configuration, troubleshooting, and deployment options.', + order: 6, + icon: LuNetwork, + href: '/docs/advanced' } ]; diff --git a/src/pages/docs/quickstart.astro b/src/pages/docs/quickstart.astro index 83675bd..2193f7d 100644 --- a/src/pages/docs/quickstart.astro +++ b/src/pages/docs/quickstart.astro @@ -244,7 +244,7 @@ bun run start title: 'Create Admin Account', items: [ "You'll be prompted on first access", - 'Choose a secure username and password', + 'Enter your email address and password', 'This will be your administrator account' ] }, diff --git a/src/pages/index.astro b/src/pages/index.astro index 854fb9f..dfd1253 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -1,12 +1,13 @@ --- import '../styles/global.css'; import App from '@/components/layout/MainLayout'; -import { db, repositories, mirrorJobs, client } from '@/lib/db'; +import { db, repositories, mirrorJobs, users } from '@/lib/db'; +import { sql } from 'drizzle-orm'; import ThemeScript from '@/components/theme/ThemeScript.astro'; // Check if any users exist in the database -const userCountResult = await client.execute(`SELECT COUNT(*) as count FROM users`); -const userCount = userCountResult.rows[0].count; +const userCountResult = await db.select({ count: sql`count(*)` }).from(users); +const userCount = userCountResult[0]?.count || 0; // Redirect to signup if no users exist if (userCount === 0) { diff --git a/src/pages/login.astro b/src/pages/login.astro index 3cf82a6..e27a6db 100644 --- a/src/pages/login.astro +++ b/src/pages/login.astro @@ -1,12 +1,15 @@ --- import '../styles/global.css'; import ThemeScript from '@/components/theme/ThemeScript.astro'; -import { LoginForm } from '@/components/auth/LoginForm'; -import { client } from '../lib/db'; +import { LoginPage } from '@/components/auth/LoginPage'; +import { db, users } from '@/lib/db'; +import { sql } from 'drizzle-orm'; // Check if any users exist in the database -const userCountResult = await client.execute(`SELECT COUNT(*) as count FROM users`); -const userCount = userCountResult.rows[0].count; +const userCountResult = await db + .select({ count: sql`count(*)` }) + .from(users); +const userCount = userCountResult[0].count; // Redirect to signup if no users exist if (userCount === 0) { @@ -27,7 +30,7 @@ const generator = Astro.generator;
    - +
    diff --git a/src/pages/oauth/consent.astro b/src/pages/oauth/consent.astro new file mode 100644 index 0000000..c741944 --- /dev/null +++ b/src/pages/oauth/consent.astro @@ -0,0 +1,28 @@ +--- +import '@/styles/global.css'; +import ConsentPage from '@/components/oauth/ConsentPage'; +import ThemeScript from '@/components/theme/ThemeScript.astro'; +import Providers from '@/components/layout/Providers'; + +// Check if user is authenticated +const sessionCookie = Astro.cookies.get('better-auth-session'); +if (!sessionCookie) { + return Astro.redirect('/login'); +} +--- + + + + + + + + Authorize Application - Gitea Mirror + + + + + + + + \ No newline at end of file diff --git a/src/pages/signup.astro b/src/pages/signup.astro index d7f09d3..3dcfc4c 100644 --- a/src/pages/signup.astro +++ b/src/pages/signup.astro @@ -1,12 +1,15 @@ --- import '../styles/global.css'; import ThemeScript from '@/components/theme/ThemeScript.astro'; -import { SignupForm } from '@/components/auth/SignupForm'; -import { client } from '../lib/db'; +import { SignupPage } from '@/components/auth/SignupPage'; +import { db, users } from '@/lib/db'; +import { sql } from 'drizzle-orm'; // Check if any users exist in the database -const userCountResult = await client.execute(`SELECT COUNT(*) as count FROM users`); -const userCount = userCountResult.rows[0]?.count; +const userCountResult = await db + .select({ count: sql`count(*)` }) + .from(users); +const userCount = userCountResult[0]?.count; // Redirect to login if users already exist if (userCount !== null && Number(userCount) > 0) { @@ -31,7 +34,7 @@ const generator = Astro.generator;

    Welcome to Gitea Mirror

    Let's set up your administrator account to get started.

- +
diff --git a/src/tests/setup.bun.ts b/src/tests/setup.bun.ts index 1de560d..7050445 100644 --- a/src/tests/setup.bun.ts +++ b/src/tests/setup.bun.ts @@ -3,16 +3,71 @@ * This file is automatically loaded before running tests */ -import { afterEach, beforeEach } from "bun:test"; +import { mock } from "bun:test"; -// Clean up after each test -afterEach(() => { - // Add any cleanup logic here +// Set NODE_ENV to test +process.env.NODE_ENV = "test"; + +// Mock the database module to prevent real database access during tests +mock.module("@/lib/db", () => { + const mockDb = { + select: () => ({ + from: () => ({ + where: () => ({ + limit: () => Promise.resolve([]) + }) + }) + }), + insert: () => ({ + values: () => Promise.resolve() + }), + update: () => ({ + set: () => ({ + where: () => Promise.resolve() + }) + }), + delete: () => ({ + where: () => Promise.resolve() + }) + }; + + return { + db: mockDb, + users: {}, + events: {}, + configs: {}, + repositories: {}, + mirrorJobs: {}, + organizations: {}, + sessions: {}, + accounts: {}, + verificationTokens: {}, + oauthApplications: {}, + oauthAccessTokens: {}, + oauthConsent: {}, + ssoProviders: {} + }; }); -// Setup before each test -beforeEach(() => { - // Add any setup logic here +// Mock drizzle-orm to prevent database migrations from running +mock.module("drizzle-orm/bun-sqlite/migrator", () => { + return { + migrate: () => {} + }; +}); + +// Mock config encryption utilities +mock.module("@/lib/utils/config-encryption", () => { + return { + decryptConfigTokens: (config: any) => { + // Return the config as-is for tests + return config; + }, + encryptConfigTokens: (config: any) => { + // Return the config as-is for tests + return config; + } + }; }); // Add DOM testing support if needed diff --git a/tsconfig.json b/tsconfig.json index 90604a7..e1b23b6 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -15,6 +15,7 @@ "@/*": [ "./src/*" ] - } + }, + "types": ["bun-types"] } } \ No newline at end of file diff --git a/www/src/components/ui/button.tsx b/www/src/components/ui/button.tsx index a2df8dc..d4bf579 100644 --- a/www/src/components/ui/button.tsx +++ b/www/src/components/ui/button.tsx @@ -35,25 +35,24 @@ const buttonVariants = cva( } ) -function Button({ - className, - variant, - size, - asChild = false, - ...props -}: React.ComponentProps<"button"> & - VariantProps & { - asChild?: boolean - }) { +const Button = React.forwardRef< + HTMLButtonElement, + React.ComponentProps<"button"> & + VariantProps & { + asChild?: boolean + } +>(({ className, variant, size, asChild = false, ...props }, ref) => { const Comp = asChild ? Slot : "button" return ( ) -} +}) +Button.displayName = "Button" export { Button, buttonVariants }