diff --git a/docs/oauth-setup.md b/docs/oauth-setup.md new file mode 100644 index 00000000..d9f14ff0 --- /dev/null +++ b/docs/oauth-setup.md @@ -0,0 +1,499 @@ +# OAuth and Email Verification Setup + +This guide covers setting up OAuth authentication providers (Google, Apple, Facebook) and email verification for Boardsesh. + +## Table of Contents + +- [Architecture Overview](#architecture-overview) +- [Prerequisites](#prerequisites) +- [Environment Variables](#environment-variables) +- [OAuth Provider Setup](#oauth-provider-setup) + - [Google OAuth](#1-google-oauth) + - [Apple Sign-In](#2-apple-sign-in) + - [Facebook OAuth](#3-facebook-oauth) +- [Email Verification Setup](#email-verification-setup) +- [Testing](#testing) +- [Production Deployment](#production-deployment) +- [Troubleshooting](#troubleshooting) + +--- + +## Architecture Overview + +Boardsesh uses NextAuth.js v4 for authentication with the following components: + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ Client (Browser) │ +├─────────────────────────────────────────────────────────────────┤ +│ SocialLoginButtons │ AuthPageContent │ AuthModal │ +│ (dynamic provider │ (login/register │ (modal auth flow) │ +│ discovery) │ forms) │ │ +└──────────┬───────────┴────────┬──────────┴──────────────────────┘ + │ │ + ▼ ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ NextAuth.js API Routes │ +├─────────────────────────────────────────────────────────────────┤ +│ /api/auth/[...nextauth] - Core NextAuth handlers │ +│ /api/auth/providers-config - Available providers endpoint │ +│ /api/auth/register - Email/password registration │ +│ /api/auth/verify-email - Email verification callback │ +│ /api/auth/resend-verification - Resend verification emails │ +└──────────┬───────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ Auth Configuration │ +├─────────────────────────────────────────────────────────────────┤ +│ packages/web/app/lib/auth/auth-options.ts │ +│ - Conditional OAuth providers (Google, Apple, Facebook) │ +│ - Credentials provider (email/password) │ +│ - JWT session strategy │ +│ - DrizzleAdapter for database persistence │ +└──────────┬───────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ Database (PostgreSQL) │ +├─────────────────────────────────────────────────────────────────┤ +│ users - User accounts │ +│ accounts - OAuth provider accounts (linked) │ +│ sessions - Active sessions │ +│ verificationTokens - Email verification tokens │ +│ userCredentials - Password hashes (separate from users) │ +│ userProfiles - Display preferences │ +└─────────────────────────────────────────────────────────────────┘ +``` + +### Key Features + +- **Conditional Providers**: OAuth buttons only appear when provider credentials are configured +- **Email Verification**: New email/password accounts require email verification +- **Account Linking**: OAuth users can add passwords; password users cannot add OAuth (security) +- **JWT Sessions**: Stateless authentication with 5-minute refresh interval +- **Rate Limiting**: In-memory rate limiting on auth endpoints (best-effort in serverless) + +--- + +## Prerequisites + +Before setting up OAuth providers, ensure you have: + +1. **Node.js 18+** installed +2. **PostgreSQL database** running (via `npm run db:up` for local dev) +3. **NEXTAUTH_SECRET** generated: + ```bash + openssl rand -base64 32 + ``` +4. **NEXTAUTH_URL** set to your application URL + +--- + +## Environment Variables + +Add these to `packages/web/.env.development.local` (local) or your production environment: + +### Required Variables + +| Variable | Description | Example | +|----------|-------------|---------| +| `NEXTAUTH_SECRET` | Session encryption key (32+ chars) | `openssl rand -base64 32` | +| `NEXTAUTH_URL` | Your app's base URL | `http://localhost:3000` | + +### OAuth Providers (Optional - configure as needed) + +| Variable | Provider | Description | +|----------|----------|-------------| +| `GOOGLE_CLIENT_ID` | Google | OAuth Client ID | +| `GOOGLE_CLIENT_SECRET` | Google | OAuth Client Secret | +| `APPLE_ID` | Apple | Services ID (e.g., `com.boardsesh.signin`) | +| `APPLE_SECRET` | Apple | JWT secret (regenerate every 6 months) | +| `FACEBOOK_CLIENT_ID` | Facebook | App ID | +| `FACEBOOK_CLIENT_SECRET` | Facebook | App Secret | + +### Email (Required for email verification) + +| Variable | Description | Default | +|----------|-------------|---------| +| `SMTP_HOST` | SMTP server hostname | `smtp.fastmail.com` | +| `SMTP_PORT` | SMTP port | `465` | +| `SMTP_USER` | SMTP username/email | - | +| `SMTP_PASSWORD` | SMTP app password | - | +| `EMAIL_FROM` | Sender email address | Same as `SMTP_USER` | + +### Example Configuration + +```bash +# Core NextAuth +NEXTAUTH_SECRET=your-32-character-secret-here +NEXTAUTH_URL=http://localhost:3000 + +# Google OAuth +GOOGLE_CLIENT_ID=123456789.apps.googleusercontent.com +GOOGLE_CLIENT_SECRET=GOCSPX-xxxxxxxxxxxxx + +# Apple Sign-In +APPLE_ID=com.boardsesh.signin +APPLE_SECRET=eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9... + +# Facebook OAuth +FACEBOOK_CLIENT_ID=1234567890 +FACEBOOK_CLIENT_SECRET=abcdef1234567890 + +# Email (Fastmail) +SMTP_HOST=smtp.fastmail.com +SMTP_PORT=465 +SMTP_USER=your-email@fastmail.com +SMTP_PASSWORD=your-app-password +EMAIL_FROM=your-email@fastmail.com +``` + +--- + +## OAuth Provider Setup + +### 1. Google OAuth + +**Difficulty**: Easy | **Time**: ~10 minutes + +1. Go to [Google Cloud Console](https://console.cloud.google.com/) +2. Create a new project or select an existing one +3. Navigate to **APIs & Services** > **Credentials** +4. Click **Create Credentials** > **OAuth client ID** +5. Select **Web application** as the application type +6. Configure authorized origins and redirects: + + | Environment | JavaScript Origins | Redirect URIs | + |-------------|-------------------|---------------| + | Development | `http://localhost:3000` | `http://localhost:3000/api/auth/callback/google` | + | Production | `https://your-domain.com` | `https://your-domain.com/api/auth/callback/google` | + +7. Copy **Client ID** and **Client Secret** to environment variables + +### 2. Apple Sign-In + +**Difficulty**: Hard | **Time**: ~30-60 minutes | **Requires**: Paid Apple Developer Account ($99/year) + +> **Important**: Apple Sign-In requires HTTPS. Use ngrok for local development. + +#### Step 1: Create an App ID + +1. Go to [Apple Developer Portal](https://developer.apple.com/) > **Certificates, Identifiers & Profiles** +2. Go to **Identifiers** > Click **+** +3. Select **App IDs** > Continue +4. Choose **App** type > Continue +5. Fill in: + - Description: `Boardsesh` + - Bundle ID: `com.boardsesh.app` (explicit) +6. Enable **Sign In with Apple** capability +7. Register the App ID + +#### Step 2: Create a Services ID + +1. Go to **Identifiers** > Click **+** +2. Select **Services IDs** > Continue +3. Fill in: + - Description: `Boardsesh Web Auth` + - Identifier: `com.boardsesh.signin` +4. Enable **Sign In with Apple** +5. Click **Configure**: + - Primary App ID: Select `Boardsesh` (created above) + - Domains: Add your domain(s) + - `your-domain.com` (production) + - `xxxx.ngrok.io` (development - get from ngrok) + - Return URLs: + - `https://your-domain.com/api/auth/callback/apple` + - `https://xxxx.ngrok.io/api/auth/callback/apple` +6. Save and Register + +#### Step 3: Create a Key + +1. Go to **Keys** > Click **+** +2. Enter name: `Boardsesh Sign In Key` +3. Enable **Sign In with Apple** +4. Click **Configure** > Select Primary App ID > Save +5. Click **Continue** > **Register** +6. **Download the `.p8` key file** (you can only download once!) +7. Note the **Key ID** displayed + +#### Step 4: Generate the Apple Secret JWT + +Apple requires a JWT that expires every 6 months. Create it with this script: + +```javascript +// generate-apple-secret.js +const jwt = require('jsonwebtoken'); +const fs = require('fs'); + +// Your values from Apple Developer Portal +const TEAM_ID = 'XXXXXXXXXX'; // Found in Membership details +const KEY_ID = 'XXXXXXXXXX'; // Key ID from Step 3 +const SERVICE_ID = 'com.boardsesh.signin'; // Services ID from Step 2 +const KEY_FILE = './AuthKey_XXXXXXXX.p8'; // Downloaded key file + +const privateKey = fs.readFileSync(KEY_FILE); + +const token = jwt.sign({}, privateKey, { + algorithm: 'ES256', + expiresIn: '180d', + audience: 'https://appleid.apple.com', + issuer: TEAM_ID, + subject: SERVICE_ID, + keyid: KEY_ID, +}); + +console.log('APPLE_SECRET=' + token); +console.log('\nThis token expires in 180 days. Set a reminder to regenerate!'); +``` + +Run it: +```bash +npm install jsonwebtoken +node generate-apple-secret.js +``` + +#### Local Development with ngrok + +```bash +# Install ngrok +npm install -g ngrok + +# Start tunnel (in a separate terminal) +ngrok http 3000 + +# Use the https URL from ngrok output +# Update NEXTAUTH_URL and Apple Services ID configuration +``` + +### 3. Facebook OAuth + +**Difficulty**: Medium | **Time**: ~15 minutes + +1. Go to [Facebook Developers](https://developers.facebook.com/) +2. Click **My Apps** > **Create App** +3. Select **Consumer** as app type > Next +4. Fill in app details and create + +5. In the app dashboard: + - Go to **Settings** > **Basic** + - Copy **App ID** and **App Secret** + +6. Add Facebook Login product: + - Click **Add Product** > Find **Facebook Login** > **Set Up** + - Go to **Facebook Login** > **Settings** + - Add Valid OAuth Redirect URIs: + - `http://localhost:3000/api/auth/callback/facebook` + - `https://your-domain.com/api/auth/callback/facebook` + +7. For production: + - Go to **Settings** > **Basic** + - Add Privacy Policy URL and Terms of Service URL + - Toggle app to **Live** mode + +--- + +## Email Verification Setup + +Email verification is required for email/password accounts. OAuth accounts are pre-verified by the provider. + +### Fastmail Setup (Recommended) + +1. Log in to [Fastmail](https://www.fastmail.com/) +2. Go to **Settings** > **Password & Security** > **Third-party apps** +3. Click **New app password** +4. Name it: `Boardsesh Email` +5. Copy the generated password + +### Other SMTP Providers + +| Provider | Host | Port | Notes | +|----------|------|------|-------| +| Fastmail | `smtp.fastmail.com` | 465 (SSL) | Recommended | +| Gmail | `smtp.gmail.com` | 587 (TLS) | Requires App Password | +| SendGrid | `smtp.sendgrid.net` | 587 | Use API key as password | +| Mailgun | `smtp.mailgun.org` | 587 | Use SMTP credentials | + +### Email Template + +Verification emails are sent using a styled HTML template that matches the Boardsesh design system. The template includes: + +- Branded header with primary color +- Clear call-to-action button +- Plain text link fallback +- 24-hour expiration notice + +--- + +## Testing + +### 1. Test Email Verification + +```bash +# Start development server +npm run dev + +# Navigate to login page +open http://localhost:3000/auth/login +``` + +1. Click **Create Account** tab +2. Enter email, password, and name +3. Submit the form +4. Check inbox for verification email +5. Click verification link +6. Login with credentials + +### 2. Test OAuth Providers + +**Google:** +1. Ensure `GOOGLE_CLIENT_ID` and `GOOGLE_CLIENT_SECRET` are set +2. Restart dev server +3. Click "Continue with Google" +4. Complete Google sign-in +5. Verify redirect back to app + +**Apple:** +1. Start ngrok: `ngrok http 3000` +2. Update `NEXTAUTH_URL` to ngrok URL +3. Ensure Apple Services ID has ngrok domain +4. Click "Continue with Apple" +5. Complete Apple sign-in + +**Facebook:** +1. Ensure Facebook app is in Development mode (for testing) +2. Add your Facebook account as a tester +3. Click "Continue with Facebook" +4. Complete Facebook sign-in + +### 3. Verify Provider Discovery + +Check that only configured providers appear: + +```bash +curl http://localhost:3000/api/auth/providers-config +# Returns: {"google":true,"apple":false,"facebook":false} +``` + +--- + +## Production Deployment + +### Checklist + +- [ ] **NEXTAUTH_SECRET**: Strong, unique secret (32+ characters) +- [ ] **NEXTAUTH_URL**: Production URL (no trailing slash) +- [ ] **OAuth Redirect URIs**: Updated for production domain +- [ ] **SMTP Credentials**: Production email service configured +- [ ] **Apple Secret**: Valid JWT (check expiration date) +- [ ] **Facebook App**: In Live mode +- [ ] **Database**: Migrations applied + +### Vercel Deployment + +1. Add environment variables in Vercel dashboard: + - Project Settings > Environment Variables + - Add all required variables + +2. The `NEXTAUTH_URL` can be omitted on Vercel (auto-detected) + +3. Ensure callback URLs are added to OAuth providers: + - `https://your-app.vercel.app/api/auth/callback/[provider]` + +### Security Recommendations + +1. **Rotate secrets regularly**: + - `NEXTAUTH_SECRET`: Every 6-12 months + - `APPLE_SECRET`: Every 6 months (required) + - SMTP passwords: Follow provider recommendations + +2. **Monitor authentication**: + - Enable logging for failed auth attempts + - Set up alerts for unusual patterns + +3. **Rate limiting**: + - Current implementation uses in-memory storage + - For production with strict requirements, consider Redis-based rate limiting: + ```bash + npm install @upstash/ratelimit @upstash/redis + ``` + +4. **HTTPS only**: + - All production OAuth must use HTTPS + - Cookies are secure by default in production + +--- + +## Troubleshooting + +### "redirect_uri_mismatch" Error + +**Cause**: OAuth callback URL doesn't match registered URL + +**Solution**: +1. Check redirect URI in provider console exactly matches +2. Watch for: + - Trailing slashes (`/api/auth/callback/google` vs `/api/auth/callback/google/`) + - Protocol mismatch (`http` vs `https`) + - Port numbers in development + +### Apple Sign-In Not Working + +**Cause 1**: Using HTTP instead of HTTPS +- Apple requires HTTPS +- Use ngrok for local development + +**Cause 2**: Expired Apple Secret JWT +- JWT expires every 6 months +- Regenerate using the script above + +**Cause 3**: Domain not registered +- Add domain to Apple Services ID configuration +- Include ngrok domains for development + +### Email Not Sending + +**Check**: +1. SMTP credentials are correct +2. App password is active (not main password) +3. Server logs for SMTP errors: + ```bash + npm run dev + # Look for "Failed to send verification email" in console + ``` + +### "OAuthAccountNotLinked" Error + +**Cause**: User already has an account with different auth method + +**Example**: User registered with email/password, then tried Google sign-in with same email + +**Solution**: +- Users must sign in with their original method +- Account linking from OAuth to password is supported +- Account linking from password to OAuth is blocked (security) + +### Social Buttons Not Appearing + +**Check**: +1. Provider environment variables are set +2. Server was restarted after adding variables +3. Check API response: + ```bash + curl http://localhost:3000/api/auth/providers-config + ``` + +### Session Not Persisting + +**Check**: +1. `NEXTAUTH_SECRET` is consistent across deployments +2. Cookies are not being blocked by browser +3. Session callback in `auth-options.ts` includes user ID + +--- + +## Related Documentation + +- [NextAuth.js Documentation](https://next-auth.js.org/) +- [Drizzle ORM Documentation](https://orm.drizzle.team/) +- [WebSocket Authentication](./websocket-implementation.md) - How auth integrates with real-time features diff --git a/package-lock.json b/package-lock.json index a8af944d..1d5138da 100644 --- a/package-lock.json +++ b/package-lock.json @@ -818,6 +818,58 @@ "node": ">=18.0.0" } }, + "node_modules/@aws-sdk/client-sesv2": { + "version": "3.958.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sesv2/-/client-sesv2-3.958.0.tgz", + "integrity": "sha512-3x3n8IIxIMAkdpt9wy9zS7MO2lqTcJwQTdHMn6BlD7YUohb+r5Q4KCOEQ2uHWd4WIJv2tlbXnfypHaXReO/WXA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.957.0", + "@aws-sdk/credential-provider-node": "3.958.0", + "@aws-sdk/middleware-host-header": "3.957.0", + "@aws-sdk/middleware-logger": "3.957.0", + "@aws-sdk/middleware-recursion-detection": "3.957.0", + "@aws-sdk/middleware-user-agent": "3.957.0", + "@aws-sdk/region-config-resolver": "3.957.0", + "@aws-sdk/signature-v4-multi-region": "3.957.0", + "@aws-sdk/types": "3.957.0", + "@aws-sdk/util-endpoints": "3.957.0", + "@aws-sdk/util-user-agent-browser": "3.957.0", + "@aws-sdk/util-user-agent-node": "3.957.0", + "@smithy/config-resolver": "^4.4.5", + "@smithy/core": "^3.20.0", + "@smithy/fetch-http-handler": "^5.3.8", + "@smithy/hash-node": "^4.2.7", + "@smithy/invalid-dependency": "^4.2.7", + "@smithy/middleware-content-length": "^4.2.7", + "@smithy/middleware-endpoint": "^4.4.1", + "@smithy/middleware-retry": "^4.4.17", + "@smithy/middleware-serde": "^4.2.8", + "@smithy/middleware-stack": "^4.2.7", + "@smithy/node-config-provider": "^4.3.7", + "@smithy/node-http-handler": "^4.4.7", + "@smithy/protocol-http": "^5.3.7", + "@smithy/smithy-client": "^4.10.2", + "@smithy/types": "^4.11.0", + "@smithy/url-parser": "^4.2.7", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-body-length-node": "^4.2.1", + "@smithy/util-defaults-mode-browser": "^4.3.16", + "@smithy/util-defaults-mode-node": "^4.2.19", + "@smithy/util-endpoints": "^3.2.7", + "@smithy/util-middleware": "^4.2.7", + "@smithy/util-retry": "^4.2.7", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@aws-sdk/client-sso": { "version": "3.958.0", "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.958.0.tgz", @@ -3575,56 +3627,6 @@ "node": ">=12" } }, - "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "license": "MIT" - }, - "node_modules/@isaacs/cliui/node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -3654,6 +3656,17 @@ "node": ">=6.0.0" } }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", @@ -4563,7 +4576,7 @@ "version": "1.57.0", "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.57.0.tgz", "integrity": "sha512-6TyEnHgd6SArQO8UO2OMTxshln3QMWBtPGrOCgs3wVEmQmwyuNtB10IZMfmYDE0riwNR1cu4q+pPcxMVtaG3TA==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "dependencies": { "playwright": "1.57.0" @@ -5762,15 +5775,6 @@ "node": ">= 14" } }, - "node_modules/@sentry/bundler-plugin-core/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, "node_modules/@sentry/bundler-plugin-core/node_modules/dotenv": { "version": "16.6.1", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", @@ -5783,26 +5787,6 @@ "url": "https://dotenvx.com" } }, - "node_modules/@sentry/bundler-plugin-core/node_modules/glob": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", - "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/@sentry/bundler-plugin-core/node_modules/magic-string": { "version": "0.30.8", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.8.tgz", @@ -5815,30 +5799,6 @@ "node": ">=12" } }, - "node_modules/@sentry/bundler-plugin-core/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@sentry/bundler-plugin-core/node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "license": "ISC", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, "node_modules/@sentry/cli": { "version": "2.58.4", "resolved": "https://registry.npmjs.org/@sentry/cli/-/cli-2.58.4.tgz", @@ -6153,30 +6113,6 @@ "@opentelemetry/semantic-conventions": "^1.37.0" } }, - "node_modules/@sentry/node/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@sentry/node/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/@sentry/opentelemetry": { "version": "10.32.1", "resolved": "https://registry.npmjs.org/@sentry/opentelemetry/-/opentelemetry-10.32.1.tgz", @@ -7249,6 +7185,28 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/eslint": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -7279,6 +7237,13 @@ "node": ">= 6.x" } }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "license": "MIT", + "peer": true + }, "node_modules/@types/mysql": { "version": "2.15.27", "resolved": "https://registry.npmjs.org/@types/mysql/-/mysql-2.15.27.tgz", @@ -7297,6 +7262,17 @@ "undici-types": "~7.16.0" } }, + "node_modules/@types/nodemailer": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-7.0.4.tgz", + "integrity": "sha512-ee8fxWqOchH+Hv6MDDNNy028kwvVnLplrStm4Zf/3uHWw5zzo8FoYYeffpJtGs2wWysEumMH0ZIdMGMY1eMAow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@aws-sdk/client-sesv2": "^3.839.0", + "@types/node": "*" + } + }, "node_modules/@types/parse-json": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", @@ -7327,7 +7303,7 @@ "version": "19.2.7", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz", "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "csstype": "^3.2.2" @@ -7628,6 +7604,167 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/@webassemblyjs/ast": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "license": "MIT", + "peer": true + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "license": "MIT", + "peer": true + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "license": "MIT", + "peer": true + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "license": "MIT", + "peer": true + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "license": "MIT", + "peer": true + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" + } + }, "node_modules/@whatwg-node/disposablestack": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/@whatwg-node/disposablestack/-/disposablestack-0.0.6.tgz", @@ -7709,6 +7846,20 @@ "node": ">=18.0.0" } }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "license": "Apache-2.0", + "peer": true + }, "node_modules/abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -7737,6 +7888,19 @@ "acorn": "^8" } }, + "node_modules/acorn-import-phases": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", + "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10.13.0" + }, + "peerDependencies": { + "acorn": "^8.14.0" + } + }, "node_modules/agent-base": { "version": "7.1.4", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", @@ -7747,9 +7911,57 @@ "node": ">= 14" } }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "license": "MIT", + "peer": true, + "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/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "license": "MIT", + "peer": true, + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "license": "MIT", "engines": { @@ -7994,6 +8206,20 @@ "integrity": "sha512-PJvH288AWQhKs2v9zyfYdPzlPqf5bXbGMmhmUIY9x4dAUGIWgomO771oBQNwJnMQSnUIXhKu6sgzpBRXTlvb8Q==", "license": "MIT" }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, "node_modules/bmp-js": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/bmp-js/-/bmp-js-0.1.0.tgz", @@ -8011,14 +8237,12 @@ "license": "MIT" }, "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "license": "MIT", - "optional": true, "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "balanced-match": "^1.0.0" } }, "node_modules/braces": { @@ -8066,18 +8290,67 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "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", + "optional": true, + "peer": true, + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true, "license": "MIT" }, + "node_modules/buffer/node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "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", + "optional": true, + "peer": true + }, "node_modules/bufferutil": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.1.0.tgz", "integrity": "sha512-ZMANVnAixE6AWWnPzlW2KpUrxhm9woycYvPOo67jWHyFowASTEd9s+QN1EIMsSDtwhIxN4sWE1jotpuDUIgyIw==", - "dev": true, + "devOptional": true, "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -8137,19 +8410,20 @@ "license": "CC-BY-4.0" }, "node_modules/canvas": { - "version": "2.11.2", - "resolved": "https://registry.npmjs.org/canvas/-/canvas-2.11.2.tgz", - "integrity": "sha512-ItanGBMrmRV7Py2Z+Xhs7cT+FNt5K0vPL4p9EZ/UX/Mu7hFbkxSjKF2KVtPwX7UYWp7dRKnrTvReflgrItJbdw==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/canvas/-/canvas-3.2.0.tgz", + "integrity": "sha512-jk0GxrLtUEmW/TmFsk2WghvgHe8B0pxGilqCL21y8lHkPUGa6FTsnCNtHPOzT8O3y+N+m3espawV80bbBlgfTA==", + "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { - "@mapbox/node-pre-gyp": "^1.0.0", - "nan": "^2.17.0", - "simple-get": "^3.0.3" + "node-addon-api": "^7.0.0", + "prebuild-install": "^7.1.3" }, "engines": { - "node": ">=6" + "node": "^18.12.0 || >= 20.9.0" } }, "node_modules/chai": { @@ -8199,13 +8473,22 @@ } }, "node_modules/chownr": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", - "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "dev": true, "license": "ISC", "optional": true, + "peer": true + }, + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "license": "MIT", + "peer": true, "engines": { - "node": ">=10" + "node": ">=6.0" } }, "node_modules/cjs-module-lexer": { @@ -8595,16 +8878,33 @@ "license": "MIT" }, "node_modules/decompress-response": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz", - "integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { - "mimic-response": "^2.0.0" + "mimic-response": "^3.1.0" }, "engines": { - "node": ">=8" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">=4.0.0" } }, "node_modules/delegates": { @@ -8814,9 +9114,9 @@ "license": "ISC" }, "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "license": "MIT" }, "node_modules/emoji-regex-xs": { @@ -8828,6 +9128,32 @@ "node": ">=10.0.0" } }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.18.4", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.4.tgz", + "integrity": "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==", + "license": "MIT", + "peer": true, + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/entities": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", @@ -8939,6 +9265,53 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "license": "BSD-2-Clause", + "peer": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "license": "BSD-2-Clause", + "peer": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "license": "BSD-2-Clause", + "peer": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "license": "BSD-2-Clause", + "peer": true, + "engines": { + "node": ">=4.0" + } + }, "node_modules/estree-walker": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", @@ -8955,6 +9328,28 @@ "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", "license": "MIT" }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "dev": true, + "license": "(MIT OR WTFPL)", + "optional": true, + "peer": true, + "engines": { + "node": ">=6" + } + }, "node_modules/expect-type": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", @@ -8965,6 +9360,30 @@ "node": ">=12.0.0" } }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT", + "peer": true + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause", + "peer": true + }, "node_modules/fast-xml-parser": { "version": "5.2.5", "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.2.5.tgz", @@ -9056,24 +9475,21 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/foreground-child/node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/forwarded-parse": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/forwarded-parse/-/forwarded-parse-2.1.2.tgz", "integrity": "sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw==", "license": "MIT" }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true + }, "node_modules/fs-minipass": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", @@ -9159,6 +9575,48 @@ "node": ">=10" } }, + "node_modules/gauge/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT", + "optional": true + }, + "node_modules/gauge/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC", + "optional": true + }, + "node_modules/gauge/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "optional": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/gauge/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "optional": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -9180,23 +9638,30 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true + }, "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", "license": "ISC", - "optional": true, "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" }, - "engines": { - "node": "*" + "bin": { + "glob": "dist/esm/bin.mjs" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -9214,6 +9679,20 @@ "node": ">= 6" } }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "license": "BSD-2-Clause", + "peer": true + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC", + "peer": true + }, "node_modules/graphql": { "version": "16.12.0", "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.12.0.tgz", @@ -9280,7 +9759,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -9405,6 +9883,29 @@ "integrity": "sha512-yjD9nARJ/jb1g+CvD0tlhUHOrJ9Sy0P8T9MF3YaLlHnSRpwPfpTX0XIvpmw3gAJUmEu3FiICLBDPXVwyEvrleg==", "license": "Apache-2.0" }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "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": "BSD-3-Clause", + "optional": true, + "peer": true + }, "node_modules/import-fresh": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", @@ -9462,6 +9963,15 @@ "license": "ISC", "optional": true }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "license": "ISC", + "optional": true, + "peer": true + }, "node_modules/internmap": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", @@ -9716,6 +10226,37 @@ "@pkgjs/parseargs": "^0.11.0" } }, + "node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "license": "MIT", + "peer": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, "node_modules/jose": { "version": "6.1.3", "resolved": "https://registry.npmjs.org/jose/-/jose-6.1.3.tgz", @@ -9789,6 +10330,13 @@ "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", "license": "MIT" }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT", + "peer": true + }, "node_modules/json2mq": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/json2mq/-/json2mq-0.2.0.tgz", @@ -9826,6 +10374,20 @@ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "license": "MIT" }, + "node_modules/loader-runner": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz", + "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.11.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -9941,14 +10503,46 @@ "dev": true, "license": "CC0-1.0" }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "license": "MIT", + "peer": true + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "peer": true, + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/mimic-response": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz", - "integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true, "license": "MIT", "optional": true, + "peer": true, "engines": { - "node": ">=8" + "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -9965,25 +10559,39 @@ } }, "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "license": "ISC", - "optional": true, "dependencies": { - "brace-expansion": "^1.1.7" + "brace-expansion": "^2.0.1" }, "engines": { - "node": "*" + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/minipass": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", - "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", "license": "ISC", "engines": { - "node": ">=8" + "node": ">=16 || 14 >=14.17" } }, "node_modules/minizlib": { @@ -10020,6 +10628,28 @@ "license": "ISC", "optional": true }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", + "optional": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true + }, "node_modules/module-details-from-path": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.4.tgz", @@ -10057,6 +10687,22 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "license": "MIT", + "peer": true + }, "node_modules/next": { "version": "16.1.1", "resolved": "https://registry.npmjs.org/next/-/next-16.1.1.tgz", @@ -10187,6 +10833,45 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/node-abi": { + "version": "3.85.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.85.0.tgz", + "integrity": "sha512-zsFhmbkAzwhTft6nd3VxcG0cvJsT70rL+BIGHWVq5fi6MwGrHwzqKaxXE+Hl2GmnGItnDKPPkO5/LQqjVkIdFg==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-abi/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "optional": true, + "peer": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true + }, "node_modules/node-fetch": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", @@ -10233,7 +10918,7 @@ "version": "4.8.4", "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", - "dev": true, + "devOptional": true, "license": "MIT", "bin": { "node-gyp-build": "bin.js", @@ -10247,6 +10932,15 @@ "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", "license": "MIT" }, + "node_modules/nodemailer": { + "version": "7.0.12", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.12.tgz", + "integrity": "sha512-H+rnK5bX2Pi/6ms3sN4/jRQvYSMltV6vqup/0SFOrxYYY/qoNvhXPlYq3e+Pm9RFJRwrMGbMIwi81M4dxpomhA==", + "license": "MIT-0", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/nopt": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", @@ -10713,7 +11407,7 @@ "version": "1.57.0", "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.57.0.tgz", "integrity": "sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "dependencies": { "playwright-core": "1.57.0" @@ -10732,7 +11426,7 @@ "version": "1.57.0", "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.57.0.tgz", "integrity": "sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "bin": { "playwright-core": "cli.js" @@ -10861,6 +11555,35 @@ "preact": ">=10" } }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/prettier": { "version": "3.7.4", "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.7.4.tgz", @@ -10944,6 +11667,19 @@ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", "license": "MIT" }, + "node_modules/pump": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", + "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -10960,11 +11696,38 @@ "integrity": "sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==", "license": "MIT" }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dev": true, + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "optional": true, + "peer": true, + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, "node_modules/react": { "version": "19.2.3", "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -10984,7 +11747,6 @@ "version": "19.2.3", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", - "dev": true, "license": "MIT", "dependencies": { "scheduler": "^0.27.0" @@ -11123,7 +11885,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -11197,6 +11958,52 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/rimraf/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", + "optional": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "optional": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "optional": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/rollup": { "version": "4.54.0", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.54.0.tgz", @@ -11238,6 +12045,26 @@ "fsevents": "~2.3.2" } }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "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", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -11284,9 +12111,28 @@ "version": "0.27.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", - "dev": true, "license": "MIT" }, + "node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, "node_modules/scroll-into-view-if-needed": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/scroll-into-view-if-needed/-/scroll-into-view-if-needed-3.1.0.tgz", @@ -11305,6 +12151,16 @@ "semver": "bin/semver.js" } }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, "node_modules/server-only": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/server-only/-/server-only-0.0.1.tgz", @@ -11405,11 +12261,16 @@ "license": "ISC" }, "node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", "license": "ISC", - "optional": true + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } }, "node_modules/simple-concat": { "version": "1.0.1", @@ -11433,13 +12294,29 @@ "optional": true }, "node_modules/simple-get": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.1.tgz", - "integrity": "sha512-CQ5LTKGfCpvE1K0n2us+kuMPbk/q0EKl82s4aheV9oXjFEz6W/Y7oQFVJuU6QG77hRT4Ghb5RURteF5vnWjupA==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "dev": true, + "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", "optional": true, + "peer": true, "dependencies": { - "decompress-response": "^4.2.0", + "decompress-response": "^6.0.0", "once": "^1.3.1", "simple-concat": "^1.0.0" } @@ -11481,7 +12358,6 @@ "version": "0.5.21", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "dev": true, "license": "MIT", "dependencies": { "buffer-from": "^1.0.0", @@ -11492,7 +12368,6 @@ "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -11548,22 +12423,15 @@ } }, "node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", "license": "MIT", "optional": true, "dependencies": { - "safe-buffer": "~5.1.0" + "safe-buffer": "~5.2.0" } }, - "node_modules/string_decoder/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "license": "MIT", - "optional": true - }, "node_modules/string-convert": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/string-convert/-/string-convert-0.2.1.tgz", @@ -11571,17 +12439,20 @@ "license": "MIT" }, "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", "license": "MIT", "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" }, "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/string-width-cjs": { @@ -11599,13 +12470,13 @@ "node": ">=8" } }, - "node_modules/string.prototype.codepointat": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/string.prototype.codepointat/-/string.prototype.codepointat-0.2.1.tgz", - "integrity": "sha512-2cBVCj6I4IOvEnjgO/hWqXjqBGsY+zwPmHl12Srk9IXSZ56Jwwmy+66XO5Iut/oQVR7t5ihYdLB0GMa4alEUcg==", + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "license": "MIT" }, - "node_modules/strip-ansi": { + "node_modules/string-width-cjs/node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", @@ -11617,6 +12488,27 @@ "node": ">=8" } }, + "node_modules/string.prototype.codepointat": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/string.prototype.codepointat/-/string.prototype.codepointat-0.2.1.tgz", + "integrity": "sha512-2cBVCj6I4IOvEnjgO/hWqXjqBGsY+zwPmHl12Srk9IXSZ56Jwwmy+66XO5Iut/oQVR7t5ihYdLB0GMa4alEUcg==", + "license": "MIT" + }, + "node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/strip-ansi-cjs": { "name": "strip-ansi", "version": "6.0.1", @@ -11630,6 +12522,18 @@ "node": ">=8" } }, + "node_modules/strip-ansi/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, "node_modules/strip-indent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", @@ -11643,6 +12547,18 @@ "node": ">=8" } }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/strnum": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.2.tgz", @@ -11729,6 +12645,20 @@ "dev": true, "license": "MIT" }, + "node_modules/tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, "node_modules/tar": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", @@ -11747,19 +12677,60 @@ "node": ">=10" } }, - "node_modules/tar/node_modules/mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "dev": true, "license": "MIT", "optional": true, - "bin": { - "mkdirp": "bin/cmd.js" + "peer": true, + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tar/node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "license": "ISC", + "optional": true, "engines": { "node": ">=10" } }, + "node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "license": "ISC", + "optional": true, + "engines": { + "node": ">=8" + } + }, "node_modules/tar/node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", @@ -11767,6 +12738,67 @@ "license": "ISC", "optional": true }, + "node_modules/terser": { + "version": "5.44.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.44.1.tgz", + "integrity": "sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==", + "license": "BSD-2-Clause", + "peer": true, + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.3.16", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.16.tgz", + "integrity": "sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q==", + "license": "MIT", + "peer": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "serialize-javascript": "^6.0.2", + "terser": "^5.31.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "license": "MIT", + "peer": true + }, "node_modules/tesseract.js": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/tesseract.js/-/tesseract.js-5.1.1.tgz", @@ -12396,6 +13428,21 @@ "@esbuild/win32-x64": "0.27.2" } }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, "node_modules/type-fest": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.7.1.tgz", @@ -13204,6 +14251,20 @@ "integrity": "sha512-zksaLKM2fVlnB5jQQDqKXXwYHLQUVH9es+5TOOHwGOVJOCeRBCiPjwSg+3tN2AdTCzjgli4jijCH290kXb/zWQ==", "license": "Apache-2.0" }, + "node_modules/watchpack": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.0.tgz", + "integrity": "sha512-e6vZvY6xboSwLz2GD36c16+O/2Z6fKvIf4pOXptw2rY9MVwE/TXc6RGqxD3I3x0a28lwBY7DE+76uTPSsBrrCA==", + "license": "MIT", + "peer": true, + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/webidl-conversions": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.0.tgz", @@ -13214,6 +14275,55 @@ "node": ">=20" } }, + "node_modules/webpack": { + "version": "5.104.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.104.1.tgz", + "integrity": "sha512-Qphch25abbMNtekmEGJmeRUhLDbe+QfiWTiqpKYkpCOWY64v9eyl+KRRLmqOFA2AvKPpc9DC6+u2n76tQLBoaA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.8", + "@types/json-schema": "^7.0.15", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.15.0", + "acorn-import-phases": "^1.0.3", + "browserslist": "^4.28.1", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.17.4", + "es-module-lexer": "^2.0.0", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.3.1", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^4.3.3", + "tapable": "^2.3.0", + "terser-webpack-plugin": "^5.3.16", + "watchpack": "^2.4.4", + "webpack-sources": "^3.3.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, "node_modules/webpack-sources": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.3.tgz", @@ -13229,6 +14339,13 @@ "integrity": "sha512-kyDivFZ7ZM0BVOUteVbDFhlRt7Ah/CSPwJdi8hBpkK7QLumUqdLtVfm/PX/hkcnrvr0i77fO5+TjZ94Pe+C9iw==", "license": "MIT" }, + "node_modules/webpack/node_modules/es-module-lexer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", + "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", + "license": "MIT", + "peer": true + }, "node_modules/whatwg-encoding": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", @@ -13308,6 +14425,41 @@ "string-width": "^1.0.2 || 2 || 3 || 4" } }, + "node_modules/wide-align/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT", + "optional": true + }, + "node_modules/wide-align/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "optional": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wide-align/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "optional": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/wrap-ansi": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", @@ -13358,54 +14510,36 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/wrap-ansi/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/wrap-ansi/node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "license": "MIT" }, - "node_modules/wrap-ansi/node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "license": "MIT", "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" }, "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=8" } }, - "node_modules/wrap-ansi/node_modules/strip-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "license": "MIT", "dependencies": { - "ansi-regex": "^6.0.1" + "ansi-regex": "^5.0.1" }, "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" + "node": ">=8" } }, "node_modules/wrappy": { @@ -14136,6 +15270,48 @@ "undici-types": "~6.21.0" } }, + "packages/moonboard-ocr/node_modules/canvas": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/canvas/-/canvas-2.11.2.tgz", + "integrity": "sha512-ItanGBMrmRV7Py2Z+Xhs7cT+FNt5K0vPL4p9EZ/UX/Mu7hFbkxSjKF2KVtPwX7UYWp7dRKnrTvReflgrItJbdw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@mapbox/node-pre-gyp": "^1.0.0", + "nan": "^2.17.0", + "simple-get": "^3.0.3" + }, + "engines": { + "node": ">=6" + } + }, + "packages/moonboard-ocr/node_modules/decompress-response": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz", + "integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==", + "license": "MIT", + "optional": true, + "dependencies": { + "mimic-response": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "packages/moonboard-ocr/node_modules/mimic-response": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz", + "integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "packages/moonboard-ocr/node_modules/semver": { "version": "7.7.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", @@ -14187,6 +15363,18 @@ "@img/sharp-win32-x64": "0.33.5" } }, + "packages/moonboard-ocr/node_modules/simple-get": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.1.tgz", + "integrity": "sha512-CQ5LTKGfCpvE1K0n2us+kuMPbk/q0EKl82s4aheV9oXjFEz6W/Y7oQFVJuU6QG77hRT4Ghb5RURteF5vnWjupA==", + "license": "MIT", + "optional": true, + "dependencies": { + "decompress-response": "^4.2.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, "packages/moonboard-ocr/node_modules/undici-types": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", @@ -14241,6 +15429,7 @@ "iron-session": "^8.0.4", "next": "^16.1.1", "next-auth": "^4.24.13", + "nodemailer": "^7.0.12", "pg": "^8.16.3", "react": "^19.2.3", "react-chartjs-2": "^5.3.1", @@ -14262,6 +15451,7 @@ "@testing-library/react": "^16.3.1", "@types/d3-scale": "^4.0.9", "@types/node": "^25", + "@types/nodemailer": "^7.0.4", "@types/react": "^19", "@types/react-dom": "^19", "@types/uuid": "^11.0.0", diff --git a/packages/db/drizzle/0032_verify_existing_users.sql b/packages/db/drizzle/0032_verify_existing_users.sql new file mode 100644 index 00000000..777ca0bb --- /dev/null +++ b/packages/db/drizzle/0032_verify_existing_users.sql @@ -0,0 +1,10 @@ +-- Mark all existing users with credentials as verified +-- This ensures users who registered before email verification was required can still log in +-- We use created_at as the verification timestamp for existing users + +UPDATE users +SET email_verified = created_at +WHERE email_verified IS NULL + AND id IN ( + SELECT user_id FROM user_credentials + ); diff --git a/packages/db/drizzle/meta/_journal.json b/packages/db/drizzle/meta/_journal.json index a5e12420..be4c745b 100644 --- a/packages/db/drizzle/meta/_journal.json +++ b/packages/db/drizzle/meta/_journal.json @@ -197,6 +197,13 @@ "when": 1767667200000, "tag": "0031_drop_climb_stats_fk", "breakpoints": true + }, + { + "idx": 28, + "version": "7", + "when": 1767753600000, + "tag": "0032_verify_existing_users", + "breakpoints": true } ] } \ No newline at end of file diff --git a/packages/web/app/api/auth/providers-config/route.ts b/packages/web/app/api/auth/providers-config/route.ts new file mode 100644 index 00000000..c91f3d9b --- /dev/null +++ b/packages/web/app/api/auth/providers-config/route.ts @@ -0,0 +1,13 @@ +import { NextResponse } from "next/server"; + +/** + * Returns which OAuth providers are configured. + * This allows the client to show/hide social login buttons appropriately. + */ +export async function GET() { + return NextResponse.json({ + google: !!(process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET), + apple: !!(process.env.APPLE_ID && process.env.APPLE_SECRET), + facebook: !!(process.env.FACEBOOK_CLIENT_ID && process.env.FACEBOOK_CLIENT_SECRET), + }); +} diff --git a/packages/web/app/api/auth/register/route.ts b/packages/web/app/api/auth/register/route.ts index 6164328e..2e6f9b3d 100644 --- a/packages/web/app/api/auth/register/route.ts +++ b/packages/web/app/api/auth/register/route.ts @@ -4,6 +4,8 @@ import * as schema from "@/app/lib/db/schema"; import bcrypt from "bcryptjs"; import { eq } from "drizzle-orm"; import { z } from "zod"; +import { sendVerificationEmail } from "@/app/lib/email/email-service"; +import { checkRateLimit, getClientIp } from "@/app/lib/auth/rate-limiter"; const registerSchema = z.object({ email: z.string().email("Invalid email address"), @@ -16,6 +18,22 @@ const registerSchema = z.object({ export async function POST(request: NextRequest) { try { + // Rate limiting - 10 requests per minute per IP for registration + const clientIp = getClientIp(request); + const rateLimitResult = checkRateLimit(`register:${clientIp}`, 10, 60_000); + + if (rateLimitResult.limited) { + return NextResponse.json( + { error: "Too many requests. Please try again later." }, + { + status: 429, + headers: { + "Retry-After": String(rateLimitResult.retryAfterSeconds), + }, + } + ); + } + const body = await request.json(); // Validate input @@ -54,14 +72,26 @@ export async function POST(request: NextRequest) { // User exists but has no credentials (e.g., OAuth user) // They can add a password to their existing account + // OAuth users are pre-verified by their provider, so no email verification needed const passwordHash = await bcrypt.hash(password, 12); - await db.insert(schema.userCredentials).values({ - userId: existingUser[0].id, - passwordHash, + + await db.transaction(async (tx) => { + await tx.insert(schema.userCredentials).values({ + userId: existingUser[0].id, + passwordHash, + }); + + // Ensure user is marked as verified (OAuth provider already verified their email) + if (!existingUser[0].emailVerified) { + await tx + .update(schema.users) + .set({ emailVerified: new Date() }) + .where(eq(schema.users.id, existingUser[0].id)); + } }); return NextResponse.json( - { message: "Password added to existing account", userId: existingUser[0].id }, + { message: "Password added to existing account" }, { status: 200 } ); } @@ -69,27 +99,70 @@ export async function POST(request: NextRequest) { // Create new user const userId = crypto.randomUUID(); const passwordHash = await bcrypt.hash(password, 12); + const verificationToken = crypto.randomUUID(); + const tokenExpires = new Date(Date.now() + 24 * 60 * 60 * 1000); // 24 hours - // Insert user - await db.insert(schema.users).values({ - id: userId, - email, - name: name || email.split("@")[0], - }); + // Use transaction to ensure user, credentials, profile, and token are created atomically + // If any insert fails, all changes are rolled back + try { + await db.transaction(async (tx) => { + // Insert user (emailVerified is null for unverified accounts) + await tx.insert(schema.users).values({ + id: userId, + email, + name: name || email.split("@")[0], + emailVerified: null, + }); - // Insert credentials - await db.insert(schema.userCredentials).values({ - userId, - passwordHash, - }); + // Insert credentials + await tx.insert(schema.userCredentials).values({ + userId, + passwordHash, + }); - // Create empty profile (user can customize later) - await db.insert(schema.userProfiles).values({ - userId, - }); + // Create empty profile (user can customize later) + await tx.insert(schema.userProfiles).values({ + userId, + }); + + // Insert verification token + await tx.insert(schema.verificationTokens).values({ + identifier: email, + token: verificationToken, + expires: tokenExpires, + }); + }); + } catch (insertError) { + // Handle race condition: another request created this user between our check and insert + // PostgreSQL unique constraint violation code is '23505' + if (insertError && typeof insertError === 'object' && 'code' in insertError && insertError.code === '23505') { + return NextResponse.json( + { error: "An account with this email already exists" }, + { status: 409 } + ); + } + throw insertError; + } + + // Send verification email outside transaction (don't fail registration if email fails) + const baseUrl = process.env.NEXTAUTH_URL || 'http://localhost:3000'; + let emailSent = false; + try { + await sendVerificationEmail(email, verificationToken, baseUrl); + emailSent = true; + } catch (emailError) { + console.error("Failed to send verification email:", emailError); + // User is created, they can use resend functionality + } return NextResponse.json( - { message: "Account created successfully", userId }, + { + message: emailSent + ? "Account created. Please check your email to verify your account." + : "Account created. Please request a new verification email.", + requiresVerification: true, + emailSent, + }, { status: 201 } ); } catch (error) { diff --git a/packages/web/app/api/auth/resend-verification/route.ts b/packages/web/app/api/auth/resend-verification/route.ts new file mode 100644 index 00000000..81012870 --- /dev/null +++ b/packages/web/app/api/auth/resend-verification/route.ts @@ -0,0 +1,114 @@ +import { NextRequest, NextResponse } from "next/server"; +import { getDb } from "@/app/lib/db/db"; +import * as schema from "@/app/lib/db/schema"; +import { eq } from "drizzle-orm"; +import { z } from "zod"; +import { sendVerificationEmail } from "@/app/lib/email/email-service"; +import { checkRateLimit, getClientIp } from "@/app/lib/auth/rate-limiter"; + +// Zod schema for email validation +const resendVerificationSchema = z.object({ + email: z.string().email("Invalid email address"), +}); + +// Minimum response time to prevent timing attacks +// Balances security (covering typical email send variance) with UX +const MIN_RESPONSE_TIME_MS = 1500; + +// Helper to introduce consistent delay to prevent timing attacks +async function consistentDelay(startTime: number): Promise { + const elapsed = Date.now() - startTime; + const remaining = MIN_RESPONSE_TIME_MS - elapsed; + if (remaining > 0) { + await new Promise((resolve) => setTimeout(resolve, remaining)); + } +} + +export async function POST(request: NextRequest) { + const startTime = Date.now(); + const genericMessage = "If an account exists and needs verification, a verification email will be sent"; + + try { + // Rate limiting - 5 requests per minute per IP + const clientIp = getClientIp(request); + const rateLimitResult = checkRateLimit(`resend-verification:${clientIp}`, 5, 60_000); + + if (rateLimitResult.limited) { + await consistentDelay(startTime); + return NextResponse.json( + { error: "Too many requests. Please try again later." }, + { + status: 429, + headers: { + "Retry-After": String(rateLimitResult.retryAfterSeconds), + }, + } + ); + } + + const body = await request.json(); + + // Validate input with Zod + const validationResult = resendVerificationSchema.safeParse(body); + if (!validationResult.success) { + await consistentDelay(startTime); + return NextResponse.json( + { error: validationResult.error.issues[0].message }, + { status: 400 } + ); + } + + const { email } = validationResult.data; + const db = getDb(); + + // Check if user exists and is unverified + const user = await db + .select() + .from(schema.users) + .where(eq(schema.users.email, email)) + .limit(1); + + // Don't reveal user status - return same message for all cases + // Use consistent delay for all paths to prevent timing attacks + if (user.length === 0 || user[0].emailVerified) { + await consistentDelay(startTime); + return NextResponse.json( + { message: genericMessage }, + { status: 200 } + ); + } + + // Generate new token + const token = crypto.randomUUID(); + const expires = new Date(Date.now() + 24 * 60 * 60 * 1000); // 24 hours + + // Delete existing tokens and create new one atomically + await db.transaction(async (tx) => { + await tx + .delete(schema.verificationTokens) + .where(eq(schema.verificationTokens.identifier, email)); + + await tx.insert(schema.verificationTokens).values({ + identifier: email, + token, + expires, + }); + }); + + const baseUrl = process.env.NEXTAUTH_URL || 'http://localhost:3000'; + await sendVerificationEmail(email, token, baseUrl); + + await consistentDelay(startTime); + return NextResponse.json( + { message: genericMessage }, + { status: 200 } + ); + } catch (error) { + console.error("Resend verification error:", error); + await consistentDelay(startTime); + return NextResponse.json( + { error: "Failed to send verification email" }, + { status: 500 } + ); + } +} diff --git a/packages/web/app/api/auth/verify-email/route.ts b/packages/web/app/api/auth/verify-email/route.ts new file mode 100644 index 00000000..b4409d0a --- /dev/null +++ b/packages/web/app/api/auth/verify-email/route.ts @@ -0,0 +1,112 @@ +import { NextRequest, NextResponse } from "next/server"; +import { getDb } from "@/app/lib/db/db"; +import * as schema from "@/app/lib/db/schema"; +import { eq, and } from "drizzle-orm"; +import { checkRateLimit, getClientIp } from "@/app/lib/auth/rate-limiter"; + +export async function GET(request: NextRequest) { + // Rate limiting - 20 attempts per minute per IP + // Higher limit than other endpoints since users may click verification link multiple times + const clientIp = getClientIp(request); + const rateLimitResult = checkRateLimit(`verify-email:${clientIp}`, 20, 60_000); + + if (rateLimitResult.limited) { + return NextResponse.redirect( + new URL("/auth/verify-request?error=TooManyAttempts", request.url) + ); + } + + const searchParams = request.nextUrl.searchParams; + const token = searchParams.get("token"); + const email = searchParams.get("email"); + + if (!token || !email) { + return NextResponse.redirect( + new URL("/auth/verify-request?error=InvalidToken", request.url) + ); + } + + const db = getDb(); + + // Find the verification token + const verificationToken = await db + .select() + .from(schema.verificationTokens) + .where( + and( + eq(schema.verificationTokens.identifier, email), + eq(schema.verificationTokens.token, token) + ) + ) + .limit(1); + + if (verificationToken.length === 0) { + return NextResponse.redirect( + new URL("/auth/verify-request?error=InvalidToken", request.url) + ); + } + + const tokenData = verificationToken[0]; + + // Check if token has expired + if (new Date() > tokenData.expires) { + // Delete expired token + await db + .delete(schema.verificationTokens) + .where( + and( + eq(schema.verificationTokens.identifier, email), + eq(schema.verificationTokens.token, token) + ) + ); + + return NextResponse.redirect( + new URL("/auth/verify-request?error=TokenExpired", request.url) + ); + } + + // Verify user exists before updating + const user = await db + .select() + .from(schema.users) + .where(eq(schema.users.email, email)) + .limit(1); + + if (user.length === 0) { + // Token exists but user doesn't - cleanup the orphan token + await db + .delete(schema.verificationTokens) + .where( + and( + eq(schema.verificationTokens.identifier, email), + eq(schema.verificationTokens.token, token) + ) + ); + + return NextResponse.redirect( + new URL("/auth/verify-request?error=InvalidToken", request.url) + ); + } + + // Update user and delete token atomically + await db.transaction(async (tx) => { + await tx + .update(schema.users) + .set({ emailVerified: new Date() }) + .where(eq(schema.users.email, email)); + + await tx + .delete(schema.verificationTokens) + .where( + and( + eq(schema.verificationTokens.identifier, email), + eq(schema.verificationTokens.token, token) + ) + ); + }); + + // Redirect to login with success message + return NextResponse.redirect( + new URL("/auth/login?verified=true", request.url) + ); +} diff --git a/packages/web/app/auth/error/auth-error-content.tsx b/packages/web/app/auth/error/auth-error-content.tsx new file mode 100644 index 00000000..22551ee3 --- /dev/null +++ b/packages/web/app/auth/error/auth-error-content.tsx @@ -0,0 +1,86 @@ +'use client'; + +import React from 'react'; +import { Layout, Card, Typography, Button, Space, Alert } from 'antd'; +import { CloseCircleOutlined } from '@ant-design/icons'; +import { useSearchParams } from 'next/navigation'; +import Logo from '@/app/components/brand/logo'; +import BackButton from '@/app/components/back-button'; +import { themeTokens } from '@/app/theme/theme-config'; + +const { Content, Header } = Layout; +const { Title } = Typography; + +export default function AuthErrorContent() { + const searchParams = useSearchParams(); + const error = searchParams.get('error'); + + const getErrorMessage = () => { + switch (error) { + case 'Configuration': + return 'There is a problem with the server configuration.'; + case 'AccessDenied': + return 'Access denied. You do not have permission to sign in.'; + case 'Verification': + return 'The verification link has expired or is invalid.'; + case 'OAuthSignin': + return 'Error starting the sign-in flow. Please try again.'; + case 'OAuthCallback': + return 'Error completing the sign-in. Please try again.'; + case 'OAuthCreateAccount': + return 'Could not create an account with this provider.'; + case 'EmailCreateAccount': + return 'Could not create an email account.'; + case 'Callback': + return 'Error in the authentication callback.'; + case 'OAuthAccountNotLinked': + return 'This email is already associated with another account. Please sign in using your original method.'; + case 'SessionRequired': + return 'You must be signed in to access this page.'; + default: + return 'An unexpected authentication error occurred.'; + } + }; + + return ( + +
+ + + + Authentication Error + +
+ + + + + + Authentication Error + + + + + +
+ ); +} diff --git a/packages/web/app/auth/error/page.tsx b/packages/web/app/auth/error/page.tsx new file mode 100644 index 00000000..e91cfc42 --- /dev/null +++ b/packages/web/app/auth/error/page.tsx @@ -0,0 +1,16 @@ +import React, { Suspense } from 'react'; +import { Metadata } from 'next'; +import AuthErrorContent from './auth-error-content'; + +export const metadata: Metadata = { + title: 'Authentication Error | Boardsesh', + description: 'An error occurred during authentication', +}; + +export default function AuthErrorPage() { + return ( + + + + ); +} diff --git a/packages/web/app/auth/login/auth-page-content.tsx b/packages/web/app/auth/login/auth-page-content.tsx index 373a8b3a..0d85c821 100644 --- a/packages/web/app/auth/login/auth-page-content.tsx +++ b/packages/web/app/auth/login/auth-page-content.tsx @@ -7,6 +7,8 @@ import { signIn, useSession } from 'next-auth/react'; import { useRouter, useSearchParams } from 'next/navigation'; import Logo from '@/app/components/brand/logo'; import BackButton from '@/app/components/back-button'; +import SocialLoginButtons from '@/app/components/auth/social-login-buttons'; +import { themeTokens } from '@/app/theme/theme-config'; const { Content, Header } = Layout; const { Title, Text } = Typography; @@ -24,6 +26,8 @@ export default function AuthPageContent() { const [registerLoading, setRegisterLoading] = useState(false); const [activeTab, setActiveTab] = useState('login'); + const verified = searchParams.get('verified'); + // Show error message from NextAuth useEffect(() => { if (error) { @@ -35,6 +39,13 @@ export default function AuthPageContent() { } }, [error]); + // Show success message when email is verified + useEffect(() => { + if (verified === 'true') { + message.success('Email verified! You can now log in.'); + } + }, [verified]); + // Redirect if already authenticated useEffect(() => { if (status === 'authenticated') { @@ -91,9 +102,17 @@ export default function AuthPageContent() { return; } - message.success('Account created! Logging you in...'); + // Check if email verification is required + if (data.requiresVerification) { + message.info('Please check your email to verify your account'); + setActiveTab('login'); + loginForm.setFieldValue('email', values.email); + return; + } + + // Fallback for accounts that don't require verification (e.g., adding password to OAuth account) + message.success('Account updated! Logging you in...'); - // Auto-login after registration const loginResult = await signIn('credentials', { email: values.email, password: values.password, @@ -103,10 +122,9 @@ export default function AuthPageContent() { if (loginResult?.ok) { router.push(callbackUrl); } else { - // If auto-login fails, switch to login tab setActiveTab('login'); loginForm.setFieldValue('email', values.email); - message.info('Please log in with your new account'); + message.info('Please log in with your account'); } } catch (error) { console.error('Registration error:', error); @@ -222,15 +240,15 @@ export default function AuthPageContent() { ]; return ( - +
@@ -261,35 +279,7 @@ export default function AuthPageContent() { or - + diff --git a/packages/web/app/auth/verify-request/page.tsx b/packages/web/app/auth/verify-request/page.tsx new file mode 100644 index 00000000..d95338fd --- /dev/null +++ b/packages/web/app/auth/verify-request/page.tsx @@ -0,0 +1,16 @@ +import React, { Suspense } from 'react'; +import { Metadata } from 'next'; +import VerifyRequestContent from './verify-request-content'; + +export const metadata: Metadata = { + title: 'Verify Email | Boardsesh', + description: 'Verify your email address', +}; + +export default function VerifyRequestPage() { + return ( + + + + ); +} diff --git a/packages/web/app/auth/verify-request/verify-request-content.tsx b/packages/web/app/auth/verify-request/verify-request-content.tsx new file mode 100644 index 00000000..8548c1ca --- /dev/null +++ b/packages/web/app/auth/verify-request/verify-request-content.tsx @@ -0,0 +1,141 @@ +'use client'; + +import React, { useState } from 'react'; +import { Layout, Card, Typography, Button, Space, Alert, Input, Form, message } from 'antd'; +import { MailOutlined, CloseCircleOutlined } from '@ant-design/icons'; +import { useSearchParams } from 'next/navigation'; +import Logo from '@/app/components/brand/logo'; +import BackButton from '@/app/components/back-button'; +import { themeTokens } from '@/app/theme/theme-config'; + +const { Content, Header } = Layout; +const { Title, Text, Paragraph } = Typography; + +export default function VerifyRequestContent() { + const searchParams = useSearchParams(); + const error = searchParams.get('error'); + const [resendLoading, setResendLoading] = useState(false); + const [form] = Form.useForm(); + + const getErrorMessage = () => { + switch (error) { + case 'EmailNotVerified': + return 'Please verify your email before signing in.'; + case 'InvalidToken': + return 'The verification link is invalid. Please request a new one.'; + case 'TokenExpired': + return 'The verification link has expired. Please request a new one.'; + case 'TooManyAttempts': + return 'Too many verification attempts. Please wait a minute and try again.'; + default: + return null; + } + }; + + const handleResend = async () => { + try { + const values = await form.validateFields(); + setResendLoading(true); + + const response = await fetch('/api/auth/resend-verification', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email: values.email }), + }); + + const data = await response.json(); + + if (response.ok) { + message.success('Verification email sent! Check your inbox.'); + } else { + message.error(data.error || 'Failed to send verification email'); + } + } catch (err) { + console.error('Resend error:', err); + } finally { + setResendLoading(false); + } + }; + + const errorMessage = getErrorMessage(); + + return ( + +
+ + + + Email Verification + +
+ + + + + {errorMessage ? ( + <> + + + + ) : ( + <> + + Check your email + + We sent you a verification link. Click the link in your email to verify your account. + + + )} + +
+ + } + placeholder="Enter your email to resend" + size="large" + /> + + + +
+ + +
+
+
+
+ ); +} diff --git a/packages/web/app/components/auth/auth-modal.tsx b/packages/web/app/components/auth/auth-modal.tsx index f1d0e47b..596159fb 100644 --- a/packages/web/app/components/auth/auth-modal.tsx +++ b/packages/web/app/components/auth/auth-modal.tsx @@ -4,6 +4,7 @@ import React, { useState } from 'react'; import { Modal, Form, Input, Button, Tabs, Typography, Divider, message, Space } from 'antd'; import { UserOutlined, LockOutlined, MailOutlined, HeartFilled } from '@ant-design/icons'; import { signIn } from 'next-auth/react'; +import SocialLoginButtons from '@/app/components/auth/social-login-buttons'; const { Text } = Typography; @@ -78,7 +79,17 @@ export default function AuthModal({ return; } - message.success('Account created! Logging you in...'); + // Check if email verification is required + if (data.requiresVerification) { + message.info('Please check your email to verify your account'); + setActiveTab('login'); + loginForm.setFieldValue('email', values.email); + registerForm.resetFields(); + return; + } + + // Fallback for accounts that don't require verification (e.g., adding password to OAuth account) + message.success('Account updated! Logging you in...'); const loginResult = await signIn('credentials', { email: values.email, @@ -93,7 +104,7 @@ export default function AuthModal({ } else { setActiveTab('login'); loginForm.setFieldValue('email', values.email); - message.info('Please log in with your new account'); + message.info('Please log in with your account'); } } catch (error) { console.error('Registration error:', error); @@ -227,35 +238,7 @@ export default function AuthModal({ or - + ); diff --git a/packages/web/app/components/auth/social-login-buttons.tsx b/packages/web/app/components/auth/social-login-buttons.tsx new file mode 100644 index 00000000..4a0328ac --- /dev/null +++ b/packages/web/app/components/auth/social-login-buttons.tsx @@ -0,0 +1,140 @@ +'use client'; + +import React, { useEffect, useState } from 'react'; +import { Button, Space, Skeleton } from 'antd'; +import { signIn } from 'next-auth/react'; +import { themeTokens } from '@/app/theme/theme-config'; + +// Note: OAuth provider icons and button colors use brand-specific colors +// per Google/Apple/Facebook brand guidelines, not design system tokens + +const GoogleIcon = () => ( + + + + + + +); + +const AppleIcon = () => ( + + + +); + +const FacebookIcon = () => ( + + + +); + +type ProvidersConfig = { + google: boolean; + apple: boolean; + facebook: boolean; +}; + +type SocialLoginButtonsProps = { + callbackUrl?: string; + disabled?: boolean; +}; + +export default function SocialLoginButtons({ + callbackUrl = '/', + disabled = false, +}: SocialLoginButtonsProps) { + const [providers, setProviders] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + fetch('/api/auth/providers-config') + .then((res) => res.json()) + .then((data) => { + setProviders(data); + setLoading(false); + }) + .catch(() => { + // On error, don't show any OAuth buttons + setProviders({ google: false, apple: false, facebook: false }); + setLoading(false); + }); + }, []); + + const handleSocialSignIn = (provider: string) => { + signIn(provider, { callbackUrl }); + }; + + // Don't render anything if no providers are configured + const hasAnyProvider = providers && (providers.google || providers.apple || providers.facebook); + + // Show single skeleton while loading to minimize layout shift + // (we don't know how many providers are configured yet) + if (loading) { + return ; + } + + if (!hasAnyProvider) { + return null; + } + + // Apple button needs custom colors per brand guidelines + const appleButtonStyles = { + backgroundColor: themeTokens.neutral[900], + color: themeTokens.semantic.surface, + borderColor: themeTokens.neutral[900], + }; + + return ( + + {providers.google && ( + + )} + + {providers.apple && ( + + )} + + {providers.facebook && ( + + )} + + ); +} diff --git a/packages/web/app/lib/auth/__tests__/rate-limiter.test.ts b/packages/web/app/lib/auth/__tests__/rate-limiter.test.ts new file mode 100644 index 00000000..07014923 --- /dev/null +++ b/packages/web/app/lib/auth/__tests__/rate-limiter.test.ts @@ -0,0 +1,153 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { checkRateLimit, getClientIp } from '../rate-limiter'; + +describe('rate-limiter', () => { + beforeEach(() => { + // Reset the module to clear the in-memory store between tests + vi.resetModules(); + }); + + describe('checkRateLimit', () => { + it('should allow requests under the limit', async () => { + const identifier = `test-${Date.now()}-1`; + + // First request should be allowed + const result1 = checkRateLimit(identifier, 5, 60000); + expect(result1.limited).toBe(false); + expect(result1.retryAfterSeconds).toBe(0); + + // Second request should also be allowed + const result2 = checkRateLimit(identifier, 5, 60000); + expect(result2.limited).toBe(false); + }); + + it('should block requests when limit is exceeded', async () => { + const identifier = `test-${Date.now()}-2`; + const maxRequests = 3; + + // Make requests up to the limit + for (let i = 0; i < maxRequests; i++) { + const result = checkRateLimit(identifier, maxRequests, 60000); + expect(result.limited).toBe(false); + } + + // The next request should be blocked + const blockedResult = checkRateLimit(identifier, maxRequests, 60000); + expect(blockedResult.limited).toBe(true); + expect(blockedResult.retryAfterSeconds).toBeGreaterThan(0); + }); + + it('should use different limits for different identifiers', async () => { + const identifier1 = `test-${Date.now()}-3a`; + const identifier2 = `test-${Date.now()}-3b`; + + // Exhaust limit for identifier1 + for (let i = 0; i < 2; i++) { + checkRateLimit(identifier1, 2, 60000); + } + + // identifier1 should be blocked + const result1 = checkRateLimit(identifier1, 2, 60000); + expect(result1.limited).toBe(true); + + // identifier2 should still be allowed + const result2 = checkRateLimit(identifier2, 2, 60000); + expect(result2.limited).toBe(false); + }); + + it('should reset after window expires', async () => { + const identifier = `test-${Date.now()}-4`; + const shortWindow = 100; // 100ms window for testing + + // Exhaust the limit + for (let i = 0; i < 2; i++) { + checkRateLimit(identifier, 2, shortWindow); + } + + // Should be blocked + const blockedResult = checkRateLimit(identifier, 2, shortWindow); + expect(blockedResult.limited).toBe(true); + + // Wait for window to expire + await new Promise((resolve) => setTimeout(resolve, shortWindow + 50)); + + // Should be allowed again + const allowedResult = checkRateLimit(identifier, 2, shortWindow); + expect(allowedResult.limited).toBe(false); + }); + + it('should use default values when not specified', async () => { + const identifier = `test-${Date.now()}-5`; + + // Should use defaults (5 requests, 60 seconds) + const result = checkRateLimit(identifier); + expect(result.limited).toBe(false); + expect(result.retryAfterSeconds).toBe(0); + }); + }); + + describe('getClientIp', () => { + it('should extract IP from x-forwarded-for header', () => { + const request = new Request('http://localhost', { + headers: { + 'x-forwarded-for': '192.168.1.1, 10.0.0.1', + }, + }); + + const ip = getClientIp(request); + expect(ip).toBe('192.168.1.1'); + }); + + it('should extract IP from single x-forwarded-for value', () => { + const request = new Request('http://localhost', { + headers: { + 'x-forwarded-for': '203.0.113.195', + }, + }); + + const ip = getClientIp(request); + expect(ip).toBe('203.0.113.195'); + }); + + it('should use x-real-ip when x-forwarded-for is not present', () => { + const request = new Request('http://localhost', { + headers: { + 'x-real-ip': '10.0.0.1', + }, + }); + + const ip = getClientIp(request); + expect(ip).toBe('10.0.0.1'); + }); + + it('should prefer x-forwarded-for over x-real-ip', () => { + const request = new Request('http://localhost', { + headers: { + 'x-forwarded-for': '192.168.1.1', + 'x-real-ip': '10.0.0.1', + }, + }); + + const ip = getClientIp(request); + expect(ip).toBe('192.168.1.1'); + }); + + it('should return "unknown" when no IP headers are present', () => { + const request = new Request('http://localhost'); + + const ip = getClientIp(request); + expect(ip).toBe('unknown'); + }); + + it('should trim whitespace from IP addresses', () => { + const request = new Request('http://localhost', { + headers: { + 'x-forwarded-for': ' 192.168.1.1 ', + }, + }); + + const ip = getClientIp(request); + expect(ip).toBe('192.168.1.1'); + }); + }); +}); diff --git a/packages/web/app/lib/auth/auth-options.ts b/packages/web/app/lib/auth/auth-options.ts index 6c9451a8..1b6879a7 100644 --- a/packages/web/app/lib/auth/auth-options.ts +++ b/packages/web/app/lib/auth/auth-options.ts @@ -1,25 +1,51 @@ import { NextAuthOptions } from "next-auth"; import { DrizzleAdapter } from "@auth/drizzle-adapter"; import GoogleProvider from "next-auth/providers/google"; +import AppleProvider from "next-auth/providers/apple"; +import FacebookProvider from "next-auth/providers/facebook"; import CredentialsProvider from "next-auth/providers/credentials"; import { getDb } from "@/app/lib/db/db"; import * as schema from "@/app/lib/db/schema"; import { eq } from "drizzle-orm"; import bcrypt from "bcryptjs"; -export const authOptions: NextAuthOptions = { - adapter: DrizzleAdapter(getDb(), { - usersTable: schema.users, - accountsTable: schema.accounts, - sessionsTable: schema.sessions, - verificationTokensTable: schema.verificationTokens, - }), - providers: [ +// Build providers array conditionally based on available env vars +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const providers: any[] = []; + +// Only add Google provider if credentials are configured +if (process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET) { + providers.push( GoogleProvider({ - clientId: process.env.GOOGLE_CLIENT_ID!, - clientSecret: process.env.GOOGLE_CLIENT_SECRET!, - }), - CredentialsProvider({ + clientId: process.env.GOOGLE_CLIENT_ID, + clientSecret: process.env.GOOGLE_CLIENT_SECRET, + }) + ); +} + +// Only add Apple provider if credentials are configured +if (process.env.APPLE_ID && process.env.APPLE_SECRET) { + providers.push( + AppleProvider({ + clientId: process.env.APPLE_ID, + clientSecret: process.env.APPLE_SECRET, + }) + ); +} + +// Only add Facebook provider if credentials are configured +if (process.env.FACEBOOK_CLIENT_ID && process.env.FACEBOOK_CLIENT_SECRET) { + providers.push( + FacebookProvider({ + clientId: process.env.FACEBOOK_CLIENT_ID, + clientSecret: process.env.FACEBOOK_CLIENT_SECRET, + }) + ); +} + +// Always add credentials provider +providers.push( + CredentialsProvider({ name: "Email", credentials: { email: { label: "Email", type: "email", placeholder: "your@email.com" }, @@ -74,15 +100,51 @@ export const authOptions: NextAuthOptions = { image: user.image, }; }, - }), - ], + }) +); + +export const authOptions: NextAuthOptions = { + adapter: DrizzleAdapter(getDb(), { + usersTable: schema.users, + accountsTable: schema.accounts, + sessionsTable: schema.sessions, + verificationTokensTable: schema.verificationTokens, + }), + providers, session: { strategy: "jwt", // Required for credentials provider }, pages: { signIn: "/auth/login", + verifyRequest: "/auth/verify-request", + error: "/auth/error", }, callbacks: { + async signIn({ user, account }) { + // OAuth providers - allow sign in (emails are pre-verified by provider) + if (account?.provider !== "credentials") { + return true; + } + + // For credentials, check if email is verified + if (!user.email) { + return false; + } + + const db = getDb(); + const existingUser = await db + .select() + .from(schema.users) + .where(eq(schema.users.email, user.email)) + .limit(1); + + if (existingUser.length > 0 && !existingUser[0].emailVerified) { + // Redirect to verification page with error + return "/auth/verify-request?error=EmailNotVerified"; + } + + return true; + }, async session({ session, token }) { // Include user ID in session from JWT if (session?.user && token?.sub) { @@ -98,4 +160,15 @@ export const authOptions: NextAuthOptions = { return token; }, }, + events: { + async createUser({ user }) { + // Create profile for new OAuth users + if (user.id) { + const db = getDb(); + await db.insert(schema.userProfiles).values({ + userId: user.id, + }).onConflictDoNothing(); + } + }, + }, }; diff --git a/packages/web/app/lib/auth/rate-limiter.ts b/packages/web/app/lib/auth/rate-limiter.ts new file mode 100644 index 00000000..dd4eab8b --- /dev/null +++ b/packages/web/app/lib/auth/rate-limiter.ts @@ -0,0 +1,98 @@ +/** + * Rate limiter for API endpoints. + * + * IMPORTANT: This uses in-memory storage which has limitations: + * - In serverless environments (Vercel), each function instance has its own memory + * - Rate limits are not shared across instances + * - This provides best-effort protection, not guaranteed rate limiting + * + * For production deployments requiring strict rate limiting, consider: + * - Redis (add ioredis to dependencies and use REDIS_URL) + * - Vercel KV (@vercel/kv) + * - Upstash Redis (@upstash/redis) + * + * The current implementation still provides value by: + * - Limiting rapid-fire requests within a single function instance + * - Deterring casual abuse + * - Providing a framework for upgrading to distributed storage + */ + +// In-memory store for rate limiting +const memoryStore = new Map(); + +// Default limits for email endpoints +const DEFAULT_WINDOW_MS = 60_000; // 1 minute +const DEFAULT_MAX_REQUESTS = 5; + +/** + * Check if a request should be rate limited. + * @param identifier - Unique identifier for the rate limit bucket (e.g., "register:192.168.1.1") + * @param maxRequests - Maximum requests allowed in the time window + * @param windowMs - Time window in milliseconds + * @returns Object with limited flag and retry-after seconds + */ +export function checkRateLimit( + identifier: string, + maxRequests: number = DEFAULT_MAX_REQUESTS, + windowMs: number = DEFAULT_WINDOW_MS +): { limited: boolean; retryAfterSeconds: number } { + const now = Date.now(); + const entry = memoryStore.get(identifier); + + // If no entry or window expired, create new entry + if (!entry || now > entry.resetAt) { + memoryStore.set(identifier, { + count: 1, + resetAt: now + windowMs, + }); + return { limited: false, retryAfterSeconds: 0 }; + } + + // Check if limit exceeded + if (entry.count >= maxRequests) { + const retryAfterSeconds = Math.ceil((entry.resetAt - now) / 1000); + return { limited: true, retryAfterSeconds }; + } + + // Increment counter + entry.count++; + return { limited: false, retryAfterSeconds: 0 }; +} + +/** + * Get client IP address from request headers. + * Handles common proxy headers (x-forwarded-for, x-real-ip). + */ +export function getClientIp(request: Request): string { + // Check x-forwarded-for first (most common proxy header) + const forwarded = request.headers.get('x-forwarded-for'); + if (forwarded) { + // x-forwarded-for can contain multiple IPs; the first is the original client + return forwarded.split(',')[0].trim(); + } + + // Check x-real-ip (used by some proxies like nginx) + const realIp = request.headers.get('x-real-ip'); + if (realIp) { + return realIp.trim(); + } + + // Fallback - still rate limit but with a shared bucket + return 'unknown'; +} + +// Cleanup expired entries periodically to prevent memory leaks +// Uses unref() to allow the process to exit even with the interval running +const cleanupInterval = setInterval(() => { + const now = Date.now(); + for (const [key, entry] of memoryStore) { + if (now > entry.resetAt) { + memoryStore.delete(key); + } + } +}, 60_000); + +// Allow the Node.js process to exit even if this interval is pending +if (typeof cleanupInterval.unref === 'function') { + cleanupInterval.unref(); +} diff --git a/packages/web/app/lib/email/email-service.ts b/packages/web/app/lib/email/email-service.ts new file mode 100644 index 00000000..40380558 --- /dev/null +++ b/packages/web/app/lib/email/email-service.ts @@ -0,0 +1,98 @@ +import nodemailer, { Transporter } from 'nodemailer'; +import { z } from 'zod'; +import { themeTokens } from '@/app/theme/theme-config'; + +// Email validation schema - validates format before using in URLs +const emailSchema = z.string().email(); + +// Email color palette derived from design tokens +// These are inline styles for HTML emails, so we extract the actual hex values +const emailColors = { + primary: themeTokens.colors.primary, // Cyan primary + textPrimary: themeTokens.neutral[800], // Dark text + textSecondary: themeTokens.neutral[500], // Medium text + textMuted: themeTokens.neutral[400], // Light text + border: themeTokens.neutral[200], // Light border +} as const; + +// Lazy-loaded transporter to avoid initialization at module load +let transporter: Transporter | null = null; + +function getTransporter(): Transporter { + if (!transporter) { + if (!process.env.SMTP_USER || !process.env.SMTP_PASSWORD) { + throw new Error('SMTP credentials not configured. Set SMTP_USER and SMTP_PASSWORD environment variables.'); + } + + transporter = nodemailer.createTransport({ + host: process.env.SMTP_HOST || 'smtp.fastmail.com', + port: parseInt(process.env.SMTP_PORT || '465', 10), + secure: true, // true for 465, false for 587 + auth: { + user: process.env.SMTP_USER, + pass: process.env.SMTP_PASSWORD, + }, + }); + } + return transporter; +} + +// HTML escape function to prevent XSS +function escapeHtml(text: string): string { + const htmlEscapes: Record = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''', + }; + return text.replace(/[&<>"']/g, (char) => htmlEscapes[char]); +} + +export async function sendVerificationEmail( + email: string, + token: string, + baseUrl: string +): Promise { + // Validate email format before using in URL to prevent injection + const validatedEmail = emailSchema.parse(email); + + const verifyUrl = `${baseUrl}/api/auth/verify-email?token=${token}&email=${encodeURIComponent(validatedEmail)}`; + const safeVerifyUrl = escapeHtml(verifyUrl); + + await getTransporter().sendMail({ + from: process.env.EMAIL_FROM || process.env.SMTP_USER, + to: validatedEmail, + subject: 'Verify your Boardsesh email', + html: ` +
+

Welcome to Boardsesh!

+

+ Please verify your email address by clicking the button below: +

+ Verify Email +

+ Or copy and paste this link into your browser: +

+

+ ${safeVerifyUrl} +

+
+

+ This link expires in 24 hours. If you didn't create a Boardsesh account, you can safely ignore this email. +

+
+ `, + text: `Welcome to Boardsesh!\n\nPlease verify your email address by clicking this link:\n\n${verifyUrl}\n\nThis link expires in 24 hours.\n\nIf you didn't create a Boardsesh account, you can safely ignore this email.`, + }); +} diff --git a/packages/web/package.json b/packages/web/package.json index 47301e11..c7cf3d39 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -52,6 +52,7 @@ "iron-session": "^8.0.4", "next": "^16.1.1", "next-auth": "^4.24.13", + "nodemailer": "^7.0.12", "pg": "^8.16.3", "react": "^19.2.3", "react-chartjs-2": "^5.3.1", @@ -73,6 +74,7 @@ "@testing-library/react": "^16.3.1", "@types/d3-scale": "^4.0.9", "@types/node": "^25", + "@types/nodemailer": "^7.0.4", "@types/react": "^19", "@types/react-dom": "^19", "@types/uuid": "^11.0.0",