diff --git a/.env.example b/.env.example index 167b8c15..a18c0aea 100644 --- a/.env.example +++ b/.env.example @@ -35,3 +35,8 @@ GITHUB_APP_STATE_SECRET= # Optional: For client-side API calls NEXT_PUBLIC_API_URL=http://localhost:3000 + + +# Rate Limiting (In-Memory) +RATE_LIMIT_WINDOW_MS=900000 # 15 minutes in milliseconds +RATE_LIMIT_MAX_REQUESTS=5 # Max attempts per window \ No newline at end of file diff --git a/GETTING_STARTED.md b/GETTING_STARTED.md index 02eb2376..2e41f582 100644 --- a/GETTING_STARTED.md +++ b/GETTING_STARTED.md @@ -4,7 +4,7 @@ This is the Next.js version of GitVerse, migrated from the Vite + React version ## Prerequisites -- Node.js 18+ installed +- Node.js 22.x installed (see [Supported Node Version](README.md#supported-node-version)) - PostgreSQL database (NeonDB recommended) - Google Gemini AI API key @@ -28,13 +28,13 @@ cp .env.example .env.local Edit `.env.local` and fill in your values: ```env -# Database - Get from https://neon.tech +# Database - Get from [https://neon.tech](https://neon.tech) DATABASE_URL=postgresql://user:password@host/database?sslmode=require&schema=public # JWT Secret - Generate a secure random string JWT_SECRET=your-super-secret-jwt-key-change-this-in-production -# Gemini AI - Get from https://makersuite.google.com/app/apikey +# Gemini AI - Get from [https://makersuite.google.com/app/apikey](https://makersuite.google.com/app/apikey) GEMINI_API_KEY=your_gemini_api_key_here # NextAuth (required for Google login) @@ -47,7 +47,7 @@ GOOGLE_CLIENT_SECRET=your-google-client-secret # Optional: Base URL for API calls # Leave unset to use same-origin `/api/...` (recommended). -# If you set it, avoid a trailing slash (e.g. https://example.com, not https://example.com/) +# If you set it, avoid a trailing slash (e.g. [https://example.com](https://example.com), not [https://example.com/](https://example.com/)) # NEXT_PUBLIC_API_URL=http://localhost:3000 ``` @@ -108,6 +108,8 @@ Open [http://localhost:3000](http://localhost:3000) in your browser. ### Running the App +Note on Background Tasks: For standard local development, background tasks are handled seamlessly by Next.js API routes. The legacy worker script is optional and generally not required. + ```bash npm run dev # Development server npm run build # Production build @@ -115,6 +117,14 @@ npm start # Production server npm run lint # Lint code ``` +### Legacy Worker (Optional) + +If you are testing specific background workflows locally that mimic a custom non-serverless deployment, you can optionally run the worker: + +```bash +npm run worker # Run background tasks (Legacy / Optional) +``` + ### Database Management ```bash @@ -166,12 +176,15 @@ All endpoints are prefixed with `/api`: ### Vercel (Recommended) +Vercel automatically handles background processes via serverless functions, so the worker script is not needed for this deployment method. + 1. Push code to GitHub 2. Import in Vercel dashboard 3. Add environment variables 4. Deploy automatically ### Docker +If self-hosting via Docker, run the primary application container. If your deployment uses queue-based background processing, run the legacy worker as a separate process/container. ```bash docker build -t gitverse-nextjs . @@ -179,10 +192,15 @@ docker run -p 3000:3000 gitverse-nextjs ``` ### Manual Deployment +For deployments on custom servers (e.g., EC2, VPS) where serverless functions aren't available, run the app build. If queue-based background processing is enabled, also run the worker script (for example via PM2/systemd). ```bash npm run build npm start + +# In a separate terminal or using a process manager like PM2: +npm run worker + ``` ## Troubleshooting diff --git a/GIT_GRAPH_SETUP.md b/GIT_GRAPH_SETUP.md index e7ed0c1b..ce84ea24 100644 --- a/GIT_GRAPH_SETUP.md +++ b/GIT_GRAPH_SETUP.md @@ -79,3 +79,35 @@ The graph algorithm uses these parent relationships to: - Detect when branches merge - Position commits in the correct columns - Show branch splits and joins + +## Security & Access Control + +To protect private repository data and analysis results, GitVerse enforces strict ownership and caching rules across all user-scoped API endpoints. + +### Ownership Enforcement Strategy + +Before returning sensitive data (e.g., repository insights or commit history), the backend middleware must validate that the authenticated user actually owns the requested resource. +- **Authorized:** If `repository.userId === user.id`, the data is safely returned. +- **Unauthorized (404/403):** If a user attempts to access another user's private data, the API will intentionally return a generic `404 Not Found` or `403 Forbidden` to prevent leaking the existence or metadata of private resources. + +### Cache-Control: `no-store` + +All endpoints returning private or user-scoped data explicitly define HTTP caching directives to prevent accidental data leakage via proxies or browsers. + +**Example Response Header:** +```http +Cache-Control: no-store, no-cache, must-revalidate, proxy-revalidate +``` + +This guarantees that: +- Browsers never cache sensitive repository data. +- CDNs or edge proxies do not mistakenly serve one user's data to another. +- API requests always hit the origin server to re-verify authentication and ownership. + +### Best Practices for Protected Endpoints + +When developing new GitVerse API routes, always adhere to the following pattern: +1. Extract and validate the session `user`. +2. Fetch the resource and perform an ownership check. +3. Return `404 Not Found` if the ownership check fails. +4. Append `Cache-Control: no-store` to the `NextResponse` before returning. diff --git a/GOOGLE_OAUTH_SETUP.md b/GOOGLE_OAUTH_SETUP.md index 59ac0e79..fa79476a 100644 --- a/GOOGLE_OAUTH_SETUP.md +++ b/GOOGLE_OAUTH_SETUP.md @@ -120,6 +120,37 @@ When deploying to production: 3. Ensure all environment variables are set in your production environment 4. Run the database migration in production +## Provider Rate Limits & Retry Strategies + +When GitVerse integrates with third-party providers (like GitHub or Google), it natively handles rate limiting and transient failures to ensure robust operation. + +### Rate Limit Behavior (HTTP 429) + +If a provider returns a `429 Too Many Requests` response, GitVerse will safely catch the error and surface a standardized response to the client. The frontend will typically display a message like: +`"Rate limit reached. Please retry after X seconds."` + +GitVerse extracts the `retryAfter` or reset timing directly from the provider's HTTP headers and propagates it safely without exposing internal API responses. + +### Retry with Exponential Backoff + +For transient network failures or safe server errors (e.g., `502`, `503`, `504`), GitVerse employs an automatic exponential backoff mechanism. + +**Retry Flow Example:** +- Attempt 1: Immediate Execution +- Attempt 2: 1s delay +- Attempt 3: 2s delay +- Attempt 4: 4s delay + +This prevents infinite retry loops and respects provider rate limits while improving overall system reliability. + +## Secure Logging Practices + +To protect sensitive information, GitVerse enforces strict token sanitization requirements across all provider integrations: + +1. **Token Redaction:** Never log raw OAuth access tokens, `Authorization` headers, or provider credentials. +2. **Error Sanitization:** If an HTTP fetch fails, the underlying `AxiosError` or `fetch` error is sanitized to strip out sensitive config and request headers before it reaches `console.error`. +3. **Safe Debugging:** Logs will output the HTTP status code, safe message strings, and endpoint paths (excluding query string secrets) to preserve debuggability without compromising security. + ## Troubleshooting ### Error: "redirect_uri_mismatch" diff --git a/README.md b/README.md index 7f4575b6..23d73802 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,19 @@ Paste a repo → GitVerse builds a visual map + AI onboarding so contributors ca - Ask AI questions about files, folders, and architecture - Generate analysis jobs and track progress +## Getting Started + +The canonical onboarding and setup guide is: + +- [GETTING_STARTED.md](./GETTING_STARTED.md) + +Additional setup docs: + +- [START_HERE.md](./START_HERE.md) +- [GOOGLE_OAUTH_SETUP.md](./GOOGLE_OAUTH_SETUP.md) +- [GOOGLE_OAUTH_INTEGRATION.md](./GOOGLE_OAUTH_INTEGRATION.md) +- [QUICKSTART_OAUTH.md](./QUICKSTART_OAUTH.md) + ## Quickstart (local dev) ```bash @@ -211,16 +224,24 @@ const loadMore = async () => { ### Vercel (Recommended) -1. Push your code to GitHub -2. Import project in Vercel -3. Add environment variables in Vercel dashboard -4. Deploy! +1. Push your code to GitHub. +2. Import the project in the [Vercel dashboard](https://vercel.com/new). +3. Under **Settings → Environment Variables**, add every variable listed in the [Environment Variables](#-environment-variables) section below. Vercel automatically makes them available at build time and runtime. + - For `NEXTAUTH_URL`, set the value to your Vercel deployment URL (e.g. `https://gitverse.vercel.app`). In local development, set it to `http://localhost:3000` in your `.env.local` to avoid missing-URL warnings. + - Mark sensitive secrets (e.g. `JWT_SECRET`, `NEXTAUTH_SECRET`, `GOOGLE_CLIENT_SECRET`, `GEMINI_API_KEY`) as **Sensitive** in Vercel so they are never exposed in logs. +4. Click **Deploy**. + +> **Tip:** Vercel re-deploys automatically on every push to `main`. If you update an environment variable in the dashboard, trigger a redeploy from **Deployments → Redeploy** for the new value to take effect. ### Docker ```bash docker build -t gitverse-nextjs . -docker run -p 3000:3000 gitverse-nextjs +docker run -p 3000:3000 \ + -e DATABASE_URL="..." \ + -e JWT_SECRET="..." \ + -e GEMINI_API_KEY="..." \ + gitverse-nextjs ``` ### Firebase App Hosting (Cloud Run) @@ -253,22 +274,184 @@ firebase deploy ## 📝 Environment Variables -Required: +Copy `.env.example` to `.env.local` and fill in the values before starting the dev server: + +```bash +cp .env.example .env.local +``` + +> **Never commit `.env.local` or any file containing real secrets.** It is already listed in `.gitignore`. + +### Required Variables + +| Variable | Description | How to obtain | +| :--- | :--- | :--- | +| `DATABASE_URL` | PostgreSQL connection string (with SSL) | Create a free database on [Neon](https://neon.tech) → **Connection Details** → copy the connection string. Append `?sslmode=require&schema=public` if not already present. | +| `JWT_SECRET` | Secret used to sign custom JWT tokens | Generate with `openssl rand -base64 32` or any random string ≥ 32 characters. | +| `GEMINI_API_KEY` | Google Gemini API key for AI features | Go to [Google AI Studio](https://aistudio.google.com/app/apikey) → **Create API key**. | + +### OAuth / NextAuth Variables + +| Variable | Description | How to obtain | +| :--- | :--- | :--- | +| `NEXTAUTH_URL` | Canonical base URL of your deployment | Set to `http://localhost:3000` in development. On Vercel, set to your deployment URL (e.g. `https://gitverse.vercel.app`). | +| `NEXTAUTH_SECRET` | Secret used to sign NextAuth session tokens | Generate with `openssl rand -base64 32`. Must be a strong random string. | +| `GOOGLE_CLIENT_ID` | Google OAuth 2.0 client ID | [Google Cloud Console](https://console.cloud.google.com) → **APIs & Services → Credentials → Create Credentials → OAuth client ID** (Web application). | +| `GOOGLE_CLIENT_SECRET` | Google OAuth 2.0 client secret | Obtained alongside `GOOGLE_CLIENT_ID` in the same step above. | + +### GitHub App Configuration + +GitVerse integrates heavily with GitHub via a GitHub App to read repositories, post PR reviews, and track issues. + +**Required GitHub App Permissions & Scopes:** +To ensure full functionality, your GitHub App must be granted the following repository-level permissions: +- **Contents:** Read-only (Required to fetch source code for AI analysis) +- **Metadata:** Read-only (Mandatory basic read access for all GitHub Apps) +- **Pull requests:** Read & Write (Required to read PR diffs and post review comments) +- **Issues:** Read & Write (Required for tracking and commenting on issues) -- `DATABASE_URL` - PostgreSQL connection string -- `JWT_SECRET` - JWT secret key -- `GEMINI_API_KEY` - Google Gemini API key +**Required Webhook Events:** +- `Pull request` (Fires when PRs are opened or updated, triggering the AI review service) -OAuth (Google / NextAuth): +| Variable | Description & Usage | How to obtain | +| :--- | :--- | :--- | +| `GITHUB_APP_ID` | Numeric ID of your GitHub App. Used to authenticate API requests as the App. | [GitHub Developer Settings](https://github.com/settings/apps) → create or open your App → copy **App ID**. | +| `GITHUB_APP_PRIVATE_KEY` | RSA private key. Used to sign JWTs for GitHub API authentication. | In your GitHub App settings → **Generate a private key** → paste contents with literal `\n` line breaks. | +| `GITHUB_APP_SLUG` | URL slug of your GitHub App. Used to generate installation URLs. | The part after `github.com/apps/` in the App's public URL. | +| `GITHUB_WEBHOOK_SECRET` | Secret string. Used to verify that incoming webhook payloads genuinely came from GitHub. | Set any strong random string here and enter the same value in your GitHub App's webhook configuration. | -- `NEXTAUTH_URL` - Deployed base URL (e.g. `https://`) -- `NEXTAUTH_SECRET` - Session/JWT signing secret (generate with `openssl rand -base64 32`) -- `GOOGLE_CLIENT_ID` - Google OAuth client id -- `GOOGLE_CLIENT_SECRET` - Google OAuth client secret +**Local Development Setup for GitHub Apps:** +1. Create a GitHub App in your developer settings. +2. Use a service like ngrok (`ngrok http 3000`) to expose your local server. +3. Set your App's Webhook URL to `https:///api/webhooks/github`. +4. Add the App credentials to `.env.local`. -Optional: +### Optional Variables -- `NEXT_PUBLIC_API_URL` - API URL for client-side (defaults to current domain) +| Variable | Description | Default | +| :--- | :--- | :--- | +| `NEXT_PUBLIC_API_URL` | Base URL for client-side API calls | Current domain (e.g. `http://localhost:3000`) | +| `NEXT_PUBLIC_FIREBASE_API_KEY` | Firebase API key (App Hosting only) | Not required for local dev or Vercel | +| `GITHUB_APP_STATE_SECRET` | Additional signing secret for OAuth state | Falls back to `NEXTAUTH_SECRET` if unset | + +### Google OAuth Redirect URIs + +Add these **Authorized redirect URIs** in Google Cloud Console → **OAuth client**: + +| Environment | URI | +| :--- | :--- | +| Local dev | `http://localhost:3000/api/auth/callback/google` | +| Vercel | `https:///api/auth/callback/google` | + +--- + +## 🛠️ Troubleshooting + +### `Error: PrismaClientInitializationError` / Cannot connect to database + +**Cause:** `DATABASE_URL` is missing, malformed, or the database is unreachable. + +**Fix:** +1. Verify `.env.local` contains `DATABASE_URL` and the value is correct. +2. Ensure your Neon database is not paused (Neon free-tier databases sleep after inactivity — open the Neon console to wake it). +3. Confirm the connection string includes `?sslmode=require`. +4. Run `npm run prisma:generate` followed by `npm run prisma:migrate` after any schema change. + +### `[next-auth][error][OAUTH_CALLBACK_ERROR]` during Google Sign-In + +**Cause:** `NEXTAUTH_URL`, `GOOGLE_CLIENT_ID`, or `GOOGLE_CLIENT_SECRET` is wrong, or the redirect URI is not registered in Google Cloud Console. + +**Fix:** +1. Double-check `NEXTAUTH_URL` matches the origin you are accessing (including protocol and port). +2. In Google Cloud Console → **OAuth client** → **Authorized redirect URIs**, ensure `/api/auth/callback/google` is listed. +3. On Vercel, set `NEXTAUTH_URL` to the exact deployment URL (no trailing slash). + +### `Error: NEXTAUTH_SECRET is not set` + +**Cause:** `NEXTAUTH_SECRET` is missing from the environment. + +**Fix:** Generate a secret and add it to `.env.local`: + +```bash +openssl rand -base64 32 +``` + +On Vercel, add it under **Settings → Environment Variables**. + +### AI features return `500` / Gemini errors + +**Cause:** `GEMINI_API_KEY` is missing or invalid. + +**Fix:** +1. Confirm `GEMINI_API_KEY` is set in `.env.local`. +2. Verify the key is active in [Google AI Studio](https://aistudio.google.com/app/apikey). +3. Check that the Gemini API is enabled for your Google Cloud project. + +### Environment variables not picked up after editing `.env.local` + +**Fix:** Restart the development server — Next.js reads `.env.local` only at startup: + +```bash +# Stop the server (Ctrl+C), then: +npm run dev +``` + +On Vercel, trigger a redeploy (**Deployments → ⋯ → Redeploy**) after changing any environment variable in the dashboard. + +### `prisma:migrate` fails with `P3009` or migration drift + +**Cause:** Local database is out of sync with the migration history. + +**Fix (development only — do not run in production):** + +```bash +npx prisma migrate reset +npm run prisma:migrate +``` + +### Port 3000 already in use + +**Fix:** Kill the process using port 3000, or start on a different port: + +```bash +# Windows +netstat -ano | findstr :3000 +taskkill /PID /F + +# macOS / Linux +lsof -ti:3000 | xargs kill -9 + +# Or run on a different port +npm run dev -- -p 3001 +``` + +### Build fails on Vercel with `Type error` or missing module + +**Fix:** +1. Ensure all required environment variables are set in the Vercel dashboard — missing vars can cause build-time type errors. +2. Run `npm run build` locally first to catch errors before pushing. +3. Check that your Node.js version in Vercel matches the one used locally (see `engines` in `package.json`). + +### GitHub App Integration Issues + +**Symptoms:** PR reviews aren't posting, repo analysis fails, or webhook errors. + +**Fix:** +- **Missing GitHub App Credentials:** Ensure `GITHUB_APP_ID`, `GITHUB_APP_PRIVATE_KEY`, and other related variables are correctly populated in your `.env.local` or Vercel environment settings. Without these, PR reviews and repository analysis will fail. Build or deployment failures caused by missing secrets are usually flagged during the `npm run build` step. +- **Invalid GitHub App IDs:** Ensure your `GITHUB_APP_ID` is a numeric string (not the client ID or slug). +- **Incorrect Callback URLs:** Ensure the webhook URL in the GitHub App settings exactly matches your deployed domain's endpoint (e.g., `https:///api/webhooks/github`). +- **Permission Denied / API Access Errors:** Verify your GitHub App has the exact permissions listed in the [GitHub App Configuration](#github-app-configuration) section (especially Read/Write on Pull Requests and Read on Contents). If you updated permissions on an existing App, you must accept the new permissions on your App installations. +- **Vercel Deployment / Environment Setup Issues:** In Vercel, format the `GITHUB_APP_PRIVATE_KEY` correctly. Sometimes newlines get mangled during copy-pasting. Enclosing the key in double quotes in the Vercel dashboard or ensuring literal `\n` characters are used can prevent parsing errors. +- **Authentication/Session Failures:** If `next-auth` sessions fail to read the GitHub token, ensure `NEXTAUTH_SECRET` is set and matches across your infrastructure. + +### Error-Handling Guidance for Developers + +When extending the GitHub integration or building new features, follow these error-handling practices: + +- **Detecting Missing Env Vars:** At application startup or within route handlers, explicitly check for `GITHUB_APP_ID` and `GITHUB_APP_PRIVATE_KEY`. If missing, log a clear warning and gracefully disable the GitHub-dependent features instead of crashing the server or UI. +- **Safe API Validation:** Always wrap `githubService` or `octokit` calls in `try/catch` blocks. If `octokit.request` throws a `403 Forbidden` or `404 Not Found` (which often indicates a permission issue or missing repository access), handle it cleanly by returning a `403` or `404` JSON response with a safe, descriptive error string (e.g., `"Repository access denied. Check GitHub App permissions."`). +- **Runtime Failure Behavior:** Ensure webhooks respond with a `200 OK` quickly, even if the internal AI analysis fails or throws an error. If you return a `500` to GitHub synchronously, they may disable your webhook after repeated failures. Use background jobs (like the analysis worker) or isolated error logging for the actual processing step. +- **Common Integration Mistakes:** The most frequent developer mistake is forgetting to accept new permissions on the GitHub App installation after updating them in the App settings. Always remind users to navigate to their installation settings to approve permission scope changes. ## 🤝 Contributing @@ -290,6 +473,6 @@ This project is licensed under the MIT License. - NeonDB for serverless PostgreSQL - All contributors and users of GitVerse ---- + Made with ❤️ by the GitVerse Team diff --git a/START_HERE.md b/START_HERE.md index 7c851e85..09f45d44 100644 --- a/START_HERE.md +++ b/START_HERE.md @@ -6,6 +6,10 @@ 📁 /home/time_walker/projects/projects/gitverse/gitverse-nextjs/ ``` +## System Requirements + +- **Node.js 22.x** is officially supported. See [Supported Node Version](README.md#supported-node-version) in README for details. + ## Immediate Next Steps ### 1. Open in VS Code @@ -240,7 +244,24 @@ git push origin main 4. Review Prisma docs: https://prisma.io/docs --- +## Validation Notes + +The AI API routes validate request inputs before processing. + +Invalid requests return a `400 Bad Request` response with a helpful error message instead of exposing stack traces. + +Examples: +- Empty or missing `code` +- Empty or missing `language` +- Unsupported `analysisType` +- Oversized code snippets + +Example response: +```json +{ + "error": "Code must be a non-empty string" +} ## 🚀 You're All Set! Your project has been successfully migrated to Next.js with: diff --git a/app/analysis/[jobId]/page.tsx b/app/analysis/[jobId]/page.tsx index 8229409f..d2710286 100644 --- a/app/analysis/[jobId]/page.tsx +++ b/app/analysis/[jobId]/page.tsx @@ -1,40 +1,116 @@ "use client"; -import { useRouter } from "next/navigation"; +import { useState, useEffect } from "react"; +import { useRouter, useParams } from "next/navigation"; +import { AnalysisDetailSkeleton } from "@/components/analysis/AnalysisDetailSkeleton"; +import { AnalysisFailureState } from "@/components/analysis/AnalysisFailureState"; +import { DashboardLayout } from "@/components/layout/DashboardLayout"; +import { EmptyState } from "@/components/ui"; +import { Activity } from "lucide-react"; export default function AnalysisJobPage() { const router = useRouter(); + const params = useParams(); + + const [data, setData] = useState(null); + const [error, setError] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + setLoading(true); + setData(null); + setError(null); + + if (!params?.jobId) { + setLoading(false); // prevent skeleton from getting stuck + return; + } + + const controller = new AbortController(); + const token = localStorage.getItem("gitverse_token"); + + fetch(`/api/analysis-jobs/${params.jobId}`, { + signal: controller.signal, + headers: { + ...(token ? { Authorization: `Bearer ${token}` } : {}), + }, + }) + .then(async (res) => { + if (!res.ok) { + const status = res.status; + // Fix 2: never forward raw API error — use sanitized messages + if (status === 401 || status === 403) { + throw new Error("You do not have permission to view this analysis."); + } + if (status === 404) { + throw new Error("This analysis could not be found."); + } + throw new Error("Failed to load analysis. Please try again."); + } + return res.json(); + }) + .then((result) => { + if (!controller.signal.aborted) { + setData(result); + setLoading(false); + } + }) + .catch((e) => { + if (e.name === 'AbortError') return; + if (!controller.signal.aborted) { + setError(e.message); + setLoading(false); + } + }); + + return () => { + controller.abort(); + }; + }, [params?.jobId]); + + if (loading) return ; + + if (error) return ; return ( -
-

- No Analysis Jobs Found -

-

- You haven't created any analysis jobs yet. -

- -
+ + {data ? ( +
+

{data.title || "Analysis Job Details"}

+
+
+
+

Status

+

{data.status || "Unknown"}

+
+
+

Repository

+

{data.repository || "Unknown"}

+
+
+

Created At

+

{data.createdAt ? new Date(data.createdAt).toLocaleString() : "Unknown"}

+
+
+
+
+

Raw Data

+
+              {JSON.stringify(data, null, 2)}
+            
+
+
+ ) : ( +
+ router.push("/analyze")} + /> +
+ )} +
); } diff --git a/app/analyze/route.ts b/app/analyze/route.ts index 721f05b3..6b988e67 100644 --- a/app/analyze/route.ts +++ b/app/analyze/route.ts @@ -1,5 +1,5 @@ import { NextRequest, NextResponse } from "next/server"; -import { requireAuth } from "@/lib/middleware"; +import { requireAuth } from "@/lib/api-auth"; import prisma from "@/lib/prisma"; import { repositoryService } from "@/lib/services/repositoryService"; import { analysisJobService } from "@/lib/services/analysisJobService"; diff --git a/app/api/ai/analyze-code/route.ts b/app/api/ai/analyze-code/route.ts index dc2d9e4e..e877bfc3 100644 --- a/app/api/ai/analyze-code/route.ts +++ b/app/api/ai/analyze-code/route.ts @@ -1,5 +1,5 @@ import { NextRequest, NextResponse } from "next/server"; -import { isHttpError, requireAuth } from "@/lib/middleware"; +import { isHttpError, requireAuth } from "@/lib/api-auth"; import { getGeminiService } from "@/lib/services/geminiService"; export async function POST(request: NextRequest) { @@ -38,9 +38,6 @@ export async function POST(request: NextRequest) { { status: error.status } ); } - return NextResponse.json( - { error: "Failed to analyze code" }, - { status: 500 } - ); + return NextResponse.json({ error: "Internal Server Error" }, { status: 500 }); } } diff --git a/app/api/ai/analyze-repository/route.ts b/app/api/ai/analyze-repository/route.ts index 8e64eece..7fd35fbb 100644 --- a/app/api/ai/analyze-repository/route.ts +++ b/app/api/ai/analyze-repository/route.ts @@ -1,68 +1,227 @@ import { NextRequest, NextResponse } from "next/server"; +<<<<<<< HEAD import { isHttpError, requireAuth } from "@/lib/middleware"; +import { AnalysisTimeoutError } from "@/lib/errors/AnalysisTimeoutError"; +======= +import { isHttpError, requireAuth } from "@/lib/api-auth"; +>>>>>>> upstream/main import { getGeminiService } from "@/lib/services/geminiService"; import { repositoryService } from "@/lib/services/repositoryService"; -export async function POST(request: NextRequest) { +const ANALYSIS_TIMEOUT_MS = 25_000; +const MAX_COMMITS = 10; + +const VALID_ANALYSIS_TYPES = [ + "overview", + "security", + "performance", + "quality", + "dependencies", +] as const; + +type AnalysisType = (typeof VALID_ANALYSIS_TYPES)[number]; + +interface LanguageEntry { + name: string; + percentage: number; +} + +interface ContributorEntry { + name: string; + commits: number; +} + +interface CommitEntry { + message: string; + author: string; + date: string | undefined; +} + +interface RepositoryContext { + languages: LanguageEntry[]; + contributors: ContributorEntry[]; + commits: CommitEntry[]; +} + +interface ResponseMeta { + duration: number; + success: boolean; +} + +interface SuccessResponse { + analysis: unknown; + type: AnalysisType; + meta: ResponseMeta; +} + +interface ErrorResponse { + error: string; + meta: ResponseMeta; +} + +const log = { + info: (event: string, data?: Record) => + console.log(JSON.stringify({ level: "info", event, ...data, ts: new Date().toISOString() })), + + warn: (event: string, data?: Record) => + console.warn(JSON.stringify({ level: "warn", event, ...data, ts: new Date().toISOString() })), + + error: (event: string, data?: Record) => + console.error(JSON.stringify({ level: "error", event, ...data, ts: new Date().toISOString() })), +}; + + +function isValidAnalysisType(value: string): value is AnalysisType { + return (VALID_ANALYSIS_TYPES as readonly string[]).includes(value); +} + +function buildContext( + repository: Awaited> +): RepositoryContext { + return { + languages: + repository.languages?.map((l: LanguageEntry) => ({ + name: l.name, + percentage: l.percentage, + })) ?? [], + + contributors: + repository.contributors?.map((c: ContributorEntry) => ({ + name: c.name, + commits: c.commits, + })) ?? [], + + commits: + repository.commits + ?.slice(0, MAX_COMMITS) + .map((c: { message: string; authorName: string; committedAt?: Date }) => ({ + message: c.message, + author: c.authorName, + date: c.committedAt?.toISOString(), + })) ?? [], + }; +} + +export async function POST( + request: NextRequest +): Promise> { + const startTime = Date.now(); + const noStoreHeaders = { "Cache-Control": "no-store" as const }; + + const duration = () => Date.now() - startTime; + + const errorResponse = ( + message: string, + status: number + ): NextResponse => + NextResponse.json( + { error: message, meta: { duration: duration(), success: false } }, + { status, headers: noStoreHeaders } + ); + try { const user = await requireAuth(request); - const body = await request.json(); - const { repositoryId, type } = body; + let repositoryId: unknown; + let type: unknown; - if (!repositoryId || !type) { - return NextResponse.json( - { error: "Repository ID and analysis type are required" }, - { status: 400 } - ); + try { + const body: unknown = await request.json(); + + if (!body || typeof body !== "object" || Array.isArray(body)) { + log.warn("validation_failed", { reason: "non_object_body", userId: user.userId }); + return errorResponse("Request body must be a JSON object", 400); + } + + ({ repositoryId, type } = body as Record); + } catch { + log.warn("validation_failed", { reason: "malformed_json", userId: user.userId }); + return errorResponse("Request body is not valid JSON", 400); } - const repository = await repositoryService.getRepository( - repositoryId, - user.userId - ); + if (typeof repositoryId !== "string" || repositoryId.trim() === "") { + log.warn("validation_failed", { reason: "invalid_repository_id", userId: user.userId }); + return errorResponse("Invalid repository ID", 400); + } - if (!repository) { - return NextResponse.json( - { error: "Repository not found" }, - { status: 404 } + if (typeof type !== "string" || type.trim() === "") { + log.warn("validation_failed", { reason: "missing_analysis_type", userId: user.userId }); + return errorResponse("Analysis type is required", 400); + } + + if (!isValidAnalysisType(type)) { + log.warn("validation_failed", { + reason: "unknown_analysis_type", + provided: type, + valid: VALID_ANALYSIS_TYPES, + userId: user.userId, + }); + return errorResponse( + `Invalid analysis type. Must be one of: ${VALID_ANALYSIS_TYPES.join(", ")}`, + 400 ); } - const context = { - languages: repository.languages.map((l: any) => ({ - name: l.name, - percentage: l.percentage, - })), - contributors: repository.contributors.map((c: any) => ({ - name: c.name, - commits: c.commits, - })), - commits: repository.commits.slice(0, 10).map((c: any) => ({ - message: c.message, - author: c.authorName, - date: c.committedAt.toISOString(), - })), - }; - - const analysis = await getGeminiService().analyzeRepository({ - repositoryId, - type, - context, + log.info("analysis_started", { repositoryId, type, userId: user.userId }); + + + const repository = await repositoryService.getRepository(repositoryId, user.userId); + + if (!repository || repository.userId !== user.userId) { + log.warn("repository_not_found", { repositoryId, userId: user.userId }); + return errorResponse("Repository not found", 404); + } + + const context = buildContext(repository); + const controller = new AbortController(); + let timeoutHandle: ReturnType | undefined; + + const timeoutPromise = new Promise((_, reject) => { + timeoutHandle = setTimeout(() => { + controller.abort(); // cancels the Gemini SDK request + reject(new AnalysisTimeoutError()); + }, ANALYSIS_TIMEOUT_MS); }); - return NextResponse.json({ analysis, type }); - } catch (error: any) { - console.error("Repository analysis error:", error); + let analysis: unknown; + try { + const analysisPromise = getGeminiService().analyzeRepository({ + repositoryId, + type, + context, + signal: controller.signal, // GeminiService forwards this to generateContent() + }); + analysis = await Promise.race([analysisPromise, timeoutPromise]); + } finally { + clearTimeout(timeoutHandle); // prevents timer leak on the success path + } + + log.info("analysis_completed", { repositoryId, type, userId: user.userId, duration: duration() }); + + return NextResponse.json( + { analysis, type, meta: { duration: duration(), success: true } }, + { status: 200, headers: noStoreHeaders } + ); + + } catch (error: unknown) { if (isHttpError(error)) { - return NextResponse.json( - { error: error.message }, - { status: error.status } - ); + const is403 = error.status === 403; + const status = is403 ? 404 : error.status; + // Remap 403 message too — returning the real reason under a 404 status + // still leaks that the resource exists but access was denied. + const message = is403 ? "Resource not found" : error.message; + log.warn("http_error", { originalStatus: error.status, status, message: error.message }); + return errorResponse(message, status); } - return NextResponse.json( - { error: "Failed to analyze repository" }, - { status: 500 } + + const isTimeout = error instanceof AnalysisTimeoutError; + const message = error instanceof Error ? error.message : "Unknown error"; + + log.error("unhandled_error", { message, isTimeout, duration: duration() }); + + return errorResponse( + isTimeout ? "Analysis timed out. Please try again." : "Failed to analyze repository", + isTimeout ? 504 : 500 ); } -} +} \ No newline at end of file diff --git a/app/api/ai/chat/route.ts b/app/api/ai/chat/route.ts index 6d258259..0e6975e4 100644 --- a/app/api/ai/chat/route.ts +++ b/app/api/ai/chat/route.ts @@ -1,60 +1,111 @@ import { NextRequest, NextResponse } from "next/server"; -import { isHttpError, requireAuth } from "@/lib/middleware"; +import { isHttpError, requireAuth } from "@/lib/api-auth"; import { getGeminiService } from "@/lib/services/geminiService"; import { repositoryService } from "@/lib/services/repositoryService"; export async function POST(request: NextRequest) { try { + const user = await requireAuth(request); - const body = await request.json(); - const { repositoryId, question, conversationHistory, prompt } = body; + let body; + + try { + body = await request.json(); + } catch { + return NextResponse.json( + { error: "Invalid JSON body" }, + { status: 400 } + ); + } - // Free-form mode: client provides a prebuilt prompt. + const { repositoryId, question, conversationHistory, prompt } = body || {}; + + // Validating prompt type if provided + if (prompt !== undefined && typeof prompt !== "string") { + return NextResponse.json( + { error: "Prompt must be a string" }, + { status: 400 } + ); + } + + // Free-form mode: client provides a prebuilt prompt if (typeof prompt === "string" && prompt.trim()) { const response = await getGeminiService().chatRaw(prompt); + return NextResponse.json({ response }); } - if (!repositoryId || !question) { + // Validating repositoryId + const parsedRepositoryId = Number(repositoryId); + + if ( + typeof repositoryId !== "string" || + !repositoryId.trim() || + Number.isNaN(parsedRepositoryId) + ) { + return NextResponse.json( + { error: "Valid repository ID is required" }, + { status: 400 } + ); + } + + // Validating question + if ( + typeof question !== "string" || + !question.trim() + ) { return NextResponse.json( - { error: "Repository ID and question are required" }, + { error: "Valid question is required" }, + { status: 400 } + ); + } + + // Validating conversationHistory + if ( + conversationHistory !== undefined && + !Array.isArray(conversationHistory) + ) { + return NextResponse.json( + { error: "conversationHistory must be an array" }, { status: 400 } ); } const repository = await repositoryService.getRepository( - repositoryId, + parsedRepositoryId, user.userId ); if (!repository) { - return NextResponse.json( - { error: "Repository not found" }, - { status: 404 } - ); + return NextResponse.json({ error: "Not Found" }, { status: 404 }); } const context = { - files: repository.files.slice(0, 20).map((f: { path: string }) => f.path), + files: repository.files + .slice(0, 20) + .map((f: { path: string }) => f.path), + recentCommits: repository.commits .slice(0, 5) .map( (c: { shortHash: string; message: string }) => `${c.shortHash}: ${c.message}` ), + contributors: repository.contributors.map( (c: { name: string }) => c.name ), }; const response = await getGeminiService().chatAboutRepository({ - repositoryId, + repositoryId: parsedRepositoryId, question, conversationHistory, context, }); return NextResponse.json({ response, question }); + } catch (error: any) { console.error("AI chat error:", error); @@ -65,9 +116,6 @@ export async function POST(request: NextRequest) { ); } - return NextResponse.json( - { error: "Failed to process chat" }, - { status: 500 } - ); + return NextResponse.json({ error: "Internal Server Error" }, { status: 500 }); } } diff --git a/app/api/ai/explain-file/route.ts b/app/api/ai/explain-file/route.ts index 4819b5b0..65686240 100644 --- a/app/api/ai/explain-file/route.ts +++ b/app/api/ai/explain-file/route.ts @@ -1,5 +1,5 @@ import { NextRequest, NextResponse } from "next/server"; -import { isHttpError, requireAuth } from "@/lib/middleware"; +import { isHttpError, requireAuth } from "@/lib/api-auth"; import { repositoryService } from "@/lib/services/repositoryService"; type RepositoryFile = { @@ -43,19 +43,13 @@ export async function POST(request: NextRequest) { )) as Repository; if (!repository) { - return NextResponse.json( - { error: "Repository not found" }, - { status: 404 } - ); + return NextResponse.json({ error: "Not Found" }, { status: 404 }); } const file = repository.files.find((f) => f.path === filePath); if (!file) { - return NextResponse.json( - { error: "File not found in repository" }, - { status: 404 } - ); + return NextResponse.json({ error: "Not Found" }, { status: 404 }); } const explanation = `File: ${file.path}\nSize: ${file.size} bytes\nLanguage: ${file.language || "Unknown"}\n\nThis is a ${file.extension || "file"} in the repository.`; @@ -73,9 +67,6 @@ export async function POST(request: NextRequest) { { status: error.status } ); } - return NextResponse.json( - { error: "Failed to explain file" }, - { status: 500 } - ); + return NextResponse.json({ error: "Internal Server Error" }, { status: 500 }); } } diff --git a/app/api/ai/review-pr/route.ts b/app/api/ai/review-pr/route.ts index 0a4c0a90..cb1be7c6 100644 --- a/app/api/ai/review-pr/route.ts +++ b/app/api/ai/review-pr/route.ts @@ -1,5 +1,5 @@ import { NextRequest, NextResponse } from "next/server"; -import { requireAuth } from "@/lib/middleware"; +import { requireAuth } from "@/lib/api-auth"; import { parsePullRequestUrl, reviewPullRequest, diff --git a/app/api/ai/suggest-commit/route.ts b/app/api/ai/suggest-commit/route.ts index aff6393e..722ccf6d 100644 --- a/app/api/ai/suggest-commit/route.ts +++ b/app/api/ai/suggest-commit/route.ts @@ -1,5 +1,5 @@ import { NextRequest, NextResponse } from "next/server"; -import { isHttpError, requireAuth } from "@/lib/middleware"; +import { isHttpError, requireAuth } from "@/lib/api-auth"; import { getGeminiService } from "@/lib/services/geminiService"; export async function POST(request: NextRequest) { @@ -37,9 +37,6 @@ export async function POST(request: NextRequest) { { status: error.status } ); } - return NextResponse.json( - { error: "Failed to generate suggestions" }, - { status: 500 } - ); + return NextResponse.json({ error: "Internal Server Error" }, { status: 500 }); } } diff --git a/app/api/analysis-jobs/[id]/route.ts b/app/api/analysis-jobs/[id]/route.ts index b9bf0263..a75c212c 100644 --- a/app/api/analysis-jobs/[id]/route.ts +++ b/app/api/analysis-jobs/[id]/route.ts @@ -1,6 +1,6 @@ import { NextRequest, NextResponse } from "next/server"; -import { requireAuth, isHttpError } from "@/lib/middleware"; +import { requireAuth, isHttpError } from "@/lib/api-auth"; import { analysisJobService } from "@/lib/services/analysisJobService"; export async function GET( @@ -11,15 +11,26 @@ export async function GET( const user = await requireAuth(request); const jobId = params.id; + const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; + if (!uuidPattern.test(jobId)) { + return NextResponse.json( + { error: "Invalid job ID format. Expected a UUID" }, + { status: 400 } + ); + } + const job = await analysisJobService.getJob({ jobId, userId: user.userId, }); if (!job) { - return NextResponse.json({ error: "Job not found" }, { status: 404 }); + return NextResponse.json({ error: "Not Found" }, { status: 404 }); } + const details = job.progressDetails as { retryAfter?: number; rateLimited?: boolean } | null; + const retryAfter = details?.retryAfter ?? null; + return NextResponse.json({ job: { id: job.id, @@ -37,6 +48,8 @@ export async function GET( error: job.error, updatedAt: job.updatedAt, createdAt: job.createdAt, + retryAfter, + rateLimited: details?.rateLimited ?? false, }, }); } catch (error: any) { @@ -49,9 +62,6 @@ export async function GET( ); } - return NextResponse.json( - { error: "Failed to get analysis job" }, - { status: 500 } - ); + return NextResponse.json({ error: "Internal Server Error" }, { status: 500 }); } } diff --git a/app/api/analysis/[jobId]/route.ts b/app/api/analysis/[jobId]/route.ts index 55451a28..baad2831 100644 --- a/app/api/analysis/[jobId]/route.ts +++ b/app/api/analysis/[jobId]/route.ts @@ -1,5 +1,5 @@ import { NextRequest, NextResponse } from "next/server"; -import { requireAuth } from "@/lib/middleware"; +import { requireAuth } from "@/lib/api-auth"; import { analysisJobService } from "@/lib/services/analysisJobService"; export async function GET( @@ -14,15 +14,23 @@ export async function GET( return NextResponse.json({ error: "Missing jobId" }, { status: 400 }); } + const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; + if (!uuidPattern.test(jobId)) { + return NextResponse.json( + { error: "Invalid job ID format. Expected a UUID" }, + { status: 400 } + ); + } + const job = await analysisJobService.getJob({ jobId, userId: user.userId }); if (!job) { - return NextResponse.json({ error: "Job not found" }, { status: 404 }); + return NextResponse.json({ error: "Not Found" }, { status: 404 }); } return NextResponse.json({ job }); } catch (error: any) { console.error("GET /analysis/:jobId error:", error); - return NextResponse.json({ error: "Failed to fetch job" }, { status: 500 }); + return NextResponse.json({ error: "Internal Server Error" }, { status: 500 }); } } diff --git a/app/api/auth/login/route.ts b/app/api/auth/login/route.ts index 3f9249b0..ac988fc5 100644 --- a/app/api/auth/login/route.ts +++ b/app/api/auth/login/route.ts @@ -1,10 +1,46 @@ import { NextRequest, NextResponse } from "next/server"; +import { checkRateLimit } from "@/utils/rateLimit"; +import { checkRateLimit } from '@/utils/rateLimit'; import bcrypt from "bcryptjs"; import prisma from "@/lib/prisma"; import { generateToken } from "@/lib/auth"; +import { parseJsonBody, validateEmail, validatePassword } from "@/lib/validateAuth"; +import { badRequestResponse } from "@/lib/middleware"; + export async function POST(request: NextRequest) { try { + const parsed = await parseJsonBody(request); + if ("error" in parsed) return badRequestResponse(parsed.error); + const { body } = parsed; + + const emailCheck = validateEmail(body.email); + if (!emailCheck.valid) return badRequestResponse(emailCheck.error!); + + const passwordCheck = validatePassword(body.password); + if (!passwordCheck.valid) return badRequestResponse(passwordCheck.error!); + + const email = body.email as string; + const password = body.password as string; +// 1. EXTRACT IP AND CHECK RATE LIMIT FIRST + const forwardedFor = request.headers.get('x-forwarded-for'); + const ip = forwardedFor?.split(',')[0]?.trim() || request.headers.get('x-real-ip') || 'unknown-ip'; + + const rateLimitResult = checkRateLimit(ip); + + if (!rateLimitResult.success) { + return NextResponse.json( + { error: "Too many requests. Please try again later." }, + { + status: 429, + headers: { + 'Retry-After': rateLimitResult.retryAfter?.toString() || '900' + } + } + ); + } + + // 2. PARSE BODY const body = await request.json(); const { email, password } = body; @@ -15,6 +51,14 @@ export async function POST(request: NextRequest) { { status: 400 } ); } + // Email format validation + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(email)) { + return NextResponse.json( + { error: "Please provide a valid email address" }, + { status: 400 } + ); + } // Find user const user = await prisma.user.findUnique({ @@ -22,14 +66,10 @@ export async function POST(request: NextRequest) { }); if (!user) { - return NextResponse.json( - { error: "Invalid email or password" }, - { status: 401 } - ); + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } // Security: never allow password login for Google-only accounts. - // A "Google-only" account is a user without a local password, but with a linked Google OAuth account. if (!user.passwordHash) { const hasGoogleAccount = (await prisma.account.count({ @@ -37,29 +77,20 @@ export async function POST(request: NextRequest) { })) > 0; if (hasGoogleAccount) { - return NextResponse.json( - { error: "Email already exists. Please sign in with Google." }, - { status: 401 } - ); + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } } // Verify password const passwordHash = user.passwordHash || (user as any).password; if (!passwordHash) { - return NextResponse.json( - { error: "Invalid email or password" }, - { status: 401 } - ); + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } const isValidPassword = await bcrypt.compare(password, passwordHash); if (!isValidPassword) { - return NextResponse.json( - { error: "Invalid email or password" }, - { status: 401 } - ); + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } // Generate JWT token @@ -75,10 +106,11 @@ export async function POST(request: NextRequest) { token, }); } catch (error) { - console.error("Login error:", error); + console.error("Login error:", error instanceof Error ? error.message : "Unknown error"); return NextResponse.json( - { error: "Internal server error" }, + { error: "An unexpected error occurred" }, { status: 500 } ); + return NextResponse.json({ error: "Internal Server Error" }, { status: 500 }); } } diff --git a/app/api/auth/logout/route.ts b/app/api/auth/logout/route.ts index 3f9583c1..75fadb72 100644 --- a/app/api/auth/logout/route.ts +++ b/app/api/auth/logout/route.ts @@ -1,14 +1,51 @@ import { NextRequest, NextResponse } from "next/server"; -import { getAuthUser } from "@/lib/middleware"; +import { getAuthUser, unauthorizedResponse } from "@/lib/middleware"; +import { getAuthUser } from "@/lib/api-auth"; export async function POST(request: NextRequest) { - const user = await getAuthUser(request); + try { + // Guard: reject non-JSON content-type if body is provided + const contentType = request.headers.get("content-type"); + if (contentType && !contentType.includes("application/json")) { + return NextResponse.json( + { error: "Content-Type must be application/json" }, + { status: 400 } + ); + } - if (!user) { - return NextResponse.json({ error: "Not authenticated" }, { status: 401 }); - } + const user = await getAuthUser(request); + if (!user) { + return unauthorizedResponse("No active session to log out from"); + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } - // In a stateless JWT setup, logout is handled client-side by removing the token - // We can optionally implement token blacklisting here if needed - return NextResponse.json({ message: "Logged out successfully" }); + // In a stateless JWT setup, logout is handled client-side by removing the token + // We can optionally implement token blacklisting here if needed + + return NextResponse.json( + { message: "Logged out successfully" }, + { status: 200 } + ); + } catch (err) { + console.error("[logout] Unexpected error:", err); + return NextResponse.json( + { error: "An unexpected error occurred" }, + { status: 500 } + ); + } } + +// Reject wrong HTTP methods +export async function GET() { + return NextResponse.json( + { error: "Method not allowed. Use POST." }, + { status: 405, headers: { Allow: "POST" } } + ); + +} catch (error) { + console.error("Logout API Error:", error); + + //prevent stack trace from reaching client + return NextResponse.json({ error: "Internal Server Error" }, { status: 500 }); + } +} \ No newline at end of file diff --git a/app/api/auth/me/route.ts b/app/api/auth/me/route.ts index 7cc33d7b..d6e559dc 100644 --- a/app/api/auth/me/route.ts +++ b/app/api/auth/me/route.ts @@ -1,6 +1,6 @@ import { NextRequest, NextResponse } from "next/server"; import prisma from "@/lib/prisma"; -import { getAuthUser } from "@/lib/middleware"; +import { getAuthUser } from "@/lib/api-auth"; export const dynamic = "force-dynamic"; export const runtime = "nodejs"; @@ -10,7 +10,7 @@ export async function GET(request: NextRequest) { const user = await getAuthUser(request); if (!user) { - return NextResponse.json({ error: "Not authenticated" }, { status: 401 }); + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } // Fetch user details @@ -19,7 +19,7 @@ export async function GET(request: NextRequest) { }); if (!userDetails) { - return NextResponse.json({ error: "User not found" }, { status: 404 }); + return NextResponse.json({ error: "Not Found" }, { status: 404 }); } return NextResponse.json({ @@ -31,10 +31,11 @@ export async function GET(request: NextRequest) { }, }); } catch (error) { - console.error("Get user error:", error); + console.error("Get user error:", error instanceof Error ? error.message : "Unknown error"); return NextResponse.json( - { error: "Internal server error" }, + { error: "An unexpected error occurred" }, { status: 500 } ); + return NextResponse.json({ error: "Internal Server Error" }, { status: 500 }); } } diff --git a/app/api/auth/sessions/route.ts b/app/api/auth/sessions/route.ts index 96c07d64..ebc89707 100644 --- a/app/api/auth/sessions/route.ts +++ b/app/api/auth/sessions/route.ts @@ -1,5 +1,5 @@ import { NextRequest, NextResponse } from "next/server"; -import { getAuthUser } from "@/lib/middleware"; +import { getAuthUser } from "@/lib/api-auth"; import prisma from "@/lib/prisma"; import { toJsonSafe } from "@/lib/utils/jsonSafe"; @@ -10,7 +10,7 @@ export async function GET(request: NextRequest) { const user = await getAuthUser(request); if (!user) { - return NextResponse.json({ error: "Not authenticated" }, { status: 401 }); + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } const { searchParams } = new URL(request.url); @@ -48,8 +48,9 @@ export async function GET(request: NextRequest) { } catch (error: any) { console.error("Fetch sessions error:", error); return NextResponse.json( - { error: "Internal server error" }, + { error: "An unexpected error occurred" }, { status: 500 } ); + return NextResponse.json({ error: "Internal Server Error" }, { status: 500 }); } } diff --git a/app/api/auth/signup/route.ts b/app/api/auth/signup/route.ts index 7287dc64..e4d27e8e 100644 --- a/app/api/auth/signup/route.ts +++ b/app/api/auth/signup/route.ts @@ -1,27 +1,66 @@ import { NextRequest, NextResponse } from "next/server"; +import { checkRateLimit } from "@/utils/rateLimit"; +import { checkRateLimit } from '@/utils/rateLimit'; import bcrypt from "bcryptjs"; import prisma from "@/lib/prisma"; import { generateToken } from "@/lib/auth"; +import { parseJsonBody, validateEmail, validatePassword, validateRequiredString } from "@/lib/validateAuth"; +import { badRequestResponse } from "@/lib/middleware"; + export async function POST(request: NextRequest) { try { - const body = await request.json(); - const { email, password, name } = body; + const parsed = await parseJsonBody(request); + if ("error" in parsed) return badRequestResponse(parsed.error); + const { body } = parsed; + // 1. EXTRACT IP AND CHECK RATE LIMIT FIRST + const forwardedFor = request.headers.get('x-forwarded-for'); + const ip = forwardedFor?.split(',')[0]?.trim() || request.headers.get('x-real-ip') || 'unknown-ip'; + + const rateLimitResult = checkRateLimit(ip); - // Validation - if (!email || !password || !name) { + if (!rateLimitResult.success) { return NextResponse.json( - { error: "Email, password, and name are required" }, - { status: 400 } + { error: "Too many requests. Please try again later." }, + { + status: 429, + headers: { + 'Retry-After': rateLimitResult.retryAfter?.toString() || '900' + } + } ); } + + //2. PARSE BODY + const body = await request.json(); + const { email, password, name } = body; + + const emailCheck = validateEmail(body.email); + if (!emailCheck.valid) return badRequestResponse(emailCheck.error!); + const passwordCheck = validatePassword(body.password); + if (!passwordCheck.valid) return badRequestResponse(passwordCheck.error!); + + const nameCheck = validateRequiredString(body.name, "Name"); + if (!nameCheck.valid) return badRequestResponse(nameCheck.error!); + + const email = body.email as string; + const password = body.password as string; + const name = body.name as string; if (password.length < 6) { return NextResponse.json( { error: "Password must be at least 6 characters" }, { status: 400 } ); } + // Email format validation + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(email)) { + return NextResponse.json( + { error: "Please provide a valid email address" }, + { status: 400 } + ); + } // Check if user already exists const existingUser = await prisma.user.findUnique({ @@ -78,8 +117,9 @@ export async function POST(request: NextRequest) { } catch (error) { console.error("Signup error:", error); return NextResponse.json( - { error: "Internal server error" }, + { error: "An unexpected error occurred" }, { status: 500 } ); + return NextResponse.json({ error: "Internal Server Error" }, { status: 500 }); } } diff --git a/app/api/integrations/github/app/callback/route.ts b/app/api/integrations/github/app/callback/route.ts index f031a1c7..efb34193 100644 --- a/app/api/integrations/github/app/callback/route.ts +++ b/app/api/integrations/github/app/callback/route.ts @@ -1,7 +1,8 @@ import { NextRequest, NextResponse } from "next/server"; import prisma from "@/lib/prisma"; import { GitHubAppService } from "@/lib/services/githubAppService"; -import { GitHubService } from "@/lib/services/githubService"; +import { GitHubService, GitHubRateLimitError } from "@/lib/services/githubService"; +import { sanitizeErrorMessage } from "@/lib/utils/rateLimit"; import { verifySignedState } from "@/lib/utils/signedState"; type InstallState = { userId: number; ts: number; nonce?: string }; @@ -125,7 +126,15 @@ export async function GET(request: NextRequest) { redirectUrl.searchParams.set("installation_id", String(installationId)); return NextResponse.redirect(redirectUrl); } catch (e: any) { - console.error("GitHub App callback error:", e); + console.error("GitHub App callback error:", sanitizeErrorMessage(e)); + + if (e instanceof GitHubRateLimitError) { + redirectUrl.searchParams.set("install", "error"); + redirectUrl.searchParams.set("reason", "rate_limit"); + redirectUrl.searchParams.set("retryAfter", String(e.retryAfterSeconds)); + return NextResponse.redirect(redirectUrl); + } + redirectUrl.searchParams.set("install", "error"); redirectUrl.searchParams.set("reason", e?.message || "unknown"); return NextResponse.redirect(redirectUrl); diff --git a/app/api/integrations/github/app/delete/route.ts b/app/api/integrations/github/app/delete/route.ts index 73e471e2..031cce18 100644 --- a/app/api/integrations/github/app/delete/route.ts +++ b/app/api/integrations/github/app/delete/route.ts @@ -1,7 +1,8 @@ import { NextRequest, NextResponse } from "next/server"; import prisma from "@/lib/prisma"; -import { isHttpError, requireAuth } from "@/lib/middleware"; +import { isHttpError, requireAuth } from "@/lib/api-auth"; import { GitHubAppService } from "@/lib/services/githubAppService"; +import { sanitizeErrorMessage } from "@/lib/utils/rateLimit"; export async function POST(request: NextRequest) { try { @@ -84,7 +85,7 @@ export async function POST(request: NextRequest) { { status: 200 }, ); } catch (error: any) { - console.error("GitHub App delete error:", error); + console.error("GitHub App delete error:", sanitizeErrorMessage(error)); if (isHttpError(error)) { return NextResponse.json( { error: error.message }, diff --git a/app/api/integrations/github/app/install-url/route.ts b/app/api/integrations/github/app/install-url/route.ts index a4bd727e..5c5bb7f1 100644 --- a/app/api/integrations/github/app/install-url/route.ts +++ b/app/api/integrations/github/app/install-url/route.ts @@ -1,5 +1,6 @@ import { NextRequest, NextResponse } from "next/server"; -import { isHttpError, requireAuth } from "@/lib/middleware"; +import { isHttpError, requireAuth } from "@/lib/api-auth"; +import { sanitizeErrorMessage } from "@/lib/utils/rateLimit"; import { createSignedState } from "@/lib/utils/signedState"; function getRequiredEnv(name: string): string { @@ -25,7 +26,7 @@ export async function POST(request: NextRequest) { return NextResponse.json({ url }, { status: 200 }); } catch (error: any) { - console.error("GitHub App install-url error:", error); + console.error("GitHub App install-url error:", sanitizeErrorMessage(error)); if (isHttpError(error)) { return NextResponse.json( { error: error.message }, diff --git a/app/api/integrations/github/app/sync/route.ts b/app/api/integrations/github/app/sync/route.ts index 7d487764..b4ca97bb 100644 --- a/app/api/integrations/github/app/sync/route.ts +++ b/app/api/integrations/github/app/sync/route.ts @@ -1,8 +1,9 @@ import { NextRequest, NextResponse } from "next/server"; -import { isHttpError, requireAuth } from "@/lib/middleware"; +import { isHttpError, requireAuth } from "@/lib/api-auth"; import prisma from "@/lib/prisma"; import { GitHubAppService } from "@/lib/services/githubAppService"; -import { GitHubService } from "@/lib/services/githubService"; +import { GitHubService, GitHubRateLimitError } from "@/lib/services/githubService"; +import { sanitizeErrorMessage } from "@/lib/utils/rateLimit"; export async function POST(request: NextRequest) { try { @@ -108,7 +109,15 @@ export async function POST(request: NextRequest) { { status: 200 }, ); } catch (error: any) { - console.error("GitHub App sync error:", error); + console.error("GitHub App sync error:", sanitizeErrorMessage(error)); + + if (error instanceof GitHubRateLimitError) { + return NextResponse.json( + { error: error.message, retryAfter: error.retryAfterSeconds }, + { status: 429 } + ); + } + if (isHttpError(error)) { return NextResponse.json( { error: error.message }, diff --git a/app/api/integrations/github/connect/route.ts b/app/api/integrations/github/connect/route.ts index 00fe0545..b277f8e1 100644 --- a/app/api/integrations/github/connect/route.ts +++ b/app/api/integrations/github/connect/route.ts @@ -1,7 +1,8 @@ import { NextRequest, NextResponse } from "next/server"; -import { isHttpError, requireAuth } from "@/lib/middleware"; +import { isHttpError, requireAuth } from "@/lib/api-auth"; import prisma from "@/lib/prisma"; -import { GitHubService } from "@/lib/services/githubService"; +import { GitHubService, GitHubRateLimitError } from "@/lib/services/githubService"; +import { sanitizeErrorMessage } from "@/lib/utils/rateLimit"; import { toJsonSafe } from "@/lib/utils/jsonSafe"; export async function POST(request: NextRequest) { @@ -45,7 +46,15 @@ export async function POST(request: NextRequest) { return NextResponse.json({ account: toJsonSafe(account) }, { status: 200 }); } catch (error: any) { - console.error("GitHub connect error:", error); + console.error("GitHub connect error:", sanitizeErrorMessage(error)); + + if (error instanceof GitHubRateLimitError) { + return NextResponse.json( + { error: error.message, retryAfter: error.retryAfterSeconds }, + { status: 429 } + ); + } + if (isHttpError(error)) { return NextResponse.json( { error: error.message }, diff --git a/app/api/integrations/github/connected-repos/route.ts b/app/api/integrations/github/connected-repos/route.ts index 1f6c567f..2ee2e673 100644 --- a/app/api/integrations/github/connected-repos/route.ts +++ b/app/api/integrations/github/connected-repos/route.ts @@ -1,6 +1,7 @@ import { NextRequest, NextResponse } from "next/server"; -import { isHttpError, requireAuth } from "@/lib/middleware"; +import { isHttpError, requireAuth } from "@/lib/api-auth"; import prisma from "@/lib/prisma"; +import { sanitizeErrorMessage } from "@/lib/utils/rateLimit"; import { toJsonSafe } from "@/lib/utils/jsonSafe"; export async function GET(request: NextRequest) { @@ -34,7 +35,7 @@ export async function GET(request: NextRequest) { { status: 200 }, ); } catch (error: any) { - console.error("GitHub connected repos error:", error); + console.error("GitHub connected repos error:", sanitizeErrorMessage(error)); if (isHttpError(error)) { return NextResponse.json( { error: error.message }, diff --git a/app/api/integrations/github/import/route.ts b/app/api/integrations/github/import/route.ts index 792ddfcc..57eac147 100644 --- a/app/api/integrations/github/import/route.ts +++ b/app/api/integrations/github/import/route.ts @@ -1,6 +1,7 @@ import { NextRequest, NextResponse } from "next/server"; -import { requireAuth } from "@/lib/middleware"; +import { requireAuth } from "@/lib/api-auth"; import { GitHubService, GitHubRateLimitError } from "@/lib/services/githubService"; +import { sanitizeErrorMessage } from "@/lib/utils/rateLimit"; import { repositoryService } from "@/lib/services/repositoryService"; export async function POST(request: NextRequest) { @@ -34,6 +35,10 @@ export async function POST(request: NextRequest) { const github = new GitHubService(token); const repoData = await github.getRepository(parsed.owner, parsed.repo); + if (!repoData) { + return NextResponse.json({ error: "Not Found" }, { status: 404 }); + } + const repository = await repositoryService.createRepository({ name: repoData.name, url: repoData.clone_url, @@ -43,7 +48,7 @@ export async function POST(request: NextRequest) { return NextResponse.json({ repository, source: "github" }, { status: 201 }); } catch (error: any) { - console.error("GitHub import error:", error); + console.error("GitHub import error:", sanitizeErrorMessage(error)); if (error instanceof GitHubRateLimitError) { return NextResponse.json( @@ -52,9 +57,6 @@ export async function POST(request: NextRequest) { ); } - return NextResponse.json( - { error: "Failed to import from GitHub" }, - { status: 500 } - ); + return NextResponse.json({ error: "Internal Server Error" }, { status: 500 }); } } diff --git a/app/api/integrations/github/pr-reviews/route.ts b/app/api/integrations/github/pr-reviews/route.ts index d56c2f7e..3cea71d1 100644 --- a/app/api/integrations/github/pr-reviews/route.ts +++ b/app/api/integrations/github/pr-reviews/route.ts @@ -1,7 +1,8 @@ import { NextRequest, NextResponse } from "next/server"; -import { isHttpError, requireAuth } from "@/lib/middleware"; +import { isHttpError, requireAuth } from "@/lib/api-auth"; import prisma from "@/lib/prisma"; import { toJsonSafe } from "@/lib/utils/jsonSafe"; +import { isValidRepositoryIdentifier } from "@/lib/utils/validators"; function clampInt( value: string | null, @@ -20,6 +21,14 @@ export async function GET(request: NextRequest) { const url = new URL(request.url); const repoFullName = (url.searchParams.get("repoFullName") || "").trim(); + + if (repoFullName && !isValidRepositoryIdentifier(repoFullName)) { + return NextResponse.json( + { error: "Invalid repository identifier format" }, + { status: 400 } + ); + } + const includeDisabled = (url.searchParams.get("includeDisabled") || "") .trim() .toLowerCase(); @@ -63,7 +72,23 @@ export async function GET(request: NextRequest) { }, }); - return NextResponse.json({ repos: toJsonSafe(repos) }, { status: 200 }); + if (repoFullName && repos.length === 0) { + return NextResponse.json( + { error: "Repository not found" }, + { status: 404 }, + ); + } + + return NextResponse.json( + { repos: toJsonSafe(repos) }, + { + status: 200, + headers: { + "Cache-Control": + "no-store, no-cache, must-revalidate, proxy-revalidate", + }, + }, + ); } catch (error: any) { console.error("GitHub PR reviews error:", error); if (isHttpError(error)) { diff --git a/app/api/integrations/github/repositories/route.ts b/app/api/integrations/github/repositories/route.ts index bf6c3292..a124520e 100644 --- a/app/api/integrations/github/repositories/route.ts +++ b/app/api/integrations/github/repositories/route.ts @@ -1,6 +1,7 @@ import { NextRequest, NextResponse } from "next/server"; -import { isHttpError, requireAuth } from "@/lib/middleware"; -import { GitHubService } from "@/lib/services/githubService"; +import { isHttpError, requireAuth } from "@/lib/api-auth"; +import { GitHubService, GitHubRateLimitError } from "@/lib/services/githubService"; +import { sanitizeErrorMessage } from "@/lib/utils/rateLimit"; import prisma from "@/lib/prisma"; export async function POST(request: NextRequest) { @@ -36,7 +37,7 @@ export async function POST(request: NextRequest) { return NextResponse.json( { error: - "No GitHub token or GitHub App repos found in DB. If you installed the app but weren’t redirected back, set the GitHub App Setup URL to /api/integrations/github/app/callback, or use the Sync Installation option in Contribute.", + "No GitHub token or GitHub App repos found in DB. If you installed the app but weren't redirected back, set the GitHub App Setup URL to /api/integrations/github/app/callback, or use the Sync Installation option in Contribute.", }, { status: 400 }, ); @@ -54,7 +55,15 @@ export async function POST(request: NextRequest) { return NextResponse.json({ repositories, source: "github-app-db" }); } catch (error: any) { - console.error("GitHub repositories error:", error); + console.error("GitHub repositories error:", sanitizeErrorMessage(error)); + + if (error instanceof GitHubRateLimitError) { + return NextResponse.json( + { error: error.message, retryAfter: error.retryAfterSeconds }, + { status: 429 } + ); + } + if (isHttpError(error)) { return NextResponse.json( { error: error.message }, diff --git a/app/api/integrations/github/select-repos/route.ts b/app/api/integrations/github/select-repos/route.ts index 8e43f3d2..d4f6a1f1 100644 --- a/app/api/integrations/github/select-repos/route.ts +++ b/app/api/integrations/github/select-repos/route.ts @@ -1,8 +1,10 @@ import { NextRequest, NextResponse } from "next/server"; -import { isHttpError, requireAuth } from "@/lib/middleware"; +import { isHttpError, requireAuth } from "@/lib/api-auth"; import prisma from "@/lib/prisma"; +import { sanitizeErrorMessage } from "@/lib/utils/rateLimit"; import { toJsonSafe } from "@/lib/utils/jsonSafe"; import { GitHubRateLimitError } from "@/lib/services/githubService"; +import { isValidRepositoryIdentifier } from "@/lib/utils/validators"; export async function POST(request: NextRequest) { try { @@ -21,6 +23,13 @@ export async function POST(request: NextRequest) { ); } + if (!repoFullNames.every(isValidRepositoryIdentifier)) { + return NextResponse.json( + { error: "Invalid repository identifier format in selection" }, + { status: 400 }, + ); + } + // Upsert selected repos as enabled. await prisma.$transaction(async (tx) => { for (const fullName of repoFullNames) { @@ -67,7 +76,7 @@ export async function POST(request: NextRequest) { return NextResponse.json({ repos: toJsonSafe(repos) }, { status: 200 }); } catch (error: any) { - console.error("GitHub select repos error:", error); + console.error("GitHub select repos error:", sanitizeErrorMessage(error)); if (error instanceof GitHubRateLimitError) { return NextResponse.json( diff --git a/app/api/internal/run-analysis/route.ts b/app/api/internal/run-analysis/route.ts new file mode 100644 index 00000000..3b7d13bf --- /dev/null +++ b/app/api/internal/run-analysis/route.ts @@ -0,0 +1,57 @@ +import { NextResponse } from 'next/server'; +import { startAnalysisWorkerLoop } from '../../../../scripts/analysisWorker'; + +export const dynamic = 'force-dynamic'; +export const maxDuration = 300; // Allow maximum serverless execution time + +export async function GET(request: Request) { + // Simple auth check for internal cron jobs + const authHeader = request.headers.get('authorization'); + if ( + process.env.ANALYSIS_RUNNER_SECRET && + authHeader !== `Bearer ${process.env.ANALYSIS_RUNNER_SECRET}` + ) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const { searchParams } = new URL(request.url); + const timeBudgetStr = searchParams.get('timeBudgetMs'); + + // Default to 45 seconds (45000ms) to allow graceful shutdown before typical 60s timeout + // Can be configured via query parameter for longer-running environments + let timeBudgetMs = 45000; + if (timeBudgetStr) { + const parsed = parseInt(timeBudgetStr, 10); + if (!isNaN(parsed) && parsed > 0) { + timeBudgetMs = parsed; + } + } + + try { + const startTime = Date.now(); + console.log(`Starting analysis cron run with time budget: ${timeBudgetMs}ms`); + + // Run the worker loop. It will exit when timeBudgetMs is reached or if it runs out of jobs (since it's not a daemon in this context) + // Wait, we don't want it to run indefinitely if there are no jobs. + // If we set once=false, it will loop until timeBudgetMs. If there are no jobs, it will sleep. + // That's acceptable since we want it to process as many jobs as possible within the budget. + // However, if we just want it to process one job and exit, we'd set once: true. + // Let's pass once: false so it acts as a queue processor within the time budget. + await startAnalysisWorkerLoop({ + once: false, + timeBudgetMs + }); + + const elapsed = Date.now() - startTime; + console.log(`Finished analysis cron run in ${elapsed}ms`); + + return NextResponse.json({ + success: true, + message: `Cron execution finished in ${elapsed}ms`, + elapsedMs: elapsed + }); + } catch (error: any) { + console.error('run-analysis cron error:', error instanceof Error ? error.message : "Unknown error"); + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }); + } +} diff --git a/app/api/repositories/[id]/analyze/route.ts b/app/api/repositories/[id]/analyze/route.ts index f9ce2dc6..5922c2dc 100644 --- a/app/api/repositories/[id]/analyze/route.ts +++ b/app/api/repositories/[id]/analyze/route.ts @@ -1,5 +1,5 @@ import { NextRequest, NextResponse } from "next/server"; -import { isHttpError, requireAuth } from "@/lib/middleware"; +import { isHttpError, requireAuth } from "@/lib/api-auth"; import { repositoryService } from "@/lib/services/repositoryService"; import { analysisJobService } from "@/lib/services/analysisJobService"; @@ -9,11 +9,11 @@ export async function POST( ) { try { const user = await requireAuth(request); - const id = parseInt(params.id); + const id = Number(params.id); - if (isNaN(id)) { + if (!Number.isInteger(id) || id <= 0) { return NextResponse.json( - { error: "Invalid repository ID" }, + { error: "Invalid repository ID. Must be a positive integer." }, { status: 400 } ); } @@ -22,10 +22,7 @@ export async function POST( const repository = await repositoryService.getRepository(id, user.userId); if (!repository) { - return NextResponse.json( - { error: "Repository not found" }, - { status: 404 } - ); + return NextResponse.json({ error: "Not Found" }, { status: 404 }); } const job = await analysisJobService.createRepositoryAnalysisJob({ @@ -45,9 +42,6 @@ export async function POST( { status: error.status } ); } - return NextResponse.json( - { error: "Failed to start analysis" }, - { status: 500 } - ); + return NextResponse.json({ error: "Internal Server Error" }, { status: 500 }); } } diff --git a/app/api/repositories/[id]/languages/route.ts b/app/api/repositories/[id]/languages/route.ts new file mode 100644 index 00000000..4100d705 --- /dev/null +++ b/app/api/repositories/[id]/languages/route.ts @@ -0,0 +1,83 @@ +import { NextRequest, NextResponse } from "next/server"; +import { verifyToken } from "@/lib/auth"; +import prisma from "@/lib/prisma"; + +export async function POST( + request: NextRequest, + { params }: { params: { id: string } } +) { + try { + const authHeader = request.headers.get("Authorization"); + if (!authHeader?.startsWith("Bearer ")) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + const token = authHeader.substring(7); + const user = verifyToken(token); + if (!user) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const repositoryId = Number(params.id); + + if (!Number.isInteger(repositoryId) || repositoryId <= 0) { + return NextResponse.json( + { error: "Invalid repository ID. Must be a positive integer." }, + { status: 400 } + ); + } + + // Verify ownership + const repository = await prisma.repository.findFirst({ + where: { id: repositoryId, userId: user.userId }, + }); + + if (!repository) { + return NextResponse.json({ error: "Repository not found" }, { status: 404 }); + } + + const body = await request.json(); + const { name, percentage, bytes, lines } = body; + + if (!name || typeof name !== "string") { + return NextResponse.json( + { error: "Language name is required" }, + { status: 400 } + ); + } + + const trimmedName = name.trim(); + if (!trimmedName) { + return NextResponse.json( + { error: "Language name cannot be empty or whitespace-only" }, + { status: 400 } + ); + } + + const language = await prisma.language.create({ + data: { + name: trimmedName, + percentage: Number(percentage) || 0, + bytes: Number(bytes) || 0, + lines: Number(lines) || 0, + repositoryId, + }, + }); + + return NextResponse.json(language, { status: 201 }); + } catch (error: any) { + console.error("Create language error:", error); + + // Handle duplicate entries explicitly + if (error?.code === "P2002") { + return NextResponse.json( + { error: "Language already exists for this repository" }, + { status: 409 } + ); + } + + return NextResponse.json( + { error: "Internal Server Error" }, + { status: 500 } + ); + } +} diff --git a/app/api/repositories/[id]/readme/route.ts b/app/api/repositories/[id]/readme/route.ts index d460b553..97ffa22d 100644 --- a/app/api/repositories/[id]/readme/route.ts +++ b/app/api/repositories/[id]/readme/route.ts @@ -1,7 +1,6 @@ import { NextRequest, NextResponse } from "next/server"; -import { isHttpError, requireAuth } from "@/lib/middleware"; +import { isHttpError, requireAuth } from "@/lib/api-auth"; import { repositoryService } from "@/lib/services/repositoryService"; -import { GitHubRateLimitError } from "@/lib/services/githubService"; export async function POST( request: NextRequest, @@ -11,9 +10,9 @@ export async function POST( const user = await requireAuth(request); const id = Number(params.id); - if (!Number.isFinite(id)) { + if (!Number.isInteger(id) || id <= 0) { return NextResponse.json( - { error: "Invalid repository ID" }, + { error: "Invalid repository ID. Must be a positive integer." }, { status: 400 }, ); } @@ -34,13 +33,6 @@ export async function POST( } catch (error: any) { console.error("Fetch README error:", error); - if (error instanceof GitHubRateLimitError) { - return NextResponse.json( - { error: error.message, retryAfter: error.retryAfterSeconds }, - { status: 429 } - ); - } - if (isHttpError(error)) { return NextResponse.json( { error: error.message }, @@ -49,7 +41,7 @@ export async function POST( } if (error?.message === "Repository not found") { - return NextResponse.json({ error: error.message }, { status: 404 }); + return NextResponse.json({ error: "Not Found" }, { status: 404 }); } return NextResponse.json( diff --git a/app/api/repositories/[id]/route.ts b/app/api/repositories/[id]/route.ts index fde22807..eba7547d 100644 --- a/app/api/repositories/[id]/route.ts +++ b/app/api/repositories/[id]/route.ts @@ -1,5 +1,5 @@ import { NextRequest, NextResponse } from "next/server"; -import { isHttpError, requireAuth } from "@/lib/middleware"; +import { isHttpError, requireAuth } from "@/lib/api-auth"; import prisma from "@/lib/prisma"; import { repositoryService } from "@/lib/services/repositoryService"; @@ -9,11 +9,11 @@ export async function GET( ) { try { const user = await requireAuth(request); - const id = parseInt(params.id); + const id = Number(params.id); - if (isNaN(id)) { + if (!Number.isInteger(id) || id <= 0) { return NextResponse.json( - { error: "Invalid repository ID" }, + { error: "Invalid repository ID. Must be a positive integer." }, { status: 400 } ); } @@ -21,10 +21,7 @@ export async function GET( const repository = await repositoryService.getRepository(id, user.userId); if (!repository) { - return NextResponse.json( - { error: "Repository not found" }, - { status: 404 } - ); + return NextResponse.json({ error: "Not Found" }, { status: 404 }); } const latestJob = await prisma.analysisJob.findFirst({ @@ -58,10 +55,7 @@ export async function GET( ); } - return NextResponse.json( - { error: "Failed to get repository" }, - { status: 500 } - ); + return NextResponse.json({ error: "Internal Server Error" }, { status: 500 }); } } @@ -71,11 +65,11 @@ export async function DELETE( ) { try { const user = await requireAuth(request); - const id = parseInt(params.id); + const id = Number(params.id); - if (isNaN(id)) { + if (!Number.isInteger(id) || id <= 0) { return NextResponse.json( - { error: "Invalid repository ID" }, + { error: "Invalid repository ID. Must be a positive integer." }, { status: 400 } ); } @@ -93,13 +87,10 @@ export async function DELETE( ); } - if (error.message === "Repository not found") { - return NextResponse.json({ error: error.message }, { status: 404 }); + if (error?.message === "Repository not found") { + return NextResponse.json({ error: "Not Found" }, { status: 404 }); } - return NextResponse.json( - { error: "Failed to delete repository" }, - { status: 500 } - ); + return NextResponse.json({ error: "Internal Server Error" }, { status: 500 }); } } diff --git a/app/api/repositories/[id]/stats/route.ts b/app/api/repositories/[id]/stats/route.ts index 763f463c..695607d1 100644 --- a/app/api/repositories/[id]/stats/route.ts +++ b/app/api/repositories/[id]/stats/route.ts @@ -1,5 +1,5 @@ import { NextRequest, NextResponse } from "next/server"; -import { requireAuth } from "@/lib/middleware"; +import { isHttpError, requireAuth } from "@/lib/api-auth"; import { repositoryService } from "@/lib/services/repositoryService"; export async function GET( @@ -8,11 +8,11 @@ export async function GET( ) { try { const user = await requireAuth(request); - const id = parseInt(params.id); + const id = Number(params.id); - if (isNaN(id)) { + if (!Number.isInteger(id) || id <= 0) { return NextResponse.json( - { error: "Invalid repository ID" }, + { error: "Invalid repository ID. Must be a positive integer." }, { status: 400 } ); } @@ -22,9 +22,15 @@ export async function GET( return NextResponse.json({ stats }); } catch (error: any) { console.error("Get repository stats error:", error); - return NextResponse.json( - { error: "Failed to get repository statistics" }, - { status: 500 } - ); + if (isHttpError(error)) { + return NextResponse.json( + { error: error.message }, + { status: error.status } + ); + } + if (error?.message === "Repository not found") { + return NextResponse.json({ error: "Not Found" }, { status: 404 }); + } + return NextResponse.json({ error: "Internal Server Error" }, { status: 500 }); } } diff --git a/app/api/repositories/route.ts b/app/api/repositories/route.ts index 390d8daf..fa095280 100644 --- a/app/api/repositories/route.ts +++ b/app/api/repositories/route.ts @@ -1,40 +1,130 @@ import { NextRequest, NextResponse } from "next/server"; -import { isHttpError, requireAuth } from "@/lib/middleware"; +import { isHttpError, requireAuth } from "@/lib/api-auth"; import { repositoryService } from "@/lib/services/repositoryService"; import { analysisJobService } from "@/lib/services/analysisJobService"; export async function POST(request: NextRequest) { try { const user = await requireAuth(request); - const body = await request.json(); + + let body; + try { + body = await request.json(); + } catch (e) { + return NextResponse.json( + { error: "Malformed JSON body" }, + { status: 400 } + ); + } + + if (!body || typeof body !== "object" || Array.isArray(body)) { + return NextResponse.json( + { error: "Request body must be a JSON object" }, + { status: 400 } + ); + } + const { name, url, description } = body; console.log("Create repository request:", { name, - url, userId: user.userId, }); - if (!name || !url) { + if (name === undefined || name === null) { return NextResponse.json( - { error: "Name and URL are required" }, + { error: "Repository name is required" }, { status: 400 } ); } - // Validate URL format - const urlPattern = /^https?:\/\/.+/; - if (!urlPattern.test(url)) { + if (typeof name !== "string") { return NextResponse.json( - { error: "Invalid repository URL" }, + { error: "Repository name must be a non-empty string" }, { status: 400 } ); } + const trimmedName = name.trim(); + if (!trimmedName) { + return NextResponse.json( + { error: "Repository name must be a non-empty string" }, + { status: 400 } + ); + } + + if (trimmedName.length > 100) { + return NextResponse.json( + { error: "Repository name must be 100 characters or less" }, + { status: 400 } + ); + } + + if (url === undefined || url === null) { + return NextResponse.json( + { error: "Repository URL is required" }, + { status: 400 } + ); + } + + if (typeof url !== "string") { + return NextResponse.json( + { error: "Repository URL must be a non-empty string" }, + { status: 400 } + ); + } + + const trimmedUrl = url.trim(); + if (!trimmedUrl) { + return NextResponse.json( + { error: "Repository URL must be a non-empty string" }, + { status: 400 } + ); + } + + if (trimmedUrl.length > 2000) { + return NextResponse.json( + { error: "Repository URL must be 2000 characters or less" }, + { status: 400 } + ); + } + + try { + const parsedUrl = new URL(trimmedUrl); + if (parsedUrl.protocol !== "http:" && parsedUrl.protocol !== "https:") { + return NextResponse.json( + { error: "Repository URL must use HTTP or HTTPS protocol" }, + { status: 400 } + ); + } + } catch (e) { + return NextResponse.json( + { error: "Invalid repository URL format" }, + { status: 400 } + ); + } + + let trimmedDescription: string | undefined = undefined; + if (description !== undefined && description !== null) { + if (typeof description !== "string") { + return NextResponse.json( + { error: "Repository description must be a string" }, + { status: 400 } + ); + } + trimmedDescription = description.trim(); + if (trimmedDescription.length > 1000) { + return NextResponse.json( + { error: "Repository description must be 1000 characters or less" }, + { status: 400 } + ); + } + } + const repository = await repositoryService.createRepository({ - name, - url, - description, + name: trimmedName, + url: trimmedUrl, + description: trimmedDescription || undefined, userId: user.userId, }); @@ -51,17 +141,13 @@ export async function POST(request: NextRequest) { ); } catch (error: any) { console.error("Create repository error:", error); - console.error("Error stack:", error.stack); if (isHttpError(error)) { return NextResponse.json( { error: error.message }, { status: error.status } ); } - return NextResponse.json( - { error: "Failed to create repository" }, - { status: 500 } - ); + return NextResponse.json({ error: "Internal Server Error" }, { status: 500 }); } } @@ -72,16 +158,13 @@ export async function GET(request: NextRequest) { return NextResponse.json({ repositories }); } catch (error: any) { - console.error("List repositories error:", error); + console.error("List repositories error:", error instanceof Error ? error.message : "Unknown error"); if (isHttpError(error)) { return NextResponse.json( { error: error.message }, { status: error.status } ); } - return NextResponse.json( - { error: "Failed to list repositories" }, - { status: 500 } - ); + return NextResponse.json({ error: "Internal Server Error" }, { status: 500 }); } } diff --git a/app/api/users/change-password/route.ts b/app/api/users/change-password/route.ts index ab0ab24d..092e52b7 100644 --- a/app/api/users/change-password/route.ts +++ b/app/api/users/change-password/route.ts @@ -1,7 +1,7 @@ import { NextRequest, NextResponse } from "next/server"; import bcrypt from "bcryptjs"; import prisma from "@/lib/prisma"; -import { requireAuth } from "@/lib/middleware"; +import { requireAuth } from "@/lib/api-auth"; export async function POST(request: NextRequest) { try { @@ -28,7 +28,7 @@ export async function POST(request: NextRequest) { }); if (!userDetails) { - return NextResponse.json({ message: "User not found" }, { status: 404 }); + return NextResponse.json({ error: "Not Found" }, { status: 404 }); } const passwordHash = @@ -47,10 +47,7 @@ export async function POST(request: NextRequest) { ); if (!isPasswordValid) { - return NextResponse.json( - { message: "Current password is incorrect" }, - { status: 401 } - ); + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } } @@ -64,9 +61,6 @@ export async function POST(request: NextRequest) { return NextResponse.json({ message: "Password changed successfully" }); } catch (error: any) { console.error("Error changing password:", error); - return NextResponse.json( - { message: "Failed to change password" }, - { status: 500 } - ); + return NextResponse.json({ error: "Internal Server Error" }, { status: 500 }); } } diff --git a/app/api/users/me/route.ts b/app/api/users/me/route.ts index 02d01429..14ab8e36 100644 --- a/app/api/users/me/route.ts +++ b/app/api/users/me/route.ts @@ -1,6 +1,7 @@ import { NextRequest, NextResponse } from "next/server"; import prisma from "@/lib/prisma"; -import { requireAuth } from "@/lib/middleware"; +import { requireAuth } from "@/lib/api-auth"; +import { sanitizeErrorMessage } from "@/lib/utils/rateLimit"; export const dynamic = "force-dynamic"; export const runtime = "nodejs"; @@ -26,24 +27,28 @@ export async function GET(request: NextRequest) { })) > 0; if (!userDetails) { - return NextResponse.json({ message: "User not found" }, { status: 404 }); + return NextResponse.json({ error: "Not Found" }, { status: 404 }); } - return NextResponse.json({ - id: userDetails.id, - name: userDetails.name, - email: userDetails.email, - image: userDetails.image, - createdAt: userDetails.createdAt, - avatarUrl: (userDetails as any).image, - isGoogleLinked: hasGoogleAccount, - }); - } catch (error: any) { - console.error("Error fetching user:", error); return NextResponse.json( - { message: "Failed to fetch user" }, - { status: 500 } + { + id: userDetails.id, + name: userDetails.name, + email: userDetails.email, + image: userDetails.image, + createdAt: userDetails.createdAt, + avatarUrl: (userDetails as any).image, + isGoogleLinked: hasGoogleAccount, + }, + { + headers: { + "Cache-Control": "no-store", + }, + } ); + } catch (error: any) { + console.error("Error fetching user:", sanitizeErrorMessage(error)); + return NextResponse.json({ error: "Internal Server Error" }, { status: 500 }); } } @@ -57,13 +62,10 @@ export async function DELETE(request: NextRequest) { return NextResponse.json({ message: "Account deleted" }); } catch (error: any) { - console.error("Error deleting account:", error); + console.error("Error deleting account:", error instanceof Error ? error.message : "Unknown error"); if (error?.code === "P2025") { - return NextResponse.json({ message: "User not found" }, { status: 404 }); + return NextResponse.json({ error: "Not Found" }, { status: 404 }); } - return NextResponse.json( - { message: "Failed to delete account" }, - { status: 500 } - ); + return NextResponse.json({ error: "Internal Server Error" }, { status: 500 }); } } diff --git a/app/api/users/profile/route.ts b/app/api/users/profile/route.ts index 6fa13827..23506bee 100644 --- a/app/api/users/profile/route.ts +++ b/app/api/users/profile/route.ts @@ -1,6 +1,7 @@ import { NextRequest, NextResponse } from "next/server"; import prisma from "@/lib/prisma"; -import { requireAuth } from "@/lib/middleware"; +import { requireAuth } from "@/lib/api-auth"; +import { sanitizeErrorMessage } from "@/lib/utils/rateLimit"; import bcrypt from "bcryptjs"; export async function PUT(request: NextRequest) { @@ -96,10 +97,7 @@ export async function PUT(request: NextRequest) { avatarUrl: (updatedUser as any).image, }); } catch (error: any) { - console.error("Error updating profile:", error); - return NextResponse.json( - { message: "Failed to update profile" }, - { status: 500 } - ); + console.error("Error updating profile:", sanitizeErrorMessage(error)); + return NextResponse.json({ error: "Internal Server Error" }, { status: 500 }); } } diff --git a/app/api/webhooks/github/route.ts b/app/api/webhooks/github/route.ts index eba6ba72..afc2d847 100644 --- a/app/api/webhooks/github/route.ts +++ b/app/api/webhooks/github/route.ts @@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from "next/server"; import { verifyGitHubWebhookSignature } from "@/lib/utils/githubWebhook"; import { GitHubAppService } from "@/lib/services/githubAppService"; import { GitHubService } from "@/lib/services/githubService"; +import { sanitizeErrorMessage } from "@/lib/utils/rateLimit"; import prisma from "@/lib/prisma"; import { formatPRReviewMarkdown, @@ -36,6 +37,13 @@ function shouldHandlePullRequestAction(action: string | undefined): boolean { ); } +function noStoreResponse(data: unknown, status = 200) { + return NextResponse.json(data, { + status, + headers: { "Cache-Control": "no-store" }, + }); +} + export async function POST(request: NextRequest) { const rawBody = await request.text(); @@ -50,45 +58,33 @@ export async function POST(request: NextRequest) { webhookSecret: secret, }) ) { - return NextResponse.json({ error: "Invalid signature" }, { status: 401 }); + return noStoreResponse({ error: "Invalid signature" }, 401); } if (event !== "pull_request") { - return NextResponse.json( - { ok: true, ignored: true, event }, - { status: 200 }, - ); + return noStoreResponse({ ok: true, ignored: true, event }); } let payload: PullRequestWebhookPayload; try { payload = JSON.parse(rawBody); } catch { - return NextResponse.json({ error: "Invalid JSON" }, { status: 400 }); + return noStoreResponse({ error: "Invalid JSON" }, 400); } const action = payload.action; if (!shouldHandlePullRequestAction(action)) { - return NextResponse.json( - { ok: true, ignored: true, action }, - { status: 200 }, - ); + return noStoreResponse({ ok: true, ignored: true, action }); } // Ignore draft PRs until they become ready_for_review if (payload.pull_request?.draft && action !== "ready_for_review") { - return NextResponse.json( - { ok: true, ignored: true, reason: "draft" }, - { status: 200 }, - ); + return noStoreResponse({ ok: true, ignored: true, reason: "draft" }); } // Avoid replying to bots (including ourselves) if (payload.sender?.type === "Bot") { - return NextResponse.json( - { ok: true, ignored: true, reason: "bot" }, - { status: 200 }, - ); + return noStoreResponse({ ok: true, ignored: true, reason: "bot" }); } const owner = payload.repository?.owner?.login; @@ -97,20 +93,14 @@ export async function POST(request: NextRequest) { const installationId = payload.installation?.id; if (!owner || !repo || !number || !installationId) { - return NextResponse.json( - { - error: "Missing required fields", - details: { owner, repo, number, installationId }, - }, - { status: 400 }, - ); + return noStoreResponse({ error: "Missing required fields" }, 400); } try { const repoFullName = `${owner}/${repo}`; // Gate by DB selection: only auto-review repos that users explicitly enabled. - // Hackathon scope: if *any* user enabled this repo, we process and attach the PR to that GitHubRepo row. + // Verify the installation belongs to a registered user to enforce ownership. const enabledRepo = await prisma.gitHubRepo.findFirst({ where: { repoFullName, @@ -124,10 +114,7 @@ export async function POST(request: NextRequest) { }); if (!enabledRepo) { - return NextResponse.json( - { ok: true, ignored: true, reason: "repo_not_enabled", repoFullName }, - { status: 200 }, - ); + return noStoreResponse({ ok: true, ignored: true, reason: "repo_not_enabled" }); } // Backfill installationId for future lookups. @@ -148,12 +135,9 @@ export async function POST(request: NextRequest) { const pr = await github.getPullRequest(owner, repo, number); const headSha = pr?.head?.sha; if (!headSha) { - return NextResponse.json( - { - error: "Missing head SHA from GitHub PR response", - details: { owner, repo, number }, - }, - { status: 500 }, + return noStoreResponse( + { error: "Missing head SHA from GitHub PR response" }, + 500 ); } @@ -202,17 +186,11 @@ export async function POST(request: NextRequest) { }); } catch (e: any) { if (e?.code === "P2002") { - return NextResponse.json( - { - ok: true, - ignored: true, - reason: "already_reviewed", - repoFullName, - prNumber: number, - headSha, - }, - { status: 200 }, - ); + return noStoreResponse({ + ok: true, + ignored: true, + reason: "already_reviewed", + }); } throw e; } @@ -277,24 +255,30 @@ export async function POST(request: NextRequest) { }, }); - return NextResponse.json( - { - ok: true, - posted: postedUrl, - postError, - stored: { - pullRequestId: prRecord.id, - prReviewId: reviewRow.id, - headSha, - }, + return noStoreResponse({ + ok: true, + posted: postedUrl, + postError, + stored: { + pullRequestId: prRecord.id, + prReviewId: reviewRow.id, + headSha, }, - { status: 200 }, - ); + }); } catch (error: any) { - console.error("GitHub webhook PR review error:", error); + const safeError = isAxiosError(error) + ? { + message: error.message, + status: error.response?.status, + data: error.response?.data, + url: error.config?.url, + } + : error; + console.error("GitHub webhook PR review error:", safeError); return NextResponse.json( { error: "Failed to process PR webhook", + details: error?.message || "Unknown error", }, { status: 500 }, ); diff --git a/app/login/page.tsx b/app/login/page.tsx index c55d770e..297e38b0 100644 --- a/app/login/page.tsx +++ b/app/login/page.tsx @@ -1,7 +1,12 @@ 'use client' import Login from '@/pages/Login' +import { Suspense } from "react"; export default function LoginPage() { - return + return ( + + + + ); } diff --git a/app/signup/page.tsx b/app/signup/page.tsx index acb273ed..96ac54e2 100644 --- a/app/signup/page.tsx +++ b/app/signup/page.tsx @@ -1,7 +1,12 @@ 'use client' import Signup from '@/pages/Signup' +import { Suspense } from "react"; export default function SignupPage() { - return + return ( + + + + ); } diff --git a/dist-worker/lib/services/analysisJobService.js b/dist-worker/lib/services/analysisJobService.js index e31bd0af..3ff2d7f2 100644 --- a/dist-worker/lib/services/analysisJobService.js +++ b/dist-worker/lib/services/analysisJobService.js @@ -70,14 +70,22 @@ class AnalysisJobService { async markFailed(params) { const shouldRetry = params.attempts < params.maxAttempts; if (shouldRetry) { - const delay = computeBackoffMs(params.attempts); + const delay = params.retryAfter + ? params.retryAfter * 1000 + : computeBackoffMs(params.attempts); + const retryLabel = params.retryAfter + ? `Rate limited, retrying` + : `Retrying`; await prisma_1.default.analysisJob.update({ where: { id: params.jobId }, data: { status: "QUEUED", nextRunAt: new Date(Date.now() + delay), - progressMessage: `Retrying in ${Math.round(delay / 1000)}s`, + progressMessage: `${retryLabel} in ${Math.round(delay / 1000)}s`, error: params.error, + ...(params.retryAfter + ? { progressDetails: { retryAfter: params.retryAfter, rateLimited: true } } + : {}), lockedAt: null, lockedBy: null, lockExpiresAt: null, diff --git a/dist-worker/lib/services/gitService.js b/dist-worker/lib/services/gitService.js index 23912097..e91c2959 100644 --- a/dist-worker/lib/services/gitService.js +++ b/dist-worker/lib/services/gitService.js @@ -105,38 +105,11 @@ function normalizeNumstatFilePath(rawPath) { } return after; } -function shellEscapeSingleQuoted(value) { - // Safe for POSIX shells: wrap in single quotes and escape embedded single quotes. - return `'${value.replace(/'/g, `'\\''`)}'`; -} class GitService { repoPath; constructor(repoPath) { this.repoPath = repoPath; } - async listTreeNames(treePath) { - try { - const treeish = treePath ? `HEAD:${treePath}` : "HEAD"; - const { stdout } = await execPromise(`cd "${this.repoPath}" && git ls-tree --name-only ${shellEscapeSingleQuoted(treeish)}`, { timeout: DEFAULT_GIT_TIMEOUT_MS }); - return stdout - .split("\n") - .map((s) => s.trim()) - .filter(Boolean); - } - catch { - return []; - } - } - async showFileAtHead(filePath) { - try { - const spec = `HEAD:${filePath}`; - const { stdout } = await execPromise(`cd "${this.repoPath}" && git show ${shellEscapeSingleQuoted(spec)}`, { timeout: DEFAULT_GIT_TIMEOUT_MS }); - return typeof stdout === "string" ? stdout : ""; - } - catch { - return null; - } - } /** * Clone a repository to a temporary directory */ diff --git a/dist-worker/lib/services/githubService.js b/dist-worker/lib/services/githubService.js index 300a218c..37a37343 100644 --- a/dist-worker/lib/services/githubService.js +++ b/dist-worker/lib/services/githubService.js @@ -33,8 +33,56 @@ var __importStar = (this && this.__importStar) || (function () { }; })(); Object.defineProperty(exports, "__esModule", { value: true }); -exports.githubService = exports.GitHubService = void 0; +exports.githubService = exports.GitHubService = exports.GitHubRateLimitError = void 0; +exports.sanitizeGitHubError = sanitizeGitHubError; const axios_1 = __importStar(require("axios")); +class GitHubRateLimitError extends Error { + retryAfterSeconds; + constructor(retryAfterSeconds) { + super(`GitHub API rate limit reached. Please retry after ${retryAfterSeconds} seconds.`); + this.name = "GitHubRateLimitError"; + this.retryAfterSeconds = retryAfterSeconds; + } +} +exports.GitHubRateLimitError = GitHubRateLimitError; +function sanitizeGitHubHeaders(headers) { + if (headers == null) { + return headers; + } + if (Array.isArray(headers)) { + return headers.map((value) => sanitizeGitHubHeaders(value)); + } + if (typeof headers !== "object") { + return headers; + } + const source = typeof headers.toJSON === "function" ? headers.toJSON() : headers; + if (source == null || typeof source !== "object") { + return source; + } + const sanitized = Array.isArray(source) ? [] : {}; + for (const [key, value] of Object.entries(source)) { + if (key.toLowerCase() === "authorization") { + sanitized[key] = "[REDACTED]"; + } + else if (value != null && typeof value === "object") { + sanitized[key] = sanitizeGitHubHeaders(value); + } + else { + sanitized[key] = value; + } + } + return sanitized; +} +function sanitizeGitHubError(error) { + if ((0, axios_1.isAxiosError)(error) && error.config) { + const safeConfig = { + ...error.config, + headers: sanitizeGitHubHeaders(error.config.headers), + }; + error.config = safeConfig; + } + return error; +} class GitHubService { client; token; @@ -48,6 +96,42 @@ class GitHubService { ...(token && { Authorization: `Bearer ${token}` }), }, }); + this.client.interceptors.response.use((response) => response, async (error) => { + if (!(0, axios_1.isAxiosError)(error) || !error.config) { + throw sanitizeGitHubError(error); + } + const status = error.response?.status; + const config = error.config; + if (status === 429 || status === 403) { + const rateLimitRemaining = error.response?.headers?.["x-ratelimit-remaining"]; + if (status === 429 || rateLimitRemaining === "0") { + const retryAfterHeader = error.response?.headers?.["retry-after"]; + const resetHeader = error.response?.headers?.["x-ratelimit-reset"]; + let retrySeconds = 60; + if (retryAfterHeader) { + retrySeconds = parseInt(retryAfterHeader, 10); + } + else if (resetHeader) { + const resetTime = parseInt(resetHeader, 10) * 1000; + retrySeconds = Math.max(1, Math.ceil((resetTime - Date.now()) / 1000)); + } + throw new GitHubRateLimitError(retrySeconds); + } + } + const retryStatusCodes = [502, 503, 504]; + if ((status && retryStatusCodes.includes(status)) || + error.code === "ECONNABORTED" || + !error.response) { + config.retryCount = config.retryCount || 0; + if (config.retryCount < 3) { + config.retryCount += 1; + const backoff = Math.pow(2, config.retryCount) * 1000 + Math.random() * 1000; + await new Promise((resolve) => setTimeout(resolve, backoff)); + return this.client(config); + } + } + throw sanitizeGitHubError(error); + }); } /** * Get authenticated user information diff --git a/dist-worker/lib/services/repositoryService.js b/dist-worker/lib/services/repositoryService.js index 12eedfdb..bc9bf560 100644 --- a/dist-worker/lib/services/repositoryService.js +++ b/dist-worker/lib/services/repositoryService.js @@ -80,39 +80,6 @@ class RepositoryService { return null; } } - async tryReadmeFromGit(gitService) { - const candidates = [ - "readme.md", - "readme.markdown", - "readme.mdx", - "readme.txt", - "readme.rst", - "readme", - ]; - const scanDirs = ["", "docs", ".github"]; - for (const dir of scanDirs) { - const names = await gitService.listTreeNames(dir || undefined); - if (!names.length) - continue; - const byLower = new Map(names.map((n) => [n.toLowerCase(), n])); - for (const lower of candidates) { - const actual = byLower.get(lower); - if (!actual) - continue; - const relPath = dir ? `${dir}/${actual}` : actual; - const content = await gitService.showFileAtHead(relPath); - if (!content) - continue; - const trimmed = content.trim(); - if (!trimmed) - continue; - const maxChars = 200_000; - const safeText = trimmed.length > maxChars ? trimmed.slice(0, maxChars) : trimmed; - return { path: relPath, text: safeText }; - } - } - return null; - } async fetchAndStoreReadme(repositoryId, userId) { const repository = await prisma_1.default.repository.findFirst({ where: { id: repositoryId, userId }, @@ -129,10 +96,7 @@ class RepositoryService { depth: 1, noSingleBranch: false, }); - let readme = await this.tryReadmeFromRepoPath(tempDir); - if (!readme) { - readme = await this.tryReadmeFromGit(gitService); - } + const readme = await this.tryReadmeFromRepoPath(tempDir); const updated = await prisma_1.default.repository.update({ where: { id: repositoryId }, data: { @@ -219,18 +183,7 @@ class RepositoryService { gitService = await gitService_1.GitService.cloneRepository(repository.url, tempDir); // Capture README early (best-effort) await report({ progressPercent: 8, progressMessage: "Reading README" }); - let readme = await this.tryReadmeFromRepoPath(tempDir); - if (!readme) { - readme = await this.tryReadmeFromGit(gitService); - } - console.log(`README capture for repo ${repositoryId}: ${readme ? "found" : "missing"}${readme?.path ? ` (${readme.path})` : ""}`); - await report({ - progressDetails: { - readmeFound: Boolean(readme), - readmePath: readme?.path ?? null, - readmeChars: readme?.text?.length ?? 0, - }, - }); + const readme = await this.tryReadmeFromRepoPath(tempDir); await prisma_1.default.repository.update({ where: { id: repositoryId }, data: { diff --git a/dist-worker/lib/utils/rateLimit.js b/dist-worker/lib/utils/rateLimit.js new file mode 100644 index 00000000..71004e07 --- /dev/null +++ b/dist-worker/lib/utils/rateLimit.js @@ -0,0 +1,67 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.isRateLimitError = isRateLimitError; +exports.extractRetryAfter = extractRetryAfter; +exports.sanitizeErrorMessage = sanitizeErrorMessage; +const axios_1 = require("axios"); +const RATE_LIMIT_PATTERNS = [ + "rate limit", + "api rate limit", + "secondary rate limit", + "abuse limit", + "too many requests", + "429", +]; +function isRateLimitError(err) { + if (err instanceof axios_1.AxiosError) { + if (err.response?.status === 429) + return true; + const retryAfter = err.response?.headers?.["retry-after"]; + if (retryAfter != null) + return true; + const xRateLimit = err.response?.headers?.["x-ratelimit-remaining"]; + if (xRateLimit === "0") + return true; + } + if (err instanceof Error) { + const msg = err.message.toLowerCase(); + return RATE_LIMIT_PATTERNS.some((p) => msg.includes(p)); + } + return false; +} +function extractRetryAfter(err) { + if (!(err instanceof axios_1.AxiosError) || !err.response) + return null; + const header = err.response.headers?.["retry-after"]; + if (header == null) + return null; + const parsed = parseInt(header, 10); + if (Number.isFinite(parsed) && parsed > 0) + return parsed; + const date = new Date(header); + if (!isNaN(date.getTime())) { + const seconds = Math.max(0, Math.ceil((date.getTime() - Date.now()) / 1000)); + return seconds > 0 ? seconds : null; + } + return null; +} +function sanitizeErrorMessage(err) { + if (err instanceof axios_1.AxiosError) { + const status = err.response?.status; + const message = err.response?.data?.message || err.message; + const safe = stripSensitive(String(message)); + return status ? `[${status}] ${safe}` : safe; + } + if (err instanceof Error) { + return stripSensitive(err.message); + } + return stripSensitive(String(err)); +} +function stripSensitive(text) { + return text + .replace(/Bearer\s+[A-Za-z0-9\-._~+/]+=*/gi, "Bearer [REDACTED]") + .replace(/token[=:]\s*[A-Za-z0-9\-._~+/]+=*/gi, "token=[REDACTED]") + .replace(/gh[pousr]_[A-Za-z0-9_]{36,}/gi, "[REDACTED]") + .replace(/https?:\/\/[^@\s]+:[^@\s]+@/g, "https://[REDACTED]@") + .replace(/Authorization:\s*.+/gi, "Authorization: [REDACTED]"); +} diff --git a/dist-worker/scripts/analysisWorker.js b/dist-worker/scripts/analysisWorker.js index c4df80fb..6f44a36c 100644 --- a/dist-worker/scripts/analysisWorker.js +++ b/dist-worker/scripts/analysisWorker.js @@ -9,6 +9,7 @@ const os_1 = __importDefault(require("os")); const prisma_1 = __importDefault(require("../lib/prisma")); const analysisJobService_1 = require("../lib/services/analysisJobService"); const repositoryService_1 = require("../lib/services/repositoryService"); +const rateLimit_1 = require("../lib/utils/rateLimit"); const POLL_INTERVAL_MS = 2000; const HEARTBEAT_INTERVAL_MS = 30_000; const LOCK_MS = 5 * 60_000; @@ -56,7 +57,7 @@ async function runJob(job, params) { workerId: params.workerId, lockMs: params.lockMs, }) - .catch((e) => console.error("heartbeat failed", e)); + .catch((e) => console.error("heartbeat failed", (0, rateLimit_1.sanitizeErrorMessage)(e))); }, params.heartbeatIntervalMs); if (job.type !== "repository_analysis") { throw new Error(`Unsupported job type: ${job.type}`); @@ -70,17 +71,28 @@ async function runJob(job, params) { jobId: job.id, workerId: params.workerId, }); + return true; } catch (err) { - const message = err?.message ? String(err.message) : String(err); - console.error(`Job ${job.id} failed:`, err); + const rateLimited = (0, rateLimit_1.isRateLimitError)(err); + const retryAfter = rateLimited ? (0, rateLimit_1.extractRetryAfter)(err) : null; + const safeMessage = (0, rateLimit_1.sanitizeErrorMessage)(err); + if (rateLimited) { + console.error(`Job ${job.id} rate limited (attempt ${job.attempts}/${job.maxAttempts})` + + (retryAfter ? `, retry after ${retryAfter}s` : "")); + } + else { + console.error(`Job ${job.id} failed: ${safeMessage}`); + } await analysisJobService_1.analysisJobService.markFailed({ jobId: job.id, workerId: params.workerId, - error: message, + error: safeMessage, attempts: job.attempts, maxAttempts: job.maxAttempts, + retryAfter: retryAfter ?? undefined, }); + return false; } finally { if (heartbeatTimer) @@ -94,6 +106,11 @@ async function startAnalysisWorkerLoop(opts) { const lockMs = opts?.lockMs ?? LOCK_MS; console.log(`analysis worker starting: ${workerId}`); let stopping = false; + const startTimeMs = Date.now(); + let totalJobsScanned = 0; + let jobsProcessed = 0; + let jobsSkipped = 0; + let jobsFailed = 0; const shutdown = async (signal) => { if (stopping) return; @@ -116,30 +133,55 @@ async function startAnalysisWorkerLoop(opts) { lockMs, }); if (!job) { + jobsSkipped++; if (opts?.once) - return; + break; await sleep(pollIntervalMs); continue; } + totalJobsScanned++; console.log(`claimed job ${job.id} (attempt ${job.attempts}/${job.maxAttempts})`); - await runJob(job, { workerId, lockMs, heartbeatIntervalMs }); + const isSuccess = await runJob(job, { workerId, lockMs, heartbeatIntervalMs }); + if (isSuccess) { + jobsProcessed++; + } + else { + jobsFailed++; + } if (opts?.once) - return; + break; } catch (e) { - console.error("worker loop error:", e); - if (opts?.once) - return; + console.error("worker loop error:", (0, rateLimit_1.sanitizeErrorMessage)(e)); + if (opts?.once) { + return { + totalJobsScanned, + jobsProcessed, + jobsSkipped, + jobsFailed, + executionDurationMs: Date.now() - startTimeMs, + success: false, + }; + } await sleep(pollIntervalMs); } } + return { + totalJobsScanned, + jobsProcessed, + jobsSkipped, + jobsFailed, + executionDurationMs: Date.now() - startTimeMs, + success: true, + }; } // Run as standalone script // (tsc -> CJS) so `require.main === module` works after compilation. // eslint-disable-next-line @typescript-eslint/no-explicit-any const isMain = typeof require !== "undefined" && require.main === module; if (isMain) { - startAnalysisWorkerLoop().catch((e) => { + const once = !!process.env.WORKER_ONCE; + startAnalysisWorkerLoop({ once }).catch((e) => { console.error("worker fatal:", e); process.exit(1); }); diff --git a/lib/api-auth.ts b/lib/api-auth.ts new file mode 100644 index 00000000..15e44fa1 --- /dev/null +++ b/lib/api-auth.ts @@ -0,0 +1,64 @@ +import { NextRequest } from "next/server"; +import { verifyToken, JWTPayload } from "./auth"; +import { getToken } from "next-auth/jwt"; + +export interface AuthenticatedRequest { + user: JWTPayload; +} + +export class HttpError extends Error { + status: number; + + constructor(status: number, message: string) { + super(message); + this.status = status; + } +} + +export function isHttpError(error: unknown): error is HttpError { + return ( + typeof error === "object" && + error !== null && + "status" in error && + typeof (error as any).status === "number" + ); +} + +export async function getAuthUser( + request: NextRequest +): Promise { + const authHeader = request.headers.get("authorization"); + + // 1) Existing JWT auth (Authorization: Bearer ...) + if (authHeader && authHeader.startsWith("Bearer ")) { + const token = authHeader.substring(7); + const payload = verifyToken(token); + if (payload) return payload; + } + + // 2) NextAuth session cookie (Google OAuth) + try { + const token = await getToken({ + req: request, + secret: process.env.NEXTAUTH_SECRET, + }); + if (!token?.sub || !token.email) return null; + + const userId = Number(token.sub); + if (!Number.isFinite(userId)) return null; + + return { userId, email: token.email }; + } catch { + return null; + } +} + +export async function requireAuth(request: NextRequest): Promise { + const user = await getAuthUser(request); + + if (!user) { + throw new HttpError(401, "Unauthorized"); + } + + return user; +} diff --git a/lib/auth-config.ts b/lib/auth-config.ts index bc90be1d..db2aa523 100644 --- a/lib/auth-config.ts +++ b/lib/auth-config.ts @@ -5,6 +5,7 @@ import prisma from "@/lib/prisma"; import bcrypt from "bcryptjs"; import dns from "dns"; import { OAuth2Client } from "google-auth-library"; +import { withRetry, sanitizeErrorMessage } from "@/lib/utils/rateLimit"; import type { Adapter, AdapterAccount, @@ -213,47 +214,27 @@ const googleTokenVerifier = isGoogleConfigured ? new OAuth2Client({ clientId: googleClientId! }) : null; -function sleep(ms: number) { - return new Promise((resolve) => setTimeout(resolve, ms)); -} - -function isTransientNetworkError(error: unknown) { - const anyErr = error as any; - const code = anyErr?.code as string | undefined; - const message = (anyErr?.message as string | undefined) || ""; - - return ( - code === "ETIMEDOUT" || - code === "ECONNRESET" || - code === "EAI_AGAIN" || - code === "ENOTFOUND" || - message.includes("ETIMEDOUT") || - message.includes("ECONNRESET") - ); -} - async function verifyGoogleIdToken(idToken: string) { if (!googleTokenVerifier || !googleClientId) { throw new Error("Google OAuth is not configured"); } - // Retry once for intermittent network/cert-fetch issues. - for (let attempt = 0; attempt < 2; attempt++) { - try { - return await googleTokenVerifier.verifyIdToken({ + return withRetry( + async () => { + return await googleTokenVerifier!.verifyIdToken({ idToken, audience: googleClientId, }); - } catch (err) { - if (attempt === 0 && isTransientNetworkError(err)) { - await sleep(200); - continue; - } - throw err; + }, + { + maxRetries: 3, + onRetry: (attempt, err, delayMs) => { + console.warn( + `[auth] Google token verification failed (attempt ${attempt}), retrying in ${delayMs}ms. Error: ${sanitizeErrorMessage(err)}` + ); + }, } - } - - throw new Error("Google token verification failed"); + ); } if ((googleClientId || googleClientSecret) && !isGoogleConfigured) { @@ -482,8 +463,7 @@ export const authOptions: NextAuthOptions = { } catch (err: any) { // Avoid logging secrets/tokens. Provide enough context to diagnose. console.error("[auth] google oauth callback failed", { - message: err?.message, - code: err?.code, + message: sanitizeErrorMessage(err), providerAccountId: account?.providerAccountId, hasUserEmail: !!user?.email, }); diff --git a/lib/errors/AnalysisTimeoutError.ts b/lib/errors/AnalysisTimeoutError.ts new file mode 100644 index 00000000..dc9903c1 --- /dev/null +++ b/lib/errors/AnalysisTimeoutError.ts @@ -0,0 +1,12 @@ +/** + * Custom error class for analysis timeout scenarios. + * Allows typed error handling instead of relying on string matching. + */ +export class AnalysisTimeoutError extends Error { + constructor(message: string = "Analysis timeout exceeded") { + super(message); + this.name = "AnalysisTimeoutError"; + // Ensure proper prototype chain for instanceof checks + Object.setPrototypeOf(this, AnalysisTimeoutError.prototype); + } +} diff --git a/lib/middleware.ts b/lib/middleware.ts index f1c0d5bc..9d0385ce 100644 --- a/lib/middleware.ts +++ b/lib/middleware.ts @@ -1,24 +1,25 @@ -import { NextRequest } from "next/server"; -import { verifyToken, JWTPayload } from "./auth"; +import { NextResponse } from "next/server"; +import type { NextRequest } from "next/server"; import { getToken } from "next-auth/jwt"; +import { verifyToken } from "@/lib/auth"; -export interface AuthenticatedRequest { - user: JWTPayload; +export class HttpError extends Error { + constructor(public status: number, message: string) { + super(message); + this.name = "HttpError"; + Object.setPrototypeOf(this, HttpError.prototype); + } } -export async function getAuthUser( - request: NextRequest -): Promise { +export async function getAuthUser(request: NextRequest) { const authHeader = request.headers.get("authorization"); - // 1) Existing JWT auth (Authorization: Bearer ...) if (authHeader && authHeader.startsWith("Bearer ")) { const token = authHeader.substring(7); const payload = verifyToken(token); if (payload) return payload; } - // 2) NextAuth session cookie (Google OAuth) try { const token = await getToken({ req: request, @@ -35,30 +36,58 @@ export async function getAuthUser( } } -export async function requireAuth(request: NextRequest): Promise { +export async function requireAuth(request: NextRequest) { const user = await getAuthUser(request); - if (!user) { throw new HttpError(401, "Unauthorized"); } - return user; } -export class HttpError extends Error { - status: number; - - constructor(status: number, message: string) { - super(message); - this.status = status; +export function isHttpError(error: unknown): error is HttpError { + if (error instanceof HttpError) { + return true; } -} -export function isHttpError(error: unknown): error is HttpError { return ( typeof error === "object" && error !== null && + (error as Record).name === "HttpError" && "status" in error && - typeof (error as any).status === "number" + typeof (error as Record).status === "number" ); } + +export async function middleware(request: NextRequest) { + try { + const token = await getToken({ + req: request, + secret: process.env.NEXTAUTH_SECRET, + }); + + if (!token) { + return NextResponse.redirect(new URL("/login", request.url)); + } + + const userId = token.sub; + + // REMOVE header-based ownership check (untrusted input) + +// Ownership validation must be handled in route handlers +// where the actual resource can be verified server-side + + return NextResponse.next(); + + } catch (error) { + + console.error("Middleware error:", error); + return NextResponse.json( + { error: "Internal Server Error" }, + { status: 500 } + ); + } +} + +export const config = { + matcher: ["/api/:path*", "/dashboard/:path*", "/profile/:path*"], +}; \ No newline at end of file diff --git a/lib/services/analysisJobService.ts b/lib/services/analysisJobService.ts index 41536298..3778951a 100644 --- a/lib/services/analysisJobService.ts +++ b/lib/services/analysisJobService.ts @@ -94,18 +94,27 @@ export class AnalysisJobService { error: string; attempts: number; maxAttempts: number; + retryAfter?: number; }): Promise { const shouldRetry = params.attempts < params.maxAttempts; if (shouldRetry) { - const delay = computeBackoffMs(params.attempts); + const delay = params.retryAfter + ? params.retryAfter * 1000 + : computeBackoffMs(params.attempts); + const retryLabel = params.retryAfter + ? `Rate limited, retrying` + : `Retrying`; await prisma.analysisJob.update({ where: { id: params.jobId }, data: { status: "QUEUED", nextRunAt: new Date(Date.now() + delay), - progressMessage: `Retrying in ${Math.round(delay / 1000)}s`, + progressMessage: `${retryLabel} in ${Math.round(delay / 1000)}s`, error: params.error, + ...(params.retryAfter + ? { progressDetails: { retryAfter: params.retryAfter, rateLimited: true } } + : {}), lockedAt: null, lockedBy: null, lockExpiresAt: null, diff --git a/lib/services/bitbucketService.ts b/lib/services/bitbucketService.ts index b91b6ebf..2022d740 100644 --- a/lib/services/bitbucketService.ts +++ b/lib/services/bitbucketService.ts @@ -1,4 +1,34 @@ -import axios, { AxiosInstance } from 'axios' +import axios, { AxiosInstance, isAxiosError } from 'axios' +import { getRetryDelayMs } from '@/lib/utils/rateLimit' + +export class BitbucketRateLimitError extends Error { + retryAfterSeconds: number + constructor(retryAfterSeconds: number) { + super( + retryAfterSeconds > 0 + ? `Bitbucket API rate limit reached. Please retry after ${retryAfterSeconds} seconds.` + : 'Bitbucket API rate limit reached. Please try again later.', + ) + this.name = 'BitbucketRateLimitError' + this.retryAfterSeconds = retryAfterSeconds + } +} + +function sanitizeBitbucketError(error: any) { + if (isAxiosError(error) && error.config) { + const safeConfig = { + ...error.config, + headers: error.config.headers + ? { + ...error.config.headers, + Authorization: '[REDACTED]', + } + : error.config.headers, + } + error.config = safeConfig as any + } + return error +} export interface BitbucketRepository { uuid: string @@ -35,6 +65,51 @@ export class BitbucketService { ...(token && { Authorization: `Bearer ${token}` }), }, }) + + this.client.interceptors.response.use( + (response) => response, + async (error) => { + if (!isAxiosError(error) || !error.config) { + throw sanitizeBitbucketError(error) + } + + const status = error.response?.status + const config = error.config as any + config.retryCount = config.retryCount || 0 + + if (status === 429) { + if (config.retryCount >= 3) { + const retryAfterHeader = error.response?.headers?.['retry-after'] + const retrySeconds = retryAfterHeader + ? Math.max(1, parseInt(retryAfterHeader, 10)) + : 60 + throw new BitbucketRateLimitError(retrySeconds) + } + config.retryCount += 1 + const delayMs = getRetryDelayMs(error, config.retryCount) ?? 1000 + await new Promise((resolve) => setTimeout(resolve, delayMs)) + return this.client(config) + } + + const retryableCodes = [502, 503, 504] + if ( + (status && retryableCodes.includes(status)) || + error.code === 'ECONNABORTED' || + error.code === 'ECONNRESET' || + error.code === 'ETIMEDOUT' || + !error.response + ) { + if (config.retryCount < 3) { + config.retryCount += 1 + const backoff = Math.pow(2, config.retryCount) * 1000 + Math.random() * 1000 + await new Promise((resolve) => setTimeout(resolve, backoff)) + return this.client(config) + } + } + + throw sanitizeBitbucketError(error) + }, + ) } /** diff --git a/lib/services/githubAppService.ts b/lib/services/githubAppService.ts index db1a331c..a3352d05 100644 --- a/lib/services/githubAppService.ts +++ b/lib/services/githubAppService.ts @@ -1,5 +1,7 @@ -import axios from "axios"; +import axios, { isAxiosError } from "axios"; import jwt from "jsonwebtoken"; +import { withRetry, extractRetryAfter } from "@/lib/utils/rateLimit"; +import { GitHubRateLimitError } from "@/lib/services/githubService"; function getRequiredEnv(name: string): string { const value = process.env[name]; @@ -10,10 +12,25 @@ function getRequiredEnv(name: string): string { } function normalizePrivateKey(value: string): string { - // Common deployment pattern: store multiline key with literal "\n". return value.includes("\\n") ? value.replace(/\\n/g, "\n") : value; } +function sanitizeAppError(error: any) { + if (isAxiosError(error) && error.config) { + const safeConfig = { + ...error.config, + headers: error.config.headers + ? { + ...error.config.headers, + Authorization: "[REDACTED]", + } + : error.config.headers, + }; + error.config = safeConfig as any; + } + return error; +} + export class GitHubAppService { private appId: string; private privateKey: string; @@ -29,7 +46,7 @@ export class GitHubAppService { const now = Math.floor(Date.now() / 1000); const payload = { iat: now - 60, - exp: now + 9 * 60, // max 10 minutes + exp: now + 9 * 60, iss: this.appId, }; @@ -42,23 +59,40 @@ export class GitHubAppService { } const appJwt = this.createAppJwt(); - const response = await axios.post( - `https://api.github.com/app/installations/${installationId}/access_tokens`, - {}, - { - headers: { - Authorization: `Bearer ${appJwt}`, - Accept: "application/vnd.github+json", - "X-GitHub-Api-Version": "2022-11-28", - }, - }, - ); + try { + return await withRetry( + async () => { + const response = await axios.post( + `https://api.github.com/app/installations/${installationId}/access_tokens`, + {}, + { + headers: { + Authorization: `Bearer ${appJwt}`, + Accept: "application/vnd.github+json", + "X-GitHub-Api-Version": "2022-11-28", + }, + }, + ); - const token = response.data?.token as string | undefined; - if (!token) { - throw new Error("Failed to obtain installation access token"); + const token = response.data?.token as string | undefined; + if (!token) { + throw new Error("Failed to obtain installation access token"); + } + return token; + }, + { + maxRetries: 3, + onRetry: (attempt, _err, delayMs) => { + console.warn(`[GitHubAppService] Retrying access token fetch for installation ${installationId} (attempt ${attempt}) in ${delayMs}ms`); + } + } + ); + } catch (err: any) { + if (isAxiosError(err) && err.response?.status === 429) { + throw new GitHubRateLimitError(extractRetryAfter(err) ?? 60); + } + throw sanitizeAppError(err); } - return token; } async uninstallInstallation(installationId: number): Promise { @@ -67,15 +101,32 @@ export class GitHubAppService { } const appJwt = this.createAppJwt(); - await axios.delete( - `https://api.github.com/app/installations/${installationId}`, - { - headers: { - Authorization: `Bearer ${appJwt}`, - Accept: "application/vnd.github+json", - "X-GitHub-Api-Version": "2022-11-28", + try { + await withRetry( + async () => { + await axios.delete( + `https://api.github.com/app/installations/${installationId}`, + { + headers: { + Authorization: `Bearer ${appJwt}`, + Accept: "application/vnd.github+json", + "X-GitHub-Api-Version": "2022-11-28", + }, + }, + ); }, - }, - ); + { + maxRetries: 3, + onRetry: (attempt, _err, delayMs) => { + console.warn(`[GitHubAppService] Retrying uninstall for installation ${installationId} (attempt ${attempt}) in ${delayMs}ms`); + } + } + ); + } catch (err: any) { + if (isAxiosError(err) && err.response?.status === 429) { + throw new GitHubRateLimitError(extractRetryAfter(err) ?? 60); + } + throw sanitizeAppError(err); + } } } diff --git a/lib/services/githubService.ts b/lib/services/githubService.ts index 18787da7..5d6007fb 100644 --- a/lib/services/githubService.ts +++ b/lib/services/githubService.ts @@ -1,9 +1,14 @@ import axios, { AxiosError, AxiosInstance, isAxiosError } from "axios"; +import { getRetryDelayMs } from "@/lib/utils/rateLimit"; export class GitHubRateLimitError extends Error { retryAfterSeconds: number; constructor(retryAfterSeconds: number) { - super(`GitHub API rate limit reached. Please retry after ${retryAfterSeconds} seconds.`); + super( + retryAfterSeconds > 0 + ? `GitHub API rate limit reached. Please retry after ${retryAfterSeconds} seconds.` + : "GitHub API rate limit reached. Please try again later.", + ); this.name = "GitHubRateLimitError"; this.retryAfterSeconds = retryAfterSeconds; } @@ -165,33 +170,44 @@ export class GitHubService { const status = error.response?.status; const config = error.config as any; - if (status === 429 || status === 403) { + config.retryCount = config.retryCount || 0; + + const isRateLimit = status === 429 || status === 403; + if (isRateLimit) { const rateLimitRemaining = error.response?.headers?.["x-ratelimit-remaining"]; if (status === 429 || rateLimitRemaining === "0") { - const retryAfterHeader = error.response?.headers?.["retry-after"]; - const resetHeader = error.response?.headers?.["x-ratelimit-reset"]; - let retrySeconds = 60; - - if (retryAfterHeader) { - retrySeconds = parseInt(retryAfterHeader, 10); - } else if (resetHeader) { - const resetTime = parseInt(resetHeader, 10) * 1000; - retrySeconds = Math.max(1, Math.ceil((resetTime - Date.now()) / 1000)); + if (config.retryCount >= 3) { + const retryAfterHeader = error.response?.headers?.["retry-after"]; + const resetHeader = error.response?.headers?.["x-ratelimit-reset"]; + let retrySeconds = 60; + + if (retryAfterHeader) { + retrySeconds = parseInt(retryAfterHeader, 10); + } else if (resetHeader) { + const resetTime = parseInt(resetHeader, 10) * 1000; + retrySeconds = Math.max(1, Math.ceil((resetTime - Date.now()) / 1000)); + } + throw new GitHubRateLimitError(retrySeconds); } - throw new GitHubRateLimitError(retrySeconds); + config.retryCount += 1; + const delayMs = getRetryDelayMs(error, config.retryCount) ?? 1000; + await new Promise((resolve) => setTimeout(resolve, delayMs)); + return this.client(config); } } - const retryStatusCodes = [502, 503, 504]; + const retryableCodes = [409, 502, 503, 504]; if ( - (status && retryStatusCodes.includes(status)) || + (status && [409, 502, 503, 504].includes(status)) || error.code === "ECONNABORTED" || + error.code === "ECONNRESET" || + error.code === "ETIMEDOUT" || !error.response ) { - config.retryCount = config.retryCount || 0; if (config.retryCount < 3) { config.retryCount += 1; const backoff = Math.pow(2, config.retryCount) * 1000 + Math.random() * 1000; + console.log(`Retrying GitHub API request ${config.url} (attempt ${config.retryCount}) due to ${status || error.code}...`); await new Promise((resolve) => setTimeout(resolve, backoff)); return this.client(config); } @@ -217,9 +233,25 @@ export class GitHubService { /** * Get repository information */ - async getRepository(owner: string, repo: string): Promise { - const response = await this.client.get(`/repos/${owner}/${repo}`); - return response.data; + async getRepository(owner: string, repo: string): Promise { + try { + const response = await this.client.get(`/repos/${owner}/${repo}`); + const data = response.data as GitHubRepository; + + // Detect renamed repositories safely + const expectedFullName = `${owner}/${repo}`.toLowerCase(); + if (data.full_name && data.full_name.toLowerCase() !== expectedFullName) { + console.warn(`GitHub repository renamed: requested ${expectedFullName}, but received ${data.full_name}`); + } + + return data; + } catch (error) { + if (isAxiosError(error) && error.response?.status === 404) { + console.warn(`GitHub repository not found (404): ${owner}/${repo}. It may have been deleted or access was lost.`); + return null; + } + throw sanitizeGitHubError(error); + } } /** @@ -277,8 +309,15 @@ export class GitHubService { * Get repository branches */ async getBranches(owner: string, repo: string): Promise { - const response = await this.client.get(`/repos/${owner}/${repo}/branches`); - return response.data; + try { + const response = await this.client.get(`/repos/${owner}/${repo}/branches`); + return response.data; + } catch (error) { + if (isAxiosError(error) && error.response?.status === 404) { + return []; + } + throw sanitizeGitHubError(error); + } } /** @@ -294,16 +333,22 @@ export class GitHubService { page?: number; }, ): Promise { - const response = await this.client.get(`/repos/${owner}/${repo}/commits`, { - params: { - sha: params?.sha, - path: params?.path, - per_page: params?.per_page || 100, - page: params?.page || 1, - }, - }); - - return response.data; + try { + const response = await this.client.get(`/repos/${owner}/${repo}/commits`, { + params: { + sha: params?.sha, + path: params?.path, + per_page: params?.per_page || 100, + page: params?.page || 1, + }, + }); + return response.data; + } catch (error) { + if (isAxiosError(error) && (error.response?.status === 404 || error.response?.status === 409)) { + return []; // 409 Conflict means repository is empty + } + throw sanitizeGitHubError(error); + } } /** @@ -313,11 +358,18 @@ export class GitHubService { owner: string, repo: string, sha: string, - ): Promise { - const response = await this.client.get( - `/repos/${owner}/${repo}/commits/${sha}`, - ); - return response.data; + ): Promise { + try { + const response = await this.client.get( + `/repos/${owner}/${repo}/commits/${sha}`, + ); + return response.data; + } catch (error) { + if (isAxiosError(error) && error.response?.status === 404) { + return null; + } + throw sanitizeGitHubError(error); + } } /** @@ -327,11 +379,18 @@ export class GitHubService { owner: string, repo: string, pullNumber: number, - ): Promise { - const response = await this.client.get( - `/repos/${owner}/${repo}/pulls/${pullNumber}`, - ); - return response.data; + ): Promise { + try { + const response = await this.client.get( + `/repos/${owner}/${repo}/pulls/${pullNumber}`, + ); + return response.data; + } catch (error) { + if (isAxiosError(error) && error.response?.status === 404) { + return null; + } + throw sanitizeGitHubError(error); + } } /** @@ -347,21 +406,28 @@ export class GitHubService { const maxPages = Math.min(Math.max(params?.max_pages ?? 10, 1), 50); const all: GitHubPullRequestFile[] = []; - for (let page = 1; page <= maxPages; page++) { - const response = await this.client.get( - `/repos/${owner}/${repo}/pulls/${pullNumber}/files`, - { - params: { - per_page: perPage, - page, + try { + for (let page = 1; page <= maxPages; page++) { + const response = await this.client.get( + `/repos/${owner}/${repo}/pulls/${pullNumber}/files`, + { + params: { + per_page: perPage, + page, + }, }, - }, - ); + ); - const items: GitHubPullRequestFile[] = response.data; - if (!Array.isArray(items) || items.length === 0) break; - all.push(...items); - if (items.length < perPage) break; + const items: GitHubPullRequestFile[] = response.data; + if (!Array.isArray(items) || items.length === 0) break; + all.push(...items); + if (items.length < perPage) break; + } + } catch (error) { + if (isAxiosError(error) && error.response?.status === 404) { + return all; // Return what we have (likely empty) if 404 occurs + } + throw sanitizeGitHubError(error); } return all; @@ -429,8 +495,15 @@ export class GitHubService { owner: string, repo: string, ): Promise> { - const response = await this.client.get(`/repos/${owner}/${repo}/languages`); - return response.data; + try { + const response = await this.client.get(`/repos/${owner}/${repo}/languages`); + return response.data; + } catch (error) { + if (isAxiosError(error) && error.response?.status === 404) { + return {}; + } + throw sanitizeGitHubError(error); + } } /** @@ -446,10 +519,17 @@ export class GitHubService { avatar_url: string; }> > { - const response = await this.client.get( - `/repos/${owner}/${repo}/contributors`, - ); - return response.data; + try { + const response = await this.client.get( + `/repos/${owner}/${repo}/contributors`, + ); + return response.data; + } catch (error) { + if (isAxiosError(error) && error.response?.status === 404) { + return []; + } + throw sanitizeGitHubError(error); + } } /** diff --git a/lib/services/gitlabService.ts b/lib/services/gitlabService.ts index 9c75b69b..fbbc3393 100644 --- a/lib/services/gitlabService.ts +++ b/lib/services/gitlabService.ts @@ -1,4 +1,34 @@ -import axios, { AxiosInstance } from 'axios' +import axios, { AxiosInstance, isAxiosError } from 'axios' +import { getRetryDelayMs } from '@/lib/utils/rateLimit' + +export class GitLabRateLimitError extends Error { + retryAfterSeconds: number + constructor(retryAfterSeconds: number) { + super( + retryAfterSeconds > 0 + ? `GitLab API rate limit reached. Please retry after ${retryAfterSeconds} seconds.` + : 'GitLab API rate limit reached. Please try again later.', + ) + this.name = 'GitLabRateLimitError' + this.retryAfterSeconds = retryAfterSeconds + } +} + +function sanitizeGitLabError(error: any) { + if (isAxiosError(error) && error.config) { + const safeConfig = { + ...error.config, + headers: error.config.headers + ? { + ...error.config.headers, + 'PRIVATE-TOKEN': '[REDACTED]', + } + : error.config.headers, + } + error.config = safeConfig as any + } + return error +} export interface GitLabProject { id: number @@ -40,6 +70,51 @@ export class GitLabService { ...(token && { 'PRIVATE-TOKEN': token }), }, }) + + this.client.interceptors.response.use( + (response) => response, + async (error) => { + if (!isAxiosError(error) || !error.config) { + throw sanitizeGitLabError(error) + } + + const status = error.response?.status + const config = error.config as any + config.retryCount = config.retryCount || 0 + + if (status === 429) { + if (config.retryCount >= 3) { + const retryAfterHeader = error.response?.headers?.['retry-after'] + const retrySeconds = retryAfterHeader + ? Math.max(1, parseInt(retryAfterHeader, 10)) + : 60 + throw new GitLabRateLimitError(retrySeconds) + } + config.retryCount += 1 + const delayMs = getRetryDelayMs(error, config.retryCount) ?? 1000 + await new Promise((resolve) => setTimeout(resolve, delayMs)) + return this.client(config) + } + + const retryableCodes = [502, 503, 504] + if ( + (status && retryableCodes.includes(status)) || + error.code === 'ECONNABORTED' || + error.code === 'ECONNRESET' || + error.code === 'ETIMEDOUT' || + !error.response + ) { + if (config.retryCount < 3) { + config.retryCount += 1 + const backoff = Math.pow(2, config.retryCount) * 1000 + Math.random() * 1000 + await new Promise((resolve) => setTimeout(resolve, backoff)) + return this.client(config) + } + } + + throw sanitizeGitLabError(error) + }, + ) } /** @@ -88,7 +163,7 @@ export class GitLabService { */ async getBranches(projectId: string): Promise { const response = await this.client.get( - `/projects/${encodeURIComponent(projectId)}/repository/branches` + `/projects/${encodeURIComponent(projectId)}/repository/branches`, ) return response.data } @@ -102,7 +177,7 @@ export class GitLabService { ref_name?: string per_page?: number page?: number - } + }, ): Promise { const response = await this.client.get( `/projects/${encodeURIComponent(projectId)}/repository/commits`, @@ -112,7 +187,7 @@ export class GitLabService { per_page: params?.per_page || 100, page: params?.page || 1, }, - } + }, ) return response.data @@ -123,7 +198,7 @@ export class GitLabService { */ async getContributors(projectId: string): Promise { const response = await this.client.get( - `/projects/${encodeURIComponent(projectId)}/repository/contributors` + `/projects/${encodeURIComponent(projectId)}/repository/contributors`, ) return response.data } @@ -132,7 +207,10 @@ export class GitLabService { * Parse GitLab URL */ static parseGitLabUrl(url: string): { projectPath: string } | null { - const patterns = [/gitlab\.com\/([^\/]+\/[^\/]+?)(?:\.git)?$/, /gitlab\.com\/([^\/]+\/[^\/]+)/] + const patterns = [ + /gitlab\.com\/([^\/]+\/[^\/]+?)(?:\.git)?$/, + /gitlab\.com\/([^\/]+\/[^\/]+)/, + ] for (const pattern of patterns) { const match = url.match(pattern) diff --git a/lib/services/prReviewService.ts b/lib/services/prReviewService.ts index 43bdf1a4..ce08668d 100644 --- a/lib/services/prReviewService.ts +++ b/lib/services/prReviewService.ts @@ -165,6 +165,10 @@ export async function reviewPullRequest(params: { params.repo, params.number, ); + if (!pr) { + throw new Error(`Pull request ${params.owner}/${params.repo}#${params.number} not found. It may have been deleted or access is denied.`); + } + const prFiles = await github.getPullRequestFiles( params.owner, params.repo, diff --git a/lib/services/repositoryService.ts b/lib/services/repositoryService.ts index 9e00a25a..c020426d 100644 --- a/lib/services/repositoryService.ts +++ b/lib/services/repositoryService.ts @@ -22,6 +22,56 @@ export type RepositoryAnalysisProgressReporter = ( update: RepositoryAnalysisProgress, ) => void | Promise; +class AnalysisProgressTracker { + constructor( + private repositoryId: number, + private reporter?: RepositoryAnalysisProgressReporter + ) {} + + async update(percent: number, message: string, details?: unknown) { + const safePercent = Math.max(0, Math.min(100, Math.round(percent || 0))); + console.log(`[Repo ${this.repositoryId}] ${safePercent}% - ${message}`); + + if (!this.reporter) return; + try { + await this.reporter({ + progressPercent: safePercent, + progressMessage: message, + progressDetails: details, + }); + } catch { + // Progress reporting must never break analysis + } + } + + async progressSubTask( + startPercent: number, + endPercent: number, + current: number, + total: number, + message: string + ) { + if (total <= 0) { + await this.update(endPercent, `${message} (Completed)`); + return; + } + const range = endPercent - startPercent; + const ratio = Math.max(0, Math.min(1, current / total)); + const currentPercent = startPercent + (range * ratio); + await this.update(currentPercent, message); + } + + async fail(error: Error | unknown) { + const msg = error instanceof Error ? error.message : "Unknown error"; + console.error(`[Repo ${this.repositoryId}] Analysis Failed: ${msg}`); + if (this.reporter) { + try { + await this.reporter({ progressMessage: `Failed: ${msg}` }); + } catch {} + } + } +} + export class RepositoryService { private async tryReadmeFromRepoPath(repoPath: string): Promise<{ path: string; @@ -168,16 +218,8 @@ export class RepositoryService { data: { status: "analyzing" }, }); - const report = async (update: RepositoryAnalysisProgress) => { - if (!opts?.onProgress) return; - try { - await opts.onProgress(update); - } catch { - // Progress reporting must never break analysis. - } - }; - - await report({ progressPercent: 1, progressMessage: "Starting" }); + const tracker = new AnalysisProgressTracker(repositoryId, opts?.onProgress); + await tracker.update(1, "Starting analysis"); // Create temporary directory for cloning const tempDir = path.join( @@ -190,15 +232,11 @@ export class RepositoryService { try { // Clone repository - console.log(`Cloning repository ${repository.url} to ${tempDir}`); - await report({ - progressPercent: 5, - progressMessage: "Cloning repository", - }); + await tracker.update(5, `Cloning repository ${repository.url}`); gitService = await GitService.cloneRepository(repository.url, tempDir); // Capture README early (best-effort) - await report({ progressPercent: 8, progressMessage: "Reading README" }); + await tracker.update(8, "Reading README"); const readme = await this.tryReadmeFromRepoPath(tempDir); await prisma.repository.update({ where: { id: repositoryId }, @@ -210,18 +248,11 @@ export class RepositoryService { }); // Get repository size - await report({ - progressPercent: 10, - progressMessage: "Calculating size", - }); + await tracker.update(10, "Calculating size"); const size = await gitService.getRepositorySize(); // Analyze branches - console.log(`Analyzing branches for repository ${repositoryId}`); - await report({ - progressPercent: 15, - progressMessage: "Analyzing branches", - }); + await tracker.update(15, "Analyzing branches"); const branches = await gitService.getBranches(); const defaultBranch = branches.find((b) => b.isDefault)?.name || "main"; @@ -238,11 +269,7 @@ export class RepositoryService { }); // Analyze commits from all branches - console.log(`Analyzing commits for repository ${repositoryId}`); - await report({ - progressPercent: 25, - progressMessage: "Reading commit history", - }); + await tracker.update(25, "Reading commit history"); const commits = await gitService.getCommits("--all", 1000); console.log(`Total commits fetched from git: ${commits.length}`); @@ -252,12 +279,12 @@ export class RepositoryService { const existingCommits = commits.length > 0 ? await prisma.commit.findMany({ - where: { - repositoryId, - hash: { in: commits.map((c) => c.hash) }, - }, - select: { hash: true }, - }) + where: { + repositoryId, + hash: { in: commits.map((c) => c.hash) }, + }, + select: { hash: true }, + }) : []; const existingHashes = new Set(existingCommits.map((c) => c.hash)); @@ -273,9 +300,13 @@ export class RepositoryService { let insertedCount = 0; let failedCount = 0; - const totalNewCommits = Math.max(1, newCommits.length); + const totalNewCommits = newCommits.length; let lastCommitProgressUpdateAt = Date.now(); + if (totalNewCommits === 0) { + await tracker.update(60, "Storing commits (Up to date)"); + } + for (const commit of newCommits) { try { const createdCommit = await prisma.commit.create({ @@ -302,11 +333,7 @@ export class RepositoryService { // Periodically report progress (every ~2s) if (Date.now() - lastCommitProgressUpdateAt > 2000) { - const pct = 25 + Math.round((insertedCount / totalNewCommits) * 35); - await report({ - progressPercent: Math.min(60, pct), - progressMessage: `Storing commits (${insertedCount}/${newCommits.length})`, - }); + await tracker.progressSubTask(25, 60, insertedCount, totalNewCommits, `Storing commits (${insertedCount}/${totalNewCommits})`); lastCommitProgressUpdateAt = Date.now(); } @@ -338,8 +365,7 @@ export class RepositoryService { ); // Analyze files - console.log(`Analyzing file tree for repository ${repositoryId}`); - await report({ progressPercent: 65, progressMessage: "Scanning files" }); + await tracker.update(65, "Scanning files"); const files = await gitService.getFileTree(); // Avoid querying existing file paths (can be huge). Just rely on @@ -362,25 +388,17 @@ export class RepositoryService { }); const insertedSoFar = Math.min(files.length, i + chunk.length); - await report({ - progressPercent: - 65 + Math.round((insertedSoFar / files.length) * 10), - progressMessage: `Storing files (${insertedSoFar}/${files.length})`, - }); + await tracker.progressSubTask(65, 75, insertedSoFar, files.length, `Storing files (${insertedSoFar}/${files.length})`); } console.log( `File scan complete: processed ${files.length} paths for repository ${repositoryId}`, ); } else { - console.log(`No files found for repository ${repositoryId}`); + await tracker.update(75, "Storing files (Skipped - no files found)"); } // Analyze contributors - console.log(`Analyzing contributors for repository ${repositoryId}`); - await report({ - progressPercent: 80, - progressMessage: "Analyzing contributors", - }); + await tracker.update(80, "Analyzing contributors"); const contributors = await gitService.getContributors(); const totalContributions = contributors.reduce( (sum, c) => sum + c.commits, @@ -411,11 +429,7 @@ export class RepositoryService { } // Detect languages - console.log(`Detecting languages for repository ${repositoryId}`); - await report({ - progressPercent: 90, - progressMessage: "Detecting languages", - }); + await tracker.update(90, "Detecting languages"); const languages = await gitService.detectLanguages(); // Languages to ignore (config/data formats, not actual code) @@ -459,18 +473,36 @@ export class RepositoryService { }), ); - if (languagesWithAdjustedPercentage.length > 0) { - await prisma.language.createMany({ - data: languagesWithAdjustedPercentage.map((language) => ({ - name: language.name, - percentage: language.percentage, - bytes: language.bytes, - lines: language.lines, - repositoryId, - })), - skipDuplicates: true, + await prisma.$transaction(async (tx) => { + // Always clear stale languages even when result set is empty + await tx.language.deleteMany({ + where: { repositoryId } }); - } + + if (languagesWithAdjustedPercentage.length > 0) { + const validLanguages = languagesWithAdjustedPercentage + .map((language) => { + const trimmedName = language.name.trim(); + if (!trimmedName) return null; + + return { + name: trimmedName, + percentage: language.percentage, + bytes: language.bytes, + lines: language.lines, + repositoryId, + }; + }) + .filter((lang): lang is NonNullable => lang !== null); + + if (validLanguages.length > 0) { + await tx.language.createMany({ + data: validLanguages, + skipDuplicates: true, + }); + } + } + }); // Update repository with final data await prisma.repository.update({ @@ -483,16 +515,13 @@ export class RepositoryService { }, }); - await report({ progressPercent: 100, progressMessage: "Completed" }); - - console.log(`Repository ${repositoryId} analysis completed`); + await tracker.update(100, "Completed"); } catch (error: any) { - console.error(`Error analyzing repository ${repositoryId}:`, error); await prisma.repository.update({ where: { id: repositoryId }, data: { status: "failed" }, }); - await report({ progressMessage: "Failed" }); + await tracker.fail(error); throw error; } finally { // Cleanup cloned repository @@ -502,6 +531,24 @@ export class RepositoryService { } } + /** + * Safely marks a repository as failed, preventing uncaught exceptions + * if the database update fails. + */ + async markRepositoryFailed(id: number, reason?: string) { + try { + await prisma.repository.update({ + where: { id }, + data: { status: "failed" }, + }); + if (reason) { + console.log(`Repository ${id} marked as failed. Reason: ${reason}`); + } + } catch (error) { + console.error(`Safeguard: Failed to update repository ${id} status to 'failed'`, error instanceof Error ? error.message : String(error)); + } + } + /** * Get repository with all related data */ @@ -576,9 +623,16 @@ export class RepositoryService { throw new Error("Repository not found"); } - await prisma.repository.delete({ - where: { id }, - }); + // Delete related analysis jobs and the repository in a transaction + // to ensure no orphaned rows remain if the process is interrupted. + await prisma.$transaction([ + prisma.analysisJob.deleteMany({ + where: { repositoryId: id }, + }), + prisma.repository.delete({ + where: { id }, + }), + ]); return { success: true }; } diff --git a/lib/utils/rateLimit.ts b/lib/utils/rateLimit.ts new file mode 100644 index 00000000..1a2590a0 --- /dev/null +++ b/lib/utils/rateLimit.ts @@ -0,0 +1,120 @@ +import { AxiosError } from "axios"; + +const RATE_LIMIT_PATTERNS = [ + "rate limit", + "api rate limit", + "secondary rate limit", + "abuse limit", + "too many requests", + "429", +] as const; + +const RETRYABLE_STATUSES = [429, 502, 503, 504]; +const RETRYABLE_CODES = ["ECONNABORTED", "ECONNRESET", "ETIMEDOUT", "ERR_NETWORK"]; +const MAX_RETRIES = 3; + +function sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +export async function withRetry( + fn: () => Promise, + options?: { + maxRetries?: number; + onRetry?: (attempt: number, error: unknown, delayMs: number) => void; + }, +): Promise { + const maxRetries = options?.maxRetries ?? MAX_RETRIES; + + for (let attempt = 1; attempt <= maxRetries; attempt++) { + try { + return await fn(); + } catch (err: unknown) { + if (attempt === maxRetries) throw err; + + const delayMs = getRetryDelayMs(err, attempt); + if (delayMs === null) throw err; + + options?.onRetry?.(attempt, err, delayMs); + await sleep(delayMs); + } + } + + throw new Error("unreachable"); +} + +export function getRetryDelayMs(err: unknown, attempt: number): number | null { + const retryAfter = extractRetryAfter(err); + if (retryAfter != null) return retryAfter * 1000; + + const axiosErr = err instanceof AxiosError ? err : null; + const status = axiosErr?.response?.status; + + if (status === 429) return Math.min(60_000, Math.pow(2, attempt) * 1000); + if (status && RETRYABLE_STATUSES.includes(status)) return Math.pow(2, attempt) * 1000 + Math.random() * 1000; + if (axiosErr?.code && RETRYABLE_CODES.includes(axiosErr.code)) return Math.pow(2, attempt) * 1000; + + return null; +} + +export function isRateLimitError(err: unknown): boolean { + if (err instanceof AxiosError) { + if (err.response?.status === 429) return true; + + const retryAfter = err.response?.headers?.["retry-after"]; + if (retryAfter != null) return true; + + const xRateLimit = err.response?.headers?.["x-ratelimit-remaining"]; + if (xRateLimit === "0") return true; + } + + if (err instanceof Error) { + const msg = err.message.toLowerCase(); + return RATE_LIMIT_PATTERNS.some((p) => msg.includes(p)); + } + + return false; +} + +export function extractRetryAfter(err: unknown): number | null { + if (!(err instanceof AxiosError) || !err.response) return null; + + const header = err.response.headers?.["retry-after"]; + if (header == null) return null; + + const parsed = parseInt(header, 10); + if (Number.isFinite(parsed) && parsed > 0) return parsed; + + const date = new Date(header); + if (!isNaN(date.getTime())) { + const seconds = Math.max(0, Math.ceil((date.getTime() - Date.now()) / 1000)); + return seconds > 0 ? seconds : null; + } + + return null; +} + +export function sanitizeErrorMessage(err: unknown): string { + if (err instanceof AxiosError) { + const status = err.response?.status; + const message = err.response?.data?.message || err.message; + const safe = stripSensitive(String(message)); + return status ? `[${status}] ${safe}` : safe; + } + + if (err instanceof Error) { + return stripSensitive(err.message); + } + + return stripSensitive(String(err)); +} + +function stripSensitive(text: string): string { + return text + .replace(/Bearer\s+[A-Za-z0-9\-._~+/]+=*/gi, "Bearer [REDACTED]") + .replace(/PRIVATE-TOKEN\s+[A-Za-z0-9\-._~+/]+=*/gi, "PRIVATE-TOKEN [REDACTED]") + .replace(/token[=:]\s*[A-Za-z0-9\-._~+/]+=*/gi, "token=[REDACTED]") + .replace(/gh[pousr]_[A-Za-z0-9_]{36,}/gi, "[REDACTED]") + .replace(/https?:\/\/[^@\s]+:[^@\s]+@/g, "https://[REDACTED]@") + .replace(/Authorization:\s*.+/gi, "Authorization: [REDACTED]"); +} diff --git a/lib/utils/validators.ts b/lib/utils/validators.ts new file mode 100644 index 00000000..4b80217b --- /dev/null +++ b/lib/utils/validators.ts @@ -0,0 +1,29 @@ +/** + * Validates a GitHub repository identifier format (owner/repo). + * Allowed format: alphanumeric, hyphens, underscores, and periods. + * Max length: 150 characters to prevent DoS. + * Prevents path traversal patterns like ../ or excessive slashes. + */ +export function isValidRepositoryIdentifier( + identifier: string | null | undefined, +): boolean { + if (!identifier || typeof identifier !== "string") return false; + + if (identifier.length > 150 || identifier.length < 3) return false; + + // Strict regex: Owner/Repo (1 slash exactly) + // Each segment: 1 to 100 characters of [A-Za-z0-9_.-] + const regex = /^[A-Za-z0-9_.-]{1,100}\/[A-Za-z0-9_.-]{1,100}$/; + if (!regex.test(identifier)) return false; + + // Additional check to prevent encoded traversals or direct traversal matches + if ( + identifier.includes("..") || + identifier.startsWith("/") || + identifier.endsWith("/") + ) { + return false; + } + + return true; +} diff --git a/lib/validateAuth.ts b/lib/validateAuth.ts new file mode 100644 index 00000000..cd89631d --- /dev/null +++ b/lib/validateAuth.ts @@ -0,0 +1,104 @@ +/* + * Manual repro — run these curl commands against the local dev server: + * + * # Missing body → should return 400 + * curl -X POST http://localhost:3000/api/auth/logout \ + * -H "Content-Type: application/json" + * + * # Invalid email → should return 400 with field message + * curl -X POST http://localhost:3000/api/auth/register \ + * -H "Content-Type: application/json" \ + * -d '{"email":"notvalid","password":"securepass123"}' + * + * # Short password → should return 400 + * curl -X POST http://localhost:3000/api/auth/register \ + * -H "Content-Type: application/json" \ + * -d '{"email":"user@example.com","password":"abc"}' + * + * # Valid input → should return 200 or proceed normally + * curl -X POST http://localhost:3000/api/auth/register \ + * -H "Content-Type: application/json" \ + * -d '{"email":"user@example.com","password":"securepass123"}' + */ + +export interface ValidationResult { + valid: boolean; + error?: string; + field?: string; +} + +export function validateEmail(email: unknown): ValidationResult { + if (!email || typeof email !== "string") { + return { valid: false, field: "email", error: "Email is required" }; + } + const trimmed = email.trim(); + if (trimmed.length === 0) { + return { valid: false, field: "email", error: "Email cannot be empty" }; + } + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(trimmed)) { + return { valid: false, field: "email", error: "Email format is invalid" }; + } + return { valid: true }; +} + +export function validatePassword(password: unknown): ValidationResult { + if (!password || typeof password !== "string") { + return { valid: false, field: "password", error: "Password is required" }; + } + if (password.length < 6) { + return { + valid: false, + field: "password", + error: "Password must be at least 6 characters", + }; + } + return { valid: true }; +} + +export function validateRequiredString( + value: unknown, + fieldName: string +): ValidationResult { + if (value === undefined || value === null) { + return { valid: false, field: fieldName, error: `${fieldName} is required` }; + } + if (typeof value !== "string") { + return { + valid: false, + field: fieldName, + error: `${fieldName} must be a string`, + }; + } + if (value.trim().length === 0) { + return { + valid: false, + field: fieldName, + error: `${fieldName} cannot be empty`, + }; + } + return { valid: true }; +} + +// Parse request body safely — returns null if body is missing or not valid JSON +export async function parseJsonBody( + request: Request +): Promise<{ body: Record } | { error: string }> { + try { + const contentType = request.headers.get("content-type") ?? ""; + if (!contentType.includes("application/json")) { + return { error: "Content-Type must be application/json" }; + } + const text = await request.text(); + if (!text || text.trim().length === 0) { + return { error: "Request body cannot be empty" }; + } + const body = JSON.parse(text); + if (typeof body !== "object" || body === null || Array.isArray(body)) { + return { error: "Request body must be a JSON object" }; + } + return { body }; + } catch { + return { error: "Request body contains invalid JSON" }; + } +} diff --git a/package-lock.json b/package-lock.json index 015d421e..b2b02199 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,10 +22,9 @@ "@types/d3": "^7.4.3", "@types/pg": "^8.16.0", "axios": "^1.13.2", - "bcrypt": "^6.0.0", "bcryptjs": "^3.0.3", "class-variance-authority": "^0.7.0", - "clsx": "^2.0.0", + "clsx": "^2.1.1", "d3": "^7.9.0", "dotenv": "^16.4.5", "google-auth-library": "^10.3.0", @@ -43,7 +42,7 @@ "rehype-sanitize": "^6.0.0", "remark-gfm": "^4.0.1", "sonner": "^1.3.1", - "tailwind-merge": "^2.2.0", + "tailwind-merge": "^2.6.1", "ws": "^8.19.0" }, "devDependencies": { @@ -55,10 +54,12 @@ "@typescript-eslint/eslint-plugin": "^6.14.0", "@typescript-eslint/parser": "^6.14.0", "autoprefixer": "^10.4.16", + "cross-env": "^10.1.0", "eslint": "^8.55.0", "eslint-config-next": "^14.1.0", "postcss": "^8.4.32", "prettier": "^3.1.1", + "rehype-stringify": "^10.0.1", "tailwindcss": "^3.4.0", "tailwindcss-animate": "^1.0.7", "tsx": "^4.21.0", @@ -82,21 +83,21 @@ } }, "node_modules/@auth/prisma-adapter": { - "version": "2.11.1", - "resolved": "https://registry.npmjs.org/@auth/prisma-adapter/-/prisma-adapter-2.11.1.tgz", - "integrity": "sha512-Ke7DXP0Fy0Mlmjz/ZJLXwQash2UkA4621xCM0rMtEczr1kppLc/njCbUkHkIQ/PnmILjqSPEKeTjDPsYruvkug==", + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@auth/prisma-adapter/-/prisma-adapter-2.11.2.tgz", + "integrity": "sha512-GyNEUNtrPgDPs0M4xX6F5i7jTsCKwU6BXV9zutctcoo6K1Ud+juckrmQS11uyNgeWsw6sliextHbU/e+8lsizQ==", "license": "ISC", "dependencies": { - "@auth/core": "0.41.1" + "@auth/core": "0.41.2" }, "peerDependencies": { "@prisma/client": ">=2.26.0 || >=3 || >=4 || >=5 || >=6" } }, "node_modules/@auth/prisma-adapter/node_modules/@auth/core": { - "version": "0.41.1", - "resolved": "https://registry.npmjs.org/@auth/core/-/core-0.41.1.tgz", - "integrity": "sha512-t9cJ2zNYAdWMacGRMT6+r4xr1uybIdmYa49calBPeTqwgAFPV/88ac9TEvCR85pvATiSPt8VaNf+Gt24JIT/uw==", + "version": "0.41.2", + "resolved": "https://registry.npmjs.org/@auth/core/-/core-0.41.2.tgz", + "integrity": "sha512-Hx5MNBxN2fJTbJKGUKAA0wca43D0Akl3TvufY54Gn8lop7F+34vU1zA1pn0vQfIoVuLIrpfc2nkyjwIaPJMW7w==", "license": "ISC", "dependencies": { "@panva/hkdf": "^1.2.1", @@ -123,18 +124,18 @@ } }, "node_modules/@auth/prisma-adapter/node_modules/jose": { - "version": "6.1.3", - "resolved": "https://registry.npmjs.org/jose/-/jose-6.1.3.tgz", - "integrity": "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==", + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.3.tgz", + "integrity": "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/panva" } }, "node_modules/@auth/prisma-adapter/node_modules/oauth4webapi": { - "version": "3.8.3", - "resolved": "https://registry.npmjs.org/oauth4webapi/-/oauth4webapi-3.8.3.tgz", - "integrity": "sha512-pQ5BsX3QRTgnt5HxgHwgunIRaDXBdkT23tf8dfzmtTIL2LTpdmxgbpbBm0VgFWAIDlezQvQCTgnVIUmHupXHxw==", + "version": "3.8.6", + "resolved": "https://registry.npmjs.org/oauth4webapi/-/oauth4webapi-3.8.6.tgz", + "integrity": "sha512-iwemM91xz8nryHti2yTmg5fhyEMVOkOXwHNqbvcATjyajb5oQxCQzrNOA6uElRHuMhQQTKUyFKV9y/CNyg25BQ==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/panva" @@ -150,90 +151,57 @@ } }, "node_modules/@babel/runtime": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", - "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, - "node_modules/@chevrotain/cst-dts-gen": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-10.5.0.tgz", - "integrity": "sha512-lhmC/FyqQ2o7pGK4Om+hzuDrm9rhFYIJ/AXoQBeongmn870Xeb0L6oGEiuR8nohFNL5sMaQEJWCxr1oIVIVXrw==", - "license": "Apache-2.0", - "dependencies": { - "@chevrotain/gast": "10.5.0", - "@chevrotain/types": "10.5.0", - "lodash": "4.17.21" - } - }, - "node_modules/@chevrotain/gast": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-10.5.0.tgz", - "integrity": "sha512-pXdMJ9XeDAbgOWKuD1Fldz4ieCs6+nLNmyVhe2gZVqoO7v8HXuHYs5OV2EzUtbuai37TlOAQHrTDvxMnvMJz3A==", - "license": "Apache-2.0", - "dependencies": { - "@chevrotain/types": "10.5.0", - "lodash": "4.17.21" - } - }, - "node_modules/@chevrotain/types": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-10.5.0.tgz", - "integrity": "sha512-f1MAia0x/pAVPWH/T73BJVyO2XU5tI4/iE7cnxb7tqdNTNhQI3Uq3XkqcoteTmD4t1aM0LbHCJOhgIDn07kl2A==", - "license": "Apache-2.0" - }, - "node_modules/@chevrotain/utils": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-10.5.0.tgz", - "integrity": "sha512-hBzuU5+JjB2cqNZyszkDHZgOSrUUT8V3dhgRl8Q9Gp6dAj/H5+KILGjbhDpc3Iy9qmqlm/akuOI2ut9VUtzJxQ==", - "license": "Apache-2.0" - }, "node_modules/@electric-sql/pglite": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@electric-sql/pglite/-/pglite-0.3.2.tgz", - "integrity": "sha512-zfWWa+V2ViDCY/cmUfRqeWY1yLto+EpxjXnZzenB1TyxsTiXaTWeZFIZw6mac52BsuQm0RjCnisjBtdBaXOI6w==", + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@electric-sql/pglite/-/pglite-0.4.1.tgz", + "integrity": "sha512-mZ9NzzUSYPOCnxHH1oAHPRzoMFJHY472raDKwXl/+6oPbpdJ7g8LsCN4FSaIIfkiCKHhb3iF/Zqo3NYxaIhU7Q==", "license": "Apache-2.0" }, "node_modules/@electric-sql/pglite-socket": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/@electric-sql/pglite-socket/-/pglite-socket-0.0.6.tgz", - "integrity": "sha512-6RjmgzphIHIBA4NrMGJsjNWK4pu+bCWJlEWlwcxFTVY3WT86dFpKwbZaGWZV6C5Rd7sCk1Z0CI76QEfukLAUXw==", + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@electric-sql/pglite-socket/-/pglite-socket-0.1.1.tgz", + "integrity": "sha512-p2hoXw3Z3LQHwTeikdZNsFBOvXGqKY2hk51BBw+8NKND8eoH+8LFOtW9Z8CQKmTJ2qqGYu82ipqiyFZOTTXNfw==", "license": "Apache-2.0", "bin": { "pglite-server": "dist/scripts/server.js" }, "peerDependencies": { - "@electric-sql/pglite": "0.3.2" + "@electric-sql/pglite": "0.4.1" } }, "node_modules/@electric-sql/pglite-tools": { - "version": "0.2.7", - "resolved": "https://registry.npmjs.org/@electric-sql/pglite-tools/-/pglite-tools-0.2.7.tgz", - "integrity": "sha512-9dAccClqxx4cZB+Ar9B+FZ5WgxDc/Xvl9DPrTWv+dYTf0YNubLzi4wHHRGRGhrJv15XwnyKcGOZAP1VXSneSUg==", + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@electric-sql/pglite-tools/-/pglite-tools-0.3.1.tgz", + "integrity": "sha512-C+T3oivmy9bpQvSxVqXA1UDY8cB9Eb9vZHL9zxWwEUfDixbXv4G3r2LjoTdR33LD8aomR3O9ZXEO3XEwr/cUCA==", "license": "Apache-2.0", "peerDependencies": { - "@electric-sql/pglite": "0.3.2" + "@electric-sql/pglite": "0.4.1" } }, "node_modules/@emnapi/core": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.7.1.tgz", - "integrity": "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", "dev": true, "license": "MIT", "optional": true, "dependencies": { - "@emnapi/wasi-threads": "1.1.0", + "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "node_modules/@emnapi/runtime": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.1.tgz", - "integrity": "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", "dev": true, "license": "MIT", "optional": true, @@ -242,9 +210,9 @@ } }, "node_modules/@emnapi/wasi-threads": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", - "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", "dev": true, "license": "MIT", "optional": true, @@ -252,10 +220,17 @@ "tslib": "^2.4.0" } }, + "node_modules/@epic-web/invariant": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@epic-web/invariant/-/invariant-1.0.0.tgz", + "integrity": "sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==", + "dev": true, + "license": "MIT" + }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", - "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz", + "integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==", "cpu": [ "ppc64" ], @@ -270,9 +245,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", - "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz", + "integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==", "cpu": [ "arm" ], @@ -287,9 +262,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", - "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz", + "integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==", "cpu": [ "arm64" ], @@ -304,9 +279,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", - "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz", + "integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==", "cpu": [ "x64" ], @@ -321,9 +296,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", - "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz", + "integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==", "cpu": [ "arm64" ], @@ -338,9 +313,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", - "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz", + "integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==", "cpu": [ "x64" ], @@ -355,9 +330,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", - "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz", + "integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==", "cpu": [ "arm64" ], @@ -372,9 +347,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", - "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz", + "integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==", "cpu": [ "x64" ], @@ -389,9 +364,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", - "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz", + "integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==", "cpu": [ "arm" ], @@ -406,9 +381,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", - "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz", + "integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==", "cpu": [ "arm64" ], @@ -423,9 +398,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", - "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz", + "integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==", "cpu": [ "ia32" ], @@ -440,9 +415,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", - "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz", + "integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==", "cpu": [ "loong64" ], @@ -457,9 +432,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", - "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz", + "integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==", "cpu": [ "mips64el" ], @@ -474,9 +449,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", - "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz", + "integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==", "cpu": [ "ppc64" ], @@ -491,9 +466,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", - "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz", + "integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==", "cpu": [ "riscv64" ], @@ -508,9 +483,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", - "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz", + "integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==", "cpu": [ "s390x" ], @@ -525,9 +500,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", - "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz", + "integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==", "cpu": [ "x64" ], @@ -542,9 +517,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", - "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz", + "integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==", "cpu": [ "arm64" ], @@ -559,9 +534,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", - "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz", + "integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==", "cpu": [ "x64" ], @@ -576,9 +551,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", - "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz", + "integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==", "cpu": [ "arm64" ], @@ -593,9 +568,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", - "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz", + "integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==", "cpu": [ "x64" ], @@ -610,9 +585,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", - "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz", + "integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==", "cpu": [ "arm64" ], @@ -627,9 +602,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", - "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz", + "integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==", "cpu": [ "x64" ], @@ -644,9 +619,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", - "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz", + "integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==", "cpu": [ "arm64" ], @@ -661,9 +636,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", - "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz", + "integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==", "cpu": [ "ia32" ], @@ -678,9 +653,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", - "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz", + "integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==", "cpu": [ "x64" ], @@ -695,9 +670,9 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.9.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", - "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", "dev": true, "license": "MIT", "dependencies": { @@ -748,9 +723,9 @@ } }, "node_modules/@eslint/eslintrc/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": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", "dev": true, "license": "MIT", "dependencies": { @@ -759,9 +734,9 @@ } }, "node_modules/@eslint/eslintrc/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": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -782,31 +757,31 @@ } }, "node_modules/@floating-ui/core": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz", - "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==", + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz", + "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==", "license": "MIT", "dependencies": { - "@floating-ui/utils": "^0.2.10" + "@floating-ui/utils": "^0.2.11" } }, "node_modules/@floating-ui/dom": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz", - "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==", + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz", + "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", "license": "MIT", "dependencies": { - "@floating-ui/core": "^1.7.3", - "@floating-ui/utils": "^0.2.10" + "@floating-ui/core": "^1.7.5", + "@floating-ui/utils": "^0.2.11" } }, "node_modules/@floating-ui/react-dom": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.6.tgz", - "integrity": "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==", + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.8.tgz", + "integrity": "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==", "license": "MIT", "dependencies": { - "@floating-ui/dom": "^1.7.4" + "@floating-ui/dom": "^1.7.6" }, "peerDependencies": { "react": ">=16.8.0", @@ -814,9 +789,9 @@ } }, "node_modules/@floating-ui/utils": { - "version": "0.2.10", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", - "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz", + "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", "license": "MIT" }, "node_modules/@google/generative-ai": { @@ -829,9 +804,9 @@ } }, "node_modules/@hono/node-server": { - "version": "1.19.6", - "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.6.tgz", - "integrity": "sha512-Shz/KjlIeAhfiuE93NDKVdZ7HdBVLQAfdbaXEaoAVO3ic9ibRSLGIQGkcBbFyuLr+7/1D5ZCINM8B+6IvXeMtw==", + "version": "1.19.11", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.11.tgz", + "integrity": "sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g==", "license": "MIT", "engines": { "node": ">=18.14.1" @@ -857,9 +832,9 @@ } }, "node_modules/@humanwhocodes/config-array/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": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", "dev": true, "license": "MIT", "dependencies": { @@ -868,9 +843,9 @@ } }, "node_modules/@humanwhocodes/config-array/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": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -906,6 +881,7 @@ "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, "license": "ISC", "dependencies": { "string-width": "^5.1.2", @@ -923,6 +899,7 @@ "version": "6.2.2", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -932,12 +909,13 @@ } }, "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==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, "license": "MIT", "dependencies": { - "ansi-regex": "^6.0.1" + "ansi-regex": "^6.2.2" }, "engines": { "node": ">=12" @@ -985,60 +963,40 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@mrleebo/prisma-ast": { - "version": "0.12.1", - "resolved": "https://registry.npmjs.org/@mrleebo/prisma-ast/-/prisma-ast-0.12.1.tgz", - "integrity": "sha512-JwqeCQ1U3fvccttHZq7Tk0m/TMC6WcFAQZdukypW3AzlJYKYTGNVd1ANU2GuhKnv4UQuOFj3oAl0LLG/gxFN1w==", - "license": "MIT", - "dependencies": { - "chevrotain": "^10.5.0", - "lilconfig": "^2.1.0" - }, - "engines": { - "node": ">=16" - } + "node_modules/@kurkle/color": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", + "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", + "license": "MIT" }, "node_modules/@napi-rs/wasm-runtime": { - "version": "0.2.12", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", - "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", + "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", "dev": true, "license": "MIT", "optional": true, "dependencies": { - "@emnapi/core": "^1.4.3", - "@emnapi/runtime": "^1.4.3", - "@tybys/wasm-util": "^0.10.0" + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" } }, "node_modules/@neondatabase/serverless": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@neondatabase/serverless/-/serverless-1.0.2.tgz", - "integrity": "sha512-I5sbpSIAHiB+b6UttofhrN/UJXII+4tZPAq1qugzwCwLIL8EZLV7F/JyHUrEIiGgQpEXzpnjlJ+zwcEhheGvCw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@neondatabase/serverless/-/serverless-1.1.0.tgz", + "integrity": "sha512-r3ZZhRjEcfEdKIZnoB1RusNgvHuaBRqfCzV4Gi+5A9yUX0S4HTws/ASWqt13wL4y4I+0rqsWGdA2w7EQXHi3+Q==", "license": "MIT", - "dependencies": { - "@types/node": "^22.15.30", - "@types/pg": "^8.8.0" - }, "engines": { "node": ">=19.0.0" } }, - "node_modules/@neondatabase/serverless/node_modules/@types/node": { - "version": "22.19.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.3.tgz", - "integrity": "sha512-1N9SBnWYOJTrNZCdh/yJE+t910Y128BoyY+zBLWhL3r0TYzlTmFdXrPwHL9DyFZmlEXNQQolTZh3KHV31QDhyA==", - "license": "MIT", - "dependencies": { - "undici-types": "~6.21.0" - } - }, - "node_modules/@neondatabase/serverless/node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "license": "MIT" - }, "node_modules/@next/env": { "version": "14.2.35", "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.35.tgz", @@ -1260,6 +1218,7 @@ "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, "license": "MIT", "optional": true, "engines": { @@ -1267,34 +1226,35 @@ } }, "node_modules/@prisma/adapter-neon": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@prisma/adapter-neon/-/adapter-neon-7.2.0.tgz", - "integrity": "sha512-XLTCQ0mDdaOqzBO99jyMOWBofJ5iqsvgWYB7lW+2ZEXvY6888tkeKfpo2VwdKhu1Cj/q4IqHd15eFQzPwbpjJQ==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@prisma/adapter-neon/-/adapter-neon-7.8.0.tgz", + "integrity": "sha512-9MoOjRuY53sQNyji87u1NivsK0tIacoUE3PqTxdG95b7ORn1pzwribH2z83j4Vh3eb9nQ/UC+MTfVW7jCFnXlA==", "license": "Apache-2.0", "dependencies": { "@neondatabase/serverless": ">0.6.0 <2", - "@prisma/driver-adapter-utils": "7.2.0", + "@prisma/driver-adapter-utils": "7.8.0", "postgres-array": "3.0.4" } }, "node_modules/@prisma/adapter-pg": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@prisma/adapter-pg/-/adapter-pg-7.2.0.tgz", - "integrity": "sha512-euIdQ13cRB2wZ3jPsnDnFhINquo1PYFPCg6yVL8b2rp3EdinQHsX9EDdCtRr489D5uhphcRk463OdQAFlsCr0w==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@prisma/adapter-pg/-/adapter-pg-7.8.0.tgz", + "integrity": "sha512-ygb3UkerK3v8MDpXVgCISdRNDozpxh6+JVJgiIGbSr5KBgz10LLf5ejUskPGoXlsIjxsOu6nuy1JVQr2EKGSlg==", "license": "Apache-2.0", "dependencies": { - "@prisma/driver-adapter-utils": "7.2.0", + "@prisma/driver-adapter-utils": "7.8.0", + "@types/pg": "^8.16.0", "pg": "^8.16.3", "postgres-array": "3.0.4" } }, "node_modules/@prisma/client": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@prisma/client/-/client-7.2.0.tgz", - "integrity": "sha512-JdLF8lWZ+LjKGKpBqyAlenxd/kXjd1Abf/xK+6vUA7R7L2Suo6AFTHFRpPSdAKCan9wzdFApsUpSa/F6+t1AtA==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-7.8.0.tgz", + "integrity": "sha512-HFp3Dawv/3sU3JtlPha90IB+48lS7zHiH4LKZPjmcE8YH5P9DOXGPvo8dqOtO7MqLDd1p2hOWMcFlRT1DMblHw==", "license": "Apache-2.0", "dependencies": { - "@prisma/client-runtime-utils": "7.2.0" + "@prisma/client-runtime-utils": "7.8.0" }, "engines": { "node": "^20.19 || ^22.12 || >=24.0" @@ -1313,143 +1273,255 @@ } }, "node_modules/@prisma/client-runtime-utils": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@prisma/client-runtime-utils/-/client-runtime-utils-7.2.0.tgz", - "integrity": "sha512-dn7oB53v0tqkB0wBdMuTNFNPdEbfICEUe82Tn9FoKAhJCUkDH+fmyEp0ClciGh+9Hp2Tuu2K52kth2MTLstvmA==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@prisma/client-runtime-utils/-/client-runtime-utils-7.8.0.tgz", + "integrity": "sha512-5NQZztQ0oY/ADFkmd9gPuweH5A1/CCY8YQPorLLO0Mu6a87mY5gsnDkzmFmIHs9NFaLnZojzgddFVN4RpKYrdw==", "license": "Apache-2.0" }, "node_modules/@prisma/config": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@prisma/config/-/config-7.2.0.tgz", - "integrity": "sha512-qmvSnfQ6l/srBW1S7RZGfjTQhc44Yl3ldvU6y3pgmuLM+83SBDs6UQVgMtQuMRe9J3gGqB0RF8wER6RlXEr6jQ==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@prisma/config/-/config-7.8.0.tgz", + "integrity": "sha512-HFESzd9rx2ZQxlK+TL7tu1HPvCqrHiL6LCxYykI2c34mvaUuIVVl3lYuicJD/MNnzgPnyeBEMlK4WTomJCV5jw==", "license": "Apache-2.0", "dependencies": { - "c12": "3.1.0", + "c12": "3.3.4", "deepmerge-ts": "7.1.5", - "effect": "3.18.4", + "effect": "3.20.0", "empathic": "2.0.0" } }, "node_modules/@prisma/debug": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-7.2.0.tgz", - "integrity": "sha512-YSGTiSlBAVJPzX4ONZmMotL+ozJwQjRmZweQNIq/ER0tQJKJynNkRB3kyvt37eOfsbMCXk3gnLF6J9OJ4QWftw==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-7.8.0.tgz", + "integrity": "sha512-p+QZReysDUqXC+mk17q9a+Y/qzh4c2KYliDK30buYUyfrGeTGSyfmc0AIrJRhZJrLHhRiJa9Au/J72h3C+szvA==", "license": "Apache-2.0" }, "node_modules/@prisma/dev": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/@prisma/dev/-/dev-0.17.0.tgz", - "integrity": "sha512-6sGebe5jxX+FEsQTpjHLzvOGPn6ypFQprcs3jcuIWv1Xp/5v6P/rjfdvAwTkP2iF6pDx2tCd8vGLNWcsWzImTA==", + "version": "0.24.3", + "resolved": "https://registry.npmjs.org/@prisma/dev/-/dev-0.24.3.tgz", + "integrity": "sha512-ffHlQuKXZiaDt9Go0OnCTdJZrHxK0k7omJKNV86/VjpsXu5EIHZLK0T7JSWgvNlJwh56kW9JFu9v0qJciFzepg==", "license": "ISC", "dependencies": { - "@electric-sql/pglite": "0.3.2", - "@electric-sql/pglite-socket": "0.0.6", - "@electric-sql/pglite-tools": "0.2.7", - "@hono/node-server": "1.19.6", - "@mrleebo/prisma-ast": "0.12.1", - "@prisma/get-platform": "6.8.2", - "@prisma/query-plan-executor": "6.18.0", + "@electric-sql/pglite": "0.4.1", + "@electric-sql/pglite-socket": "0.1.1", + "@electric-sql/pglite-tools": "0.3.1", + "@hono/node-server": "1.19.11", + "@prisma/get-platform": "7.2.0", + "@prisma/query-plan-executor": "7.2.0", + "@prisma/streams-local": "0.1.2", "foreground-child": "3.3.1", - "get-port-please": "3.1.2", - "hono": "4.10.6", + "get-port-please": "3.2.0", + "hono": "^4.12.8", "http-status-codes": "2.3.0", "pathe": "2.0.3", "proper-lockfile": "4.1.2", - "remeda": "2.21.3", - "std-env": "3.9.0", + "remeda": "2.33.4", + "std-env": "3.10.0", "valibot": "1.2.0", - "zeptomatch": "2.0.2" + "zeptomatch": "2.1.0" } }, "node_modules/@prisma/driver-adapter-utils": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@prisma/driver-adapter-utils/-/driver-adapter-utils-7.2.0.tgz", - "integrity": "sha512-gzrUcbI9VmHS24Uf+0+7DNzdIw7keglJsD5m/MHxQOU68OhGVzlphQRobLiDMn8CHNA2XN8uugwKjudVtnfMVQ==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@prisma/driver-adapter-utils/-/driver-adapter-utils-7.8.0.tgz", + "integrity": "sha512-/Q13o0ZT0rjc1Xk0Q9KhZYwuq2EW/vSbWUBKfgEKkaCuB/Sg6bqnjmTZqC5cD4d6y1vfFAEwBRzfzoSMIVJ55A==", "license": "Apache-2.0", "dependencies": { - "@prisma/debug": "7.2.0" + "@prisma/debug": "7.8.0" } }, "node_modules/@prisma/engines": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-7.2.0.tgz", - "integrity": "sha512-HUeOI/SvCDsHrR9QZn24cxxZcujOjcS3w1oW/XVhnSATAli5SRMOfp/WkG3TtT5rCxDA4xOnlJkW7xkho4nURA==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-7.8.0.tgz", + "integrity": "sha512-jx3rCnNNrt5uzbkKlegtQ2GZHxSlihMCzutgT/BP6UIDF1r9tDI39hV/0T/cHZgzJ3ELbuQPXlVZy+Y1n0pcgw==", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "@prisma/debug": "7.2.0", - "@prisma/engines-version": "7.2.0-4.0c8ef2ce45c83248ab3df073180d5eda9e8be7a3", - "@prisma/fetch-engine": "7.2.0", - "@prisma/get-platform": "7.2.0" + "@prisma/debug": "7.8.0", + "@prisma/engines-version": "7.8.0-6.3c6e192761c0362d496ed980de936e2f3cebcd3a", + "@prisma/fetch-engine": "7.8.0", + "@prisma/get-platform": "7.8.0" } }, "node_modules/@prisma/engines-version": { - "version": "7.2.0-4.0c8ef2ce45c83248ab3df073180d5eda9e8be7a3", - "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-7.2.0-4.0c8ef2ce45c83248ab3df073180d5eda9e8be7a3.tgz", - "integrity": "sha512-KezsjCZDsbjNR7SzIiVlUsn9PnLePI7r5uxABlwL+xoerurZTfgQVbIjvjF2sVr3Uc0ZcsnREw3F84HvbggGdA==", + "version": "7.8.0-6.3c6e192761c0362d496ed980de936e2f3cebcd3a", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-7.8.0-6.3c6e192761c0362d496ed980de936e2f3cebcd3a.tgz", + "integrity": "sha512-fJPQxCkLgA5EayWaW8eArgCvjJ+N+Kz3VyeNKMEeYiQC4alNkxRKFVAGxv/ZUzuJISKqdw+zGeDbS6mn6RCPOA==", "license": "Apache-2.0" }, "node_modules/@prisma/engines/node_modules/@prisma/get-platform": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.2.0.tgz", - "integrity": "sha512-k1V0l0Td1732EHpAfi2eySTezyllok9dXb6UQanajkJQzPUGi3vO2z7jdkz67SypFTdmbnyGYxvEvYZdZsMAVA==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.8.0.tgz", + "integrity": "sha512-WlxgRGnolL8VH2EmkH1R/DkKNr/mVdS3G2h42IZFFZ3eUrH9OT6t73kIOSlkkrv50wG123Iq8d96ufv5LlZktw==", "license": "Apache-2.0", "dependencies": { - "@prisma/debug": "7.2.0" + "@prisma/debug": "7.8.0" } }, "node_modules/@prisma/fetch-engine": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-7.2.0.tgz", - "integrity": "sha512-Z5XZztJ8Ap+wovpjPD2lQKnB8nWFGNouCrglaNFjxIWAGWz0oeHXwUJRiclIoSSXN/ptcs9/behptSk8d0Yy6w==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-7.8.0.tgz", + "integrity": "sha512-gwB0Euiz/DDRyxFRpLXYlK3RfaZUj1c5dAYMuhZYfApg7arknJlcb9bIsOHDppJmbqYaVA+yBIiFMDBfprsNPQ==", "license": "Apache-2.0", "dependencies": { - "@prisma/debug": "7.2.0", - "@prisma/engines-version": "7.2.0-4.0c8ef2ce45c83248ab3df073180d5eda9e8be7a3", - "@prisma/get-platform": "7.2.0" + "@prisma/debug": "7.8.0", + "@prisma/engines-version": "7.8.0-6.3c6e192761c0362d496ed980de936e2f3cebcd3a", + "@prisma/get-platform": "7.8.0" } }, "node_modules/@prisma/fetch-engine/node_modules/@prisma/get-platform": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.2.0.tgz", - "integrity": "sha512-k1V0l0Td1732EHpAfi2eySTezyllok9dXb6UQanajkJQzPUGi3vO2z7jdkz67SypFTdmbnyGYxvEvYZdZsMAVA==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.8.0.tgz", + "integrity": "sha512-WlxgRGnolL8VH2EmkH1R/DkKNr/mVdS3G2h42IZFFZ3eUrH9OT6t73kIOSlkkrv50wG123Iq8d96ufv5LlZktw==", "license": "Apache-2.0", "dependencies": { - "@prisma/debug": "7.2.0" + "@prisma/debug": "7.8.0" } }, "node_modules/@prisma/get-platform": { - "version": "6.8.2", - "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.8.2.tgz", - "integrity": "sha512-vXSxyUgX3vm1Q70QwzwkjeYfRryIvKno1SXbIqwSptKwqKzskINnDUcx85oX+ys6ooN2ATGSD0xN2UTfg6Zcow==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.2.0.tgz", + "integrity": "sha512-k1V0l0Td1732EHpAfi2eySTezyllok9dXb6UQanajkJQzPUGi3vO2z7jdkz67SypFTdmbnyGYxvEvYZdZsMAVA==", "license": "Apache-2.0", "dependencies": { - "@prisma/debug": "6.8.2" + "@prisma/debug": "7.2.0" } }, "node_modules/@prisma/get-platform/node_modules/@prisma/debug": { - "version": "6.8.2", - "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.8.2.tgz", - "integrity": "sha512-4muBSSUwJJ9BYth5N8tqts8JtiLT8QI/RSAzEogwEfpbYGFo9mYsInsVo8dqXdPO2+Rm5OG5q0qWDDE3nyUbVg==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-7.2.0.tgz", + "integrity": "sha512-YSGTiSlBAVJPzX4ONZmMotL+ozJwQjRmZweQNIq/ER0tQJKJynNkRB3kyvt37eOfsbMCXk3gnLF6J9OJ4QWftw==", "license": "Apache-2.0" }, "node_modules/@prisma/query-plan-executor": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/@prisma/query-plan-executor/-/query-plan-executor-6.18.0.tgz", - "integrity": "sha512-jZ8cfzFgL0jReE1R10gT8JLHtQxjWYLiQ//wHmVYZ2rVkFHoh0DT8IXsxcKcFlfKN7ak7k6j0XMNn2xVNyr5cA==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@prisma/query-plan-executor/-/query-plan-executor-7.2.0.tgz", + "integrity": "sha512-EOZmNzcV8uJ0mae3DhTsiHgoNCuu1J9mULQpGCh62zN3PxPTd+qI9tJvk5jOst8WHKQNwJWR3b39t0XvfBB0WQ==", "license": "Apache-2.0" }, + "node_modules/@prisma/streams-local": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@prisma/streams-local/-/streams-local-0.1.2.tgz", + "integrity": "sha512-l49yTxKKF2odFxaAXTmwmkBKL3+bVQ1tFOooGifu4xkdb9NMNLxHj27XAhTylWZod8I+ISGM5erU1xcl/oBCtg==", + "license": "Apache-2.0", + "dependencies": { + "ajv": "^8.12.0", + "better-result": "^2.7.0", + "env-paths": "^3.0.0", + "proper-lockfile": "^4.1.2" + }, + "engines": { + "bun": ">=1.3.6", + "node": ">=22.0.0" + } + }, + "node_modules/@prisma/streams-local/node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@prisma/streams-local/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" + }, "node_modules/@prisma/studio-core": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/@prisma/studio-core/-/studio-core-0.9.0.tgz", - "integrity": "sha512-xA2zoR/ADu/NCSQuriBKTh6Ps4XjU0bErkEcgMfnSGh346K1VI7iWKnoq1l2DoxUqiddPHIEWwtxJ6xCHG6W7g==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@prisma/studio-core/-/studio-core-0.27.3.tgz", + "integrity": "sha512-AADjNFPdsrglxHQVTmHFqv6DuKQZ5WY4p5/gVFY017twvNrSwpLJ9lqUbYYxEu2W7nbvVxTZA8deJ8LseNALsw==", "license": "Apache-2.0", + "dependencies": { + "@radix-ui/react-toggle": "1.1.10", + "chart.js": "4.5.1" + }, + "engines": { + "node": "^20.19 || ^22.12 || >=24.0", + "pnpm": "8" + }, "peerDependencies": { "@types/react": "^18.0.0 || ^19.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, + "node_modules/@prisma/studio-core/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@prisma/studio-core/node_modules/@radix-ui/react-toggle": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.10.tgz", + "integrity": "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@prisma/studio-core/node_modules/@radix-ui/react-toggle/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/primitive": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", @@ -2713,9 +2785,9 @@ "license": "MIT" }, "node_modules/@reduxjs/toolkit": { - "version": "2.11.2", - "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.2.tgz", - "integrity": "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==", + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.12.0.tgz", + "integrity": "sha512-KiT+RzZbp6mQET+Mg+h2c97+9j1sNflUxQkIHI7Yuzf6Peu+OYpmkn6nbHWmLLWj+1ZODUJFwGZ7gx3L9R9EOw==", "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.0.0", @@ -2739,9 +2811,9 @@ } }, "node_modules/@reduxjs/toolkit/node_modules/immer": { - "version": "11.1.3", - "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.3.tgz", - "integrity": "sha512-6jQTc5z0KJFtr1UgFpIL3N9XSC3saRaI9PwWtzM2pSqkNGtiNkYY2OSwkOGDK2XcTRcLb1pi/aNkKZz0nxVH4Q==", + "version": "11.1.8", + "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.8.tgz", + "integrity": "sha512-/tbkHMW7y10Lx6i1crLjD4/OhNkRG+Fo7byZHtah0547nIeXYcpIXaUh0IAQY6gO5459qpGGYapcEOHtFXkIuA==", "license": "MIT", "funding": { "type": "opencollective", @@ -2756,9 +2828,9 @@ "license": "MIT" }, "node_modules/@rushstack/eslint-patch": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.15.0.tgz", - "integrity": "sha512-ojSshQPKwVvSMR8yT2L/QtUkV5SXi/IfDiJ4/8d6UbTPjiHVmxZzUAzGD8Tzks1b9+qQkZa0isUOvYObedITaw==", + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.16.1.tgz", + "integrity": "sha512-TvZbIpeKqGQQ7X0zSCvPH9riMSFQFSggnfBjFZ1mEoILW+UuXCKwOoPcgjMwiUtRqFZ8jWhPJc4um14vC6I4ag==", "dev": true, "license": "MIT" }, @@ -2791,9 +2863,9 @@ } }, "node_modules/@tybys/wasm-util": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", - "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", + "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", "dev": true, "license": "MIT", "optional": true, @@ -3016,9 +3088,9 @@ "license": "MIT" }, "node_modules/@types/d3-shape": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", - "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", + "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", "license": "MIT", "dependencies": { "@types/d3-path": "*" @@ -3062,18 +3134,18 @@ } }, "node_modules/@types/debug": { - "version": "4.1.12", - "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", - "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", + "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", "license": "MIT", "dependencies": { "@types/ms": "*" } }, "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", "license": "MIT" }, "node_modules/@types/estree-jsx": { @@ -3141,18 +3213,18 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "25.0.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.3.tgz", - "integrity": "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==", + "version": "25.9.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.1.tgz", + "integrity": "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==", "license": "MIT", "dependencies": { - "undici-types": "~7.16.0" + "undici-types": ">=7.24.0 <7.24.7" } }, "node_modules/@types/pg": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.16.0.tgz", - "integrity": "sha512-RmhMd/wD+CF8Dfo+cVIy3RR5cl8CyfXQ0tGgW6XBL8L4LM/UTEbNXYRbLwU6w+CgrKBNbrQWt4FUtTfaU5jSYQ==", + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.20.0.tgz", + "integrity": "sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==", "license": "MIT", "dependencies": { "@types/node": "*", @@ -3164,12 +3236,14 @@ "version": "15.7.15", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true, "license": "MIT" }, "node_modules/@types/react": { - "version": "18.3.27", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz", - "integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==", + "version": "18.3.29", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.29.tgz", + "integrity": "sha512-ch0qJdr2JY0r04NXSprbK6TXOgnaJ1Tz23fm5W+z0/CBah6BSBc3n96h7K9GOtwh0HrilNWHIBzE1Ko4Dcw/Wg==", + "dev": true, "license": "MIT", "dependencies": { "@types/prop-types": "*", @@ -3180,7 +3254,7 @@ "version": "18.3.7", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", - "devOptional": true, + "dev": true, "license": "MIT", "peerDependencies": { "@types/react": "^18.0.0" @@ -3404,15 +3478,15 @@ } }, "node_modules/@ungap/structured-clone": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", - "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.1.tgz", + "integrity": "sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ==", "license": "ISC" }, "node_modules/@unrs/resolver-binding-android-arm-eabi": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz", - "integrity": "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.12.2.tgz", + "integrity": "sha512-g5T90pqg1bo/7mytQx6F4iBNC0Wsh9cu+z9veDbFjc7HjpesJFWD7QMS0NGStXM075+7dJPPVvBbpZlnrdpi/w==", "cpu": [ "arm" ], @@ -3424,9 +3498,9 @@ ] }, "node_modules/@unrs/resolver-binding-android-arm64": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz", - "integrity": "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.12.2.tgz", + "integrity": "sha512-YGCRZv/9GLhwmz6mYDeTsm/92BAyR28l6c2ReweVW5pWgfsitWLY8upvfRlGdoyD8HjeTHSYJWyZGD4KJA/nFQ==", "cpu": [ "arm64" ], @@ -3438,9 +3512,9 @@ ] }, "node_modules/@unrs/resolver-binding-darwin-arm64": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.1.tgz", - "integrity": "sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.12.2.tgz", + "integrity": "sha512-u9DiNT1auQMO20A9SyTuG3wUgQWB9Z7KjAg0uFuCDR1FsAY8A0CG2S6JpHS1xwm/w1G08bjXZDcyOCjv1WAm2w==", "cpu": [ "arm64" ], @@ -3452,9 +3526,9 @@ ] }, "node_modules/@unrs/resolver-binding-darwin-x64": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz", - "integrity": "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.12.2.tgz", + "integrity": "sha512-f7rPLi/T1HVKZu/u6t87lroib16n8vrSzcyxI7lg4BGO9UF26KhQL44sd9eOUgrTYhvRXtWOIZT5PejdPyJfUA==", "cpu": [ "x64" ], @@ -3466,9 +3540,9 @@ ] }, "node_modules/@unrs/resolver-binding-freebsd-x64": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.11.1.tgz", - "integrity": "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.12.2.tgz", + "integrity": "sha512-BpcOjWCJub6nRZUS2zA20pmLvjtqAtGejETaIyRLiZiQf++cbrjltLA5NN/xaXfqeOBOSlMFbemIl5/S5tljmg==", "cpu": [ "x64" ], @@ -3480,9 +3554,9 @@ ] }, "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.11.1.tgz", - "integrity": "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.12.2.tgz", + "integrity": "sha512-vZTDvdSISZjJx66OzJqtsOhzifbqRjbmI1Mnu49fQDwog5GtDI4QidRiEAYbZCRj9C8YZEW+3ZjqsyS9GR4k2A==", "cpu": [ "arm" ], @@ -3494,9 +3568,9 @@ ] }, "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.11.1.tgz", - "integrity": "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.12.2.tgz", + "integrity": "sha512-BiPI+IrIlwcW4nLLMM21+B1dFPzd55yAVgVGrdgDjNef+ch03GdxrcyaIz8X9SsQirh/kCQ7mviyWlMxdh2D7g==", "cpu": [ "arm" ], @@ -3508,9 +3582,9 @@ ] }, "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.1.tgz", - "integrity": "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.12.2.tgz", + "integrity": "sha512-zJc0H99FEPoFfSrNpa91HYfxzfAJCr502oxNK1cfdC9hlaFI43RT+JFCann9JUgZmLzzntChHyn13Sgn9ljHNg==", "cpu": [ "arm64" ], @@ -3522,9 +3596,9 @@ ] }, "node_modules/@unrs/resolver-binding-linux-arm64-musl": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.1.tgz", - "integrity": "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.12.2.tgz", + "integrity": "sha512-KQ3Lki6l+Pz1k/eBipN41ES+YUK30beLGb9YqcB1O542cyLCNE6GaxrfcY3T6EezmGGk84wb5XyO9loTM9tkcA==", "cpu": [ "arm64" ], @@ -3535,10 +3609,38 @@ "linux" ] }, + "node_modules/@unrs/resolver-binding-linux-loong64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-loong64-gnu/-/resolver-binding-linux-loong64-gnu-1.12.2.tgz", + "integrity": "sha512-3SJGEh1DborhG6pyxvhPzCT4bbSIVihsvgJc13P1bHG7KLdNDaF9T3gsTwFc7Jw/5Y5/iWOjkEx7Zy0NvCGX3Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-loong64-musl": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-loong64-musl/-/resolver-binding-linux-loong64-musl-1.12.2.tgz", + "integrity": "sha512-jiuG/Obbel7uw1PwHNFfrkiKhLAF6mnyZ6aWlOAVN9WqKm8v0OFGnciJIHu8+CMvXLQ8AD51LPzAoUfT21D5Ew==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.1.tgz", - "integrity": "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.12.2.tgz", + "integrity": "sha512-q7xRvVpmcfeL+LlZg8Pbbo6QaTZwDU5BaGZbwfhkEsXJn3Was8xYfE0RBH266xZt0rM6B7i8xAYIvjthuUIWHg==", "cpu": [ "ppc64" ], @@ -3550,9 +3652,9 @@ ] }, "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.1.tgz", - "integrity": "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.12.2.tgz", + "integrity": "sha512-0CVdx6lcnT3Q9inOH8tsMIOJ6ImndllMjqJHg8RLVdB7Vq4SfkEXl9mCSsVNuNA4MCYycRicCUxPCabVHJRr6A==", "cpu": [ "riscv64" ], @@ -3564,9 +3666,9 @@ ] }, "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.1.tgz", - "integrity": "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.12.2.tgz", + "integrity": "sha512-iOwlRo9vnp6R6ohHQS11n0NnfdXx/omhkocmIfaPRpQhKZ+3BDMkkdRVh53qjkFkpPddf+FETA28NwGN7l5l+w==", "cpu": [ "riscv64" ], @@ -3578,9 +3680,9 @@ ] }, "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.1.tgz", - "integrity": "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.12.2.tgz", + "integrity": "sha512-HYJtLfXq94q8iZNFT1lknx258wlkkWhZeUXJRqzKBBUJ00CvZ+N33zgbCqimLjsyw5Va6uUxhVa12mI+kaveEw==", "cpu": [ "s390x" ], @@ -3592,9 +3694,9 @@ ] }, "node_modules/@unrs/resolver-binding-linux-x64-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz", - "integrity": "sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.12.2.tgz", + "integrity": "sha512-mPsUhunKKDih5O96Y6enDQyHc1SqBPlY1E/SfMWDM3EdJ95Z9CArPeCVwCCqbP45ljvivdEk8Fxn+SIb1rDAJQ==", "cpu": [ "x64" ], @@ -3606,9 +3708,9 @@ ] }, "node_modules/@unrs/resolver-binding-linux-x64-musl": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz", - "integrity": "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.12.2.tgz", + "integrity": "sha512-azrt6+5ydLd8Vt210AAFis/lZevSfPw93EJRIJG+xPu4WCJ8K0kppCTpMyLPcKT7H15M4Jnt2tMp5bOvCkRC6A==", "cpu": [ "x64" ], @@ -3619,10 +3721,24 @@ "linux" ] }, + "node_modules/@unrs/resolver-binding-openharmony-arm64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-openharmony-arm64/-/resolver-binding-openharmony-arm64-1.12.2.tgz", + "integrity": "sha512-YZ9hP4O0X9PQb8eO980qmLNGH4zT3I9+SZTdt0Pr0YyuGQhYKoOZkV02VzrzyOZJ5xIJ3UFIenKkUkGg8GjgWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, "node_modules/@unrs/resolver-binding-wasm32-wasi": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz", - "integrity": "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.12.2.tgz", + "integrity": "sha512-tYFDIkMxSflfEc/h92ZWNsZlHSwgimbNHSO3PL2JWQHfCuC2q316jMyYU9TIWZsFK2bQwyK5VAdYgn8ygPj69A==", "cpu": [ "wasm32" ], @@ -3630,16 +3746,18 @@ "license": "MIT", "optional": true, "dependencies": { - "@napi-rs/wasm-runtime": "^0.2.11" + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" }, "engines": { "node": ">=14.0.0" } }, "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz", - "integrity": "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.12.2.tgz", + "integrity": "sha512-qzNyg3xL0VPQmCaUh+N5jSitce6k+uCBfMDesWRnlULOZaqUkaJ0ybdT+UqlAWJoQjuqfIU/0Ptx9bteN4D82g==", "cpu": [ "arm64" ], @@ -3651,9 +3769,9 @@ ] }, "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz", - "integrity": "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.12.2.tgz", + "integrity": "sha512-WD9sY00OfpHVGfsnHZoA8jVT+esS/Bg8z8jzxp5BnDCjjwsuKsPQrzswwpFy4J1AUJbXPRfkpcX0mXrzeXW79g==", "cpu": [ "ia32" ], @@ -3665,9 +3783,9 @@ ] }, "node_modules/@unrs/resolver-binding-win32-x64-msvc": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz", - "integrity": "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.12.2.tgz", + "integrity": "sha512-nAB74NfSNKknqQ1RrYj6uz8FcXEomu/MATJZxh/x+BArzN2U3JbOYC0APYzUIGhVY3m5hRxA8VPNdPBoG8txlA==", "cpu": [ "x64" ], @@ -3679,9 +3797,9 @@ ] }, "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", "bin": { @@ -3702,18 +3820,21 @@ } }, "node_modules/agent-base": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", "license": "MIT", + "dependencies": { + "debug": "4" + }, "engines": { - "node": ">= 14" + "node": ">= 6.0.0" } }, "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", "dev": true, "license": "MIT", "dependencies": { @@ -3731,6 +3852,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -3740,6 +3862,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -4002,9 +4125,9 @@ "license": "MIT" }, "node_modules/autoprefixer": { - "version": "10.4.23", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.23.tgz", - "integrity": "sha512-YYTXSFulfwytnjAPlw8QHncHJmlvFKtczb8InXaAx9Q0LbfDnfEYDE55omerIJKihhmU61Ft+cAOSzQVaBUmeA==", + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.0.tgz", + "integrity": "sha512-FMhOoZV4+qR6aTUALKX2rEqGG+oyATvwBt9IIzVR5rMa2HRWPkxf+P+PAJLD1I/H5/II+HuZcBJYEFBpq39ong==", "dev": true, "funding": [ { @@ -4022,8 +4145,8 @@ ], "license": "MIT", "dependencies": { - "browserslist": "^4.28.1", - "caniuse-lite": "^1.0.30001760", + "browserslist": "^4.28.2", + "caniuse-lite": "^1.0.30001787", "fraction.js": "^5.3.4", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" @@ -4064,9 +4187,9 @@ } }, "node_modules/axe-core": { - "version": "4.11.0", - "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.0.tgz", - "integrity": "sha512-ilYanEU8vxxBexpJd8cWM4ElSQq4QctCLKih0TSfjIfCQTeyH/6zVrmIJfLPrKTKJRbiG+cfnZbQIjAlJmF1jQ==", + "version": "4.11.4", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.4.tgz", + "integrity": "sha512-KunSNx+TVpkAw/6ULfhnx+HWRecjqZGTOyquAoWHYLRSdK1tB5Ihce1ZW+UY3fj33bYAFWPu7W/GRSmmrCGuxA==", "dev": true, "license": "MPL-2.0", "engines": { @@ -4074,14 +4197,15 @@ } }, "node_modules/axios": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", - "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.16.1.tgz", + "integrity": "sha512-caYkukvroVPO8KrzuJEb50Hm07KwfBZPEC3VeFHTsqWHvKTsy54hjJz9BS/cdaypROE2rH6xvm9mHX4fgWkr3A==", "license": "MIT", "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.4", - "proxy-from-env": "^1.1.0" + "follow-redirects": "^1.16.0", + "form-data": "^4.0.5", + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" } }, "node_modules/axobject-query": { @@ -4108,6 +4232,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, "license": "MIT" }, "node_modules/base64-js": { @@ -4131,27 +4256,16 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.9.11", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.11.tgz", - "integrity": "sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ==", + "version": "2.10.31", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.31.tgz", + "integrity": "sha512-MujYO3eP72uvmSE0i4wltsodRfIpZATP3jvzRNRGGxgzId7aVocVJJV3nf01qnzzKFGxQVC9bpWxl5cjxTr/7Q==", "dev": true, "license": "Apache-2.0", "bin": { - "baseline-browser-mapping": "dist/cli.js" - } - }, - "node_modules/bcrypt": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-6.0.0.tgz", - "integrity": "sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "node-addon-api": "^8.3.0", - "node-gyp-build": "^4.8.4" + "baseline-browser-mapping": "dist/cli.cjs" }, "engines": { - "node": ">= 18" + "node": ">=6.0.0" } }, "node_modules/bcryptjs": { @@ -4163,6 +4277,12 @@ "bcrypt": "bin/bcrypt" } }, + "node_modules/better-result": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/better-result/-/better-result-2.9.2.tgz", + "integrity": "sha512-WIFoBPCdnTOdk9inkE1ZRvCZ4P0CpSkAiLlchC65N7n9DcjZ3NhqkBOlafzpOVnO8ixyi37kicmSJ3ENhPZl7Q==", + "license": "MIT" + }, "node_modules/bignumber.js": { "version": "9.3.1", "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", @@ -4186,9 +4306,10 @@ } }, "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==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", + "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" @@ -4208,9 +4329,9 @@ } }, "node_modules/browserslist": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", - "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", "dev": true, "funding": [ { @@ -4228,11 +4349,11 @@ ], "license": "MIT", "dependencies": { - "baseline-browser-mapping": "^2.9.0", - "caniuse-lite": "^1.0.30001759", - "electron-to-chromium": "^1.5.263", - "node-releases": "^2.0.27", - "update-browserslist-db": "^1.2.0" + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" @@ -4259,26 +4380,26 @@ } }, "node_modules/c12": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/c12/-/c12-3.1.0.tgz", - "integrity": "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw==", - "license": "MIT", - "dependencies": { - "chokidar": "^4.0.3", - "confbox": "^0.2.2", - "defu": "^6.1.4", - "dotenv": "^16.6.1", - "exsolve": "^1.0.7", - "giget": "^2.0.0", - "jiti": "^2.4.2", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/c12/-/c12-3.3.4.tgz", + "integrity": "sha512-cM0ApFQSBXuourJejzwv/AuPRvAxordTyParRVcHjjtXirtkzM0uK2L9TTn9s0cXZbG7E55jCivRQzoxYmRAlA==", + "license": "MIT", + "dependencies": { + "chokidar": "^5.0.0", + "confbox": "^0.2.4", + "defu": "^6.1.6", + "dotenv": "^17.3.1", + "exsolve": "^1.0.8", + "giget": "^3.2.0", + "jiti": "^2.6.1", "ohash": "^2.0.11", "pathe": "^2.0.3", - "perfect-debounce": "^1.0.0", - "pkg-types": "^2.2.0", - "rc9": "^2.1.2" + "perfect-debounce": "^2.1.0", + "pkg-types": "^2.3.0", + "rc9": "^3.0.1" }, "peerDependencies": { - "magicast": "^0.3.5" + "magicast": "*" }, "peerDependenciesMeta": { "magicast": { @@ -4286,16 +4407,28 @@ } } }, + "node_modules/c12/node_modules/dotenv": { + "version": "17.4.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", + "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/call-bind": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", - "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", + "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", "dev": true, "license": "MIT", "dependencies": { - "call-bind-apply-helpers": "^1.0.0", - "es-define-property": "^1.0.0", - "get-intrinsic": "^1.2.4", + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "get-intrinsic": "^1.3.0", "set-function-length": "^1.2.2" }, "engines": { @@ -4356,9 +4489,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001762", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001762.tgz", - "integrity": "sha512-PxZwGNvH7Ak8WX5iXzoK1KPZttBXNPuaOvI2ZYU7NrlM+d9Ov+TUvlLOBNGzVXAntMSMMlJPd+jY6ovrVjSmUw==", + "version": "1.0.30001793", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001793.tgz", + "integrity": "sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==", "funding": [ { "type": "opencollective", @@ -4442,44 +4575,33 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/chevrotain": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-10.5.0.tgz", - "integrity": "sha512-Pkv5rBY3+CsHOYfV5g/Vs5JY9WTHHDEKOlohI2XeygaZhUeqhAlldZ8Hz9cRmxu709bvS08YzxHdTPHhffc13A==", - "license": "Apache-2.0", + "node_modules/chart.js": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz", + "integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==", + "license": "MIT", "dependencies": { - "@chevrotain/cst-dts-gen": "10.5.0", - "@chevrotain/gast": "10.5.0", - "@chevrotain/types": "10.5.0", - "@chevrotain/utils": "10.5.0", - "lodash": "4.17.21", - "regexp-to-ast": "0.5.0" + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" } }, "node_modules/chokidar": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", - "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", "license": "MIT", "dependencies": { - "readdirp": "^4.0.1" + "readdirp": "^5.0.0" }, "engines": { - "node": ">= 14.16.0" + "node": ">= 20.19.0" }, "funding": { "url": "https://paulmillr.com/funding/" } }, - "node_modules/citty": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz", - "integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==", - "license": "MIT", - "dependencies": { - "consola": "^3.2.3" - } - }, "node_modules/class-variance-authority": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", @@ -4511,6 +4633,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -4523,6 +4646,7 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, "license": "MIT" }, "node_modules/combined-stream": { @@ -4564,20 +4688,11 @@ "license": "MIT" }, "node_modules/confbox": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.2.tgz", - "integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==", + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz", + "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==", "license": "MIT" }, - "node_modules/consola": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", - "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", - "license": "MIT", - "engines": { - "node": "^14.18.0 || >=16.10.0" - } - }, "node_modules/cookie": { "version": "0.7.2", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", @@ -4587,6 +4702,24 @@ "node": ">= 0.6" } }, + "node_modules/cross-env": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-10.1.0.tgz", + "integrity": "sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@epic-web/invariant": "^1.0.0", + "cross-spawn": "^7.0.6" + }, + "bin": { + "cross-env": "dist/bin/cross-env.js", + "cross-env-shell": "dist/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -4618,6 +4751,7 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, "license": "MIT" }, "node_modules/d3": { @@ -4826,9 +4960,9 @@ } }, "node_modules/d3-format": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", - "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", "license": "ISC", "engines": { "node": ">=12" @@ -5115,9 +5249,9 @@ "license": "MIT" }, "node_modules/decode-named-character-reference": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.2.0.tgz", - "integrity": "sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", + "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==", "license": "MIT", "dependencies": { "character-entities": "^2.0.0" @@ -5180,15 +5314,15 @@ } }, "node_modules/defu": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", - "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", + "version": "6.1.7", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz", + "integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==", "license": "MIT" }, "node_modules/delaunator": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", - "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.1.0.tgz", + "integrity": "sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==", "license": "ISC", "dependencies": { "robust-predicates": "^3.0.2" @@ -5316,6 +5450,7 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, "license": "MIT" }, "node_modules/ecdsa-sig-formatter": { @@ -5328,9 +5463,9 @@ } }, "node_modules/effect": { - "version": "3.18.4", - "resolved": "https://registry.npmjs.org/effect/-/effect-3.18.4.tgz", - "integrity": "sha512-b1LXQJLe9D11wfnOKAk3PKxuqYshQ0Heez+y5pnkd3jLj1yx9QhM72zZ9uUrOQyNvrs2GZZd/3maL0ZV18YuDA==", + "version": "3.20.0", + "resolved": "https://registry.npmjs.org/effect/-/effect-3.20.0.tgz", + "integrity": "sha512-qMLfDJscrNG8p/aw+IkT9W7fgj50Z4wG5bLBy0Txsxz8iUHjDIkOgO3SV0WZfnQbNG2VJYb0b+rDLMrhM4+Krw==", "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.0.0", @@ -5338,9 +5473,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.267", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", - "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==", + "version": "1.5.360", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.360.tgz", + "integrity": "sha512-GkcBt6YYAw9SxFWn+xVar4cLVGlXVuswwtRLBozi2zp0GjXs4ZnOrqV4zbXzg35n7w81hCkyJNYicgXlVHAmBA==", "dev": true, "license": "ISC" }, @@ -5348,6 +5483,7 @@ "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, "license": "MIT" }, "node_modules/empathic": { @@ -5371,10 +5507,22 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/env-paths": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-3.0.0.tgz", + "integrity": "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/es-abstract": { - "version": "1.24.1", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.1.tgz", - "integrity": "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==", + "version": "1.24.2", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.2.tgz", + "integrity": "sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==", "dev": true, "license": "MIT", "dependencies": { @@ -5459,16 +5607,16 @@ } }, "node_modules/es-iterator-helpers": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.2.tgz", - "integrity": "sha512-BrUQ0cPTB/IwXj23HtwHjS9n7O4h9FX94b4xc5zlTHxeLgTAdzYUDyy6KdExAl9lbN5rtfe44xpjpmj9grxs5w==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.3.2.tgz", + "integrity": "sha512-HVLACW1TppGYjJ8H6/jqH/pqOtKRw6wMlrB23xfExmFWxFquAIWCmwoLsOyN96K4a5KbmOf5At9ZUO3GZbetAw==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.8", + "call-bind": "^1.0.9", "call-bound": "^1.0.4", "define-properties": "^1.2.1", - "es-abstract": "^1.24.1", + "es-abstract": "^1.24.2", "es-errors": "^1.3.0", "es-set-tostringtag": "^2.1.0", "function-bind": "^1.1.2", @@ -5480,7 +5628,7 @@ "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", "iterator.prototype": "^1.1.5", - "safe-array-concat": "^1.1.3" + "math-intrinsics": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -5545,9 +5693,9 @@ } }, "node_modules/es-toolkit": { - "version": "1.43.0", - "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.43.0.tgz", - "integrity": "sha512-SKCT8AsWvYzBBuUqMk4NPwFlSdqLpJwmy6AP322ERn8W2YLIB6JBXnwMI2Qsh2gfphT3q7EKAxKb23cvFHFwKA==", + "version": "1.46.1", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.46.1.tgz", + "integrity": "sha512-5eNtXOs3tbfxXOj04tjjseeWkRWaoCjdEI+96DgwzZoe6c9juL49pXlzAFTI72aWC9Y8p7168g6XIKjh7k6pyQ==", "license": "MIT", "workspaces": [ "docs", @@ -5555,9 +5703,9 @@ ] }, "node_modules/esbuild": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", - "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz", + "integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -5568,32 +5716,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.2", - "@esbuild/android-arm": "0.27.2", - "@esbuild/android-arm64": "0.27.2", - "@esbuild/android-x64": "0.27.2", - "@esbuild/darwin-arm64": "0.27.2", - "@esbuild/darwin-x64": "0.27.2", - "@esbuild/freebsd-arm64": "0.27.2", - "@esbuild/freebsd-x64": "0.27.2", - "@esbuild/linux-arm": "0.27.2", - "@esbuild/linux-arm64": "0.27.2", - "@esbuild/linux-ia32": "0.27.2", - "@esbuild/linux-loong64": "0.27.2", - "@esbuild/linux-mips64el": "0.27.2", - "@esbuild/linux-ppc64": "0.27.2", - "@esbuild/linux-riscv64": "0.27.2", - "@esbuild/linux-s390x": "0.27.2", - "@esbuild/linux-x64": "0.27.2", - "@esbuild/netbsd-arm64": "0.27.2", - "@esbuild/netbsd-x64": "0.27.2", - "@esbuild/openbsd-arm64": "0.27.2", - "@esbuild/openbsd-x64": "0.27.2", - "@esbuild/openharmony-arm64": "0.27.2", - "@esbuild/sunos-x64": "0.27.2", - "@esbuild/win32-arm64": "0.27.2", - "@esbuild/win32-ia32": "0.27.2", - "@esbuild/win32-x64": "0.27.2" + "@esbuild/aix-ppc64": "0.28.0", + "@esbuild/android-arm": "0.28.0", + "@esbuild/android-arm64": "0.28.0", + "@esbuild/android-x64": "0.28.0", + "@esbuild/darwin-arm64": "0.28.0", + "@esbuild/darwin-x64": "0.28.0", + "@esbuild/freebsd-arm64": "0.28.0", + "@esbuild/freebsd-x64": "0.28.0", + "@esbuild/linux-arm": "0.28.0", + "@esbuild/linux-arm64": "0.28.0", + "@esbuild/linux-ia32": "0.28.0", + "@esbuild/linux-loong64": "0.28.0", + "@esbuild/linux-mips64el": "0.28.0", + "@esbuild/linux-ppc64": "0.28.0", + "@esbuild/linux-riscv64": "0.28.0", + "@esbuild/linux-s390x": "0.28.0", + "@esbuild/linux-x64": "0.28.0", + "@esbuild/netbsd-arm64": "0.28.0", + "@esbuild/netbsd-x64": "0.28.0", + "@esbuild/openbsd-arm64": "0.28.0", + "@esbuild/openbsd-x64": "0.28.0", + "@esbuild/openharmony-arm64": "0.28.0", + "@esbuild/sunos-x64": "0.28.0", + "@esbuild/win32-arm64": "0.28.0", + "@esbuild/win32-ia32": "0.28.0", + "@esbuild/win32-x64": "0.28.0" } }, "node_modules/escalade": { @@ -5740,15 +5888,15 @@ } }, "node_modules/eslint-import-resolver-node": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", - "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", + "version": "0.3.10", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.10.tgz", + "integrity": "sha512-tRrKqFyCaKict5hOd244sL6EQFNycnMQnBe+j8uqGNXYzsImGbGUU4ibtoaBmv5FLwJwcFJNeg1GeVjQfbMrDQ==", "dev": true, "license": "MIT", "dependencies": { "debug": "^3.2.7", - "is-core-module": "^2.13.0", - "resolve": "^1.22.4" + "is-core-module": "^2.16.1", + "resolve": "^2.0.0-next.6" } }, "node_modules/eslint-import-resolver-node/node_modules/debug": { @@ -5824,9 +5972,9 @@ } }, "node_modules/eslint-plugin-import/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": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", "dev": true, "license": "MIT", "dependencies": { @@ -5858,9 +6006,9 @@ } }, "node_modules/eslint-plugin-import/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": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -5911,9 +6059,9 @@ } }, "node_modules/eslint-plugin-jsx-a11y/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": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", "dev": true, "license": "MIT", "dependencies": { @@ -5922,9 +6070,9 @@ } }, "node_modules/eslint-plugin-jsx-a11y/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": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -5981,9 +6129,9 @@ } }, "node_modules/eslint-plugin-react/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": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", "dev": true, "license": "MIT", "dependencies": { @@ -6005,9 +6153,9 @@ } }, "node_modules/eslint-plugin-react/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": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -6017,24 +6165,6 @@ "node": "*" } }, - "node_modules/eslint-plugin-react/node_modules/resolve": { - "version": "2.0.0-next.5", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", - "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-core-module": "^2.13.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/eslint-plugin-react/node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -6076,9 +6206,9 @@ } }, "node_modules/eslint/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": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", "dev": true, "license": "MIT", "dependencies": { @@ -6087,9 +6217,9 @@ } }, "node_modules/eslint/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": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -6118,9 +6248,9 @@ } }, "node_modules/esquery": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", - "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -6174,9 +6304,9 @@ } }, "node_modules/eventemitter3": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", - "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", "license": "MIT" }, "node_modules/exsolve": { @@ -6217,7 +6347,6 @@ "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==", - "dev": true, "license": "MIT" }, "node_modules/fast-glob": { @@ -6264,6 +6393,22 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/fastq": { "version": "1.20.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", @@ -6356,16 +6501,16 @@ } }, "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", "dev": true, "license": "ISC" }, "node_modules/follow-redirects": { - "version": "1.15.11", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", - "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", "funding": [ { "type": "individual", @@ -6519,33 +6664,39 @@ } }, "node_modules/gaxios": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.3.tgz", - "integrity": "sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ==", + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.4.tgz", + "integrity": "sha512-bTIgTsM2bWn3XklZISBTQX7ZSddGW+IO3bMdGaemHZ3tbqExMENHLx6kKZ/KlejgrMtj8q7wBItt51yegqalrA==", "license": "Apache-2.0", "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", - "node-fetch": "^3.3.2", - "rimraf": "^5.0.1" + "node-fetch": "^3.3.2" }, "engines": { "node": ">=18" } }, - "node_modules/gaxios/node_modules/rimraf": { - "version": "5.0.10", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", - "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", - "license": "ISC", + "node_modules/gaxios/node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/gaxios/node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", "dependencies": { - "glob": "^10.3.7" - }, - "bin": { - "rimraf": "dist/esm/bin.mjs" + "agent-base": "^7.1.2", + "debug": "4" }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "engines": { + "node": ">= 14" } }, "node_modules/gcp-metadata": { @@ -6615,9 +6766,9 @@ } }, "node_modules/get-port-please": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/get-port-please/-/get-port-please-3.1.2.tgz", - "integrity": "sha512-Gxc29eLs1fbn6LQ4jSU4vXjlwyZhF5HsGuMAa7gqBP4Rw4yxxltyDUuF5MBclFzDTXO+ACchGQoeela4DSfzdQ==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/get-port-please/-/get-port-please-3.2.0.tgz", + "integrity": "sha512-I9QVvBw5U/hw3RmWpYKRumUeaDgxTPd401x364rLmWBJcOQ753eov1eTgzDqRG9bqFIfDc7gfzcQEWrUri3o1A==", "license": "MIT" }, "node_modules/get-proto": { @@ -6652,9 +6803,9 @@ } }, "node_modules/get-tsconfig": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz", - "integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==", + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz", + "integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==", "dev": true, "license": "MIT", "dependencies": { @@ -6665,18 +6816,10 @@ } }, "node_modules/giget": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/giget/-/giget-2.0.0.tgz", - "integrity": "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/giget/-/giget-3.2.0.tgz", + "integrity": "sha512-GvHTWcykIR/fP8cj8dMpuMMkvaeJfPvYnhq0oW+chSeIr+ldX21ifU2Ms6KBoyKZQZmVaUAAhQ2EZ68KJF8a7A==", "license": "MIT", - "dependencies": { - "citty": "^0.1.6", - "consola": "^3.4.0", - "defu": "^6.1.4", - "node-fetch-native": "^1.6.6", - "nypm": "^0.6.0", - "pathe": "^2.0.3" - }, "bin": { "giget": "dist/cli.mjs" } @@ -6685,6 +6828,8 @@ "version": "10.3.10", "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, "license": "ISC", "dependencies": { "foreground-child": "^3.1.0", @@ -6771,17 +6916,16 @@ } }, "node_modules/google-auth-library": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.5.0.tgz", - "integrity": "sha512-7ABviyMOlX5hIVD60YOfHw4/CxOfBhyduaYB+wbFWCWoni4N7SLcV46hrVRktuBbZjFC9ONyqamZITN7q3n32w==", + "version": "10.6.2", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.6.2.tgz", + "integrity": "sha512-e27Z6EThmVNNvtYASwQxose/G57rkRuaRbQyxM2bvYLLX/GqWZ5chWq2EBoUchJbCc57eC9ArzO5wMsEmWftCw==", "license": "Apache-2.0", "dependencies": { "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", - "gaxios": "^7.0.0", - "gcp-metadata": "^8.0.0", - "google-logging-utils": "^1.0.0", - "gtoken": "^8.0.0", + "gaxios": "^7.1.4", + "gcp-metadata": "8.1.2", + "google-logging-utils": "1.1.3", "jws": "^4.0.0" }, "engines": { @@ -6828,18 +6972,11 @@ "dev": true, "license": "MIT" }, - "node_modules/gtoken": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-8.0.0.tgz", - "integrity": "sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw==", - "license": "MIT", - "dependencies": { - "gaxios": "^7.0.0", - "jws": "^4.0.0" - }, - "engines": { - "node": ">=18" - } + "node_modules/graphmatch": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/graphmatch/-/graphmatch-1.1.1.tgz", + "integrity": "sha512-5ykVn/EXM1hF0XCaWh05VbYvEiOL2lY1kBxZtaYsyvjp7cmWOU1XsAdfQBwClraEofXDT197lFbXOEVMHpvQOg==", + "license": "MIT" }, "node_modules/has-bigints": { "version": "1.1.0", @@ -6921,9 +7058,9 @@ } }, "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -7005,6 +7142,30 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/hast-util-to-html": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz", + "integrity": "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-whitespace": "^3.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "stringify-entities": "^4.0.0", + "zwitch": "^2.0.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/hast-util-to-jsx-runtime": { "version": "2.3.6", "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", @@ -7082,9 +7243,9 @@ } }, "node_modules/hono": { - "version": "4.10.6", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.10.6.tgz", - "integrity": "sha512-BIdolzGpDO9MQ4nu3AUuDwHZZ+KViNm+EZ75Ae55eMXMqLVhDFqEMXxtUe9Qh8hjL+pIna/frs2j6Y2yD5Ua/g==", + "version": "4.12.21", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.21.tgz", + "integrity": "sha512-uV63apnb0kyPtAUwoWgaGh9HyIFcv8lgmzPZSiTBQAFOFGIzka5EZ1dZocmGnn0XdX0+XTqJ6Tqv7selMuGLRQ==", "license": "MIT", "engines": { "node": ">=16.9.0" @@ -7117,16 +7278,16 @@ "license": "MIT" }, "node_modules/https-proxy-agent": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", "license": "MIT", "dependencies": { - "agent-base": "^7.1.2", + "agent-base": "6", "debug": "4" }, "engines": { - "node": ">= 14" + "node": ">= 6" } }, "node_modules/iconv-lite": { @@ -7369,13 +7530,13 @@ } }, "node_modules/is-core-module": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", "dev": true, "license": "MIT", "dependencies": { - "hasown": "^2.0.2" + "hasown": "^2.0.3" }, "engines": { "node": ">= 0.4" @@ -7459,6 +7620,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -7768,6 +7930,7 @@ "version": "2.3.6", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", + "dev": true, "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/cliui": "^8.0.2" @@ -7783,9 +7946,9 @@ } }, "node_modules/jiti": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", - "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", "license": "MIT", "bin": { "jiti": "lib/jiti-cli.mjs" @@ -7966,12 +8129,16 @@ } }, "node_modules/lilconfig": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", - "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, "license": "MIT", "engines": { - "node": ">=10" + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" } }, "node_modules/lines-and-columns": { @@ -7997,12 +8164,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "license": "MIT" - }, "node_modules/lodash.includes": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", @@ -8076,20 +8237,26 @@ "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, - "bin": { - "loose-envify": "cli.js" + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" } }, - "node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "license": "ISC" - }, "node_modules/lru.min": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.3.tgz", - "integrity": "sha512-Lkk/vx6ak3rYkRR0Nhu4lFUT2VDnQSxBe8Hbl7f36358p6ow8Bnvr8lrLt98H8J1aGxfhbX4Fs5tYg2+FTwr5Q==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.4.tgz", + "integrity": "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==", "license": "MIT", "engines": { "bun": ">=1.0.0", @@ -8158,9 +8325,9 @@ } }, "node_modules/mdast-util-from-markdown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz", - "integrity": "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz", + "integrity": "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==", "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", @@ -9023,6 +9190,7 @@ "version": "9.0.3", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" @@ -9045,10 +9213,11 @@ } }, "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "license": "ISC", + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", "engines": { "node": ">=16 || 14 >=14.17" } @@ -9080,9 +9249,9 @@ } }, "node_modules/mysql2/node_modules/iconv-lite": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.1.tgz", - "integrity": "sha512-2Tth85cXwGFHfvRgZWszZSvdo+0Xsqmw8k8ZwxScfcBneNUraK+dxRxRm24nszx80Y0TVio8kKLt5sLE7ZCLlw==", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" @@ -9120,9 +9289,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", "funding": [ { "type": "github", @@ -9211,9 +9380,9 @@ } }, "node_modules/next-auth": { - "version": "4.24.13", - "resolved": "https://registry.npmjs.org/next-auth/-/next-auth-4.24.13.tgz", - "integrity": "sha512-sgObCfcfL7BzIK76SS5TnQtc3yo2Oifp/yIpfv6fMfeBOiBJkDWF3A2y9+yqnmJ4JKc2C+nMjSjmgDeTwgN1rQ==", + "version": "4.24.14", + "resolved": "https://registry.npmjs.org/next-auth/-/next-auth-4.24.14.tgz", + "integrity": "sha512-YRz6xFDXKUwiXSMMChbrBEWyFktZ1qZXEgeSHQQ3nsy08B4c/xLk6REeutRsIFwkjY/1+ShHnu07DN3JeJguig==", "license": "ISC", "dependencies": { "@babel/runtime": "^7.20.13", @@ -9270,15 +9439,6 @@ "node": "^10 || ^12 || >=14" } }, - "node_modules/node-addon-api": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz", - "integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==", - "license": "MIT", - "engines": { - "node": "^18 || ^20 || >= 21" - } - }, "node_modules/node-domexception": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", @@ -9299,6 +9459,35 @@ "node": ">=10.5.0" } }, + "node_modules/node-exports-info": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.0.tgz", + "integrity": "sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "array.prototype.flatmap": "^1.3.3", + "es-errors": "^1.3.0", + "object.entries": "^1.1.9", + "semver": "^6.3.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/node-exports-info/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/node-fetch": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", @@ -9317,29 +9506,15 @@ "url": "https://opencollective.com/node-fetch" } }, - "node_modules/node-fetch-native": { - "version": "1.6.7", - "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", - "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==", - "license": "MIT" - }, - "node_modules/node-gyp-build": { - "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==", - "license": "MIT", - "bin": { - "node-gyp-build": "bin.js", - "node-gyp-build-optional": "optional.js", - "node-gyp-build-test": "build-test.js" - } - }, "node_modules/node-releases": { - "version": "2.0.27", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", - "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "version": "2.0.45", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.45.tgz", + "integrity": "sha512-iIbHXV9eBB2nB0wa7oTsrrXq+qQt+9SIlx9AX3T96YgobtEQfis5n6TJ6vV+3QP8DwdriEAcGhARaFCu37peBg==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=18" + } }, "node_modules/normalize-path": { "version": "3.0.0", @@ -9351,25 +9526,6 @@ "node": ">=0.10.0" } }, - "node_modules/nypm": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.2.tgz", - "integrity": "sha512-7eM+hpOtrKrBDCh7Ypu2lJ9Z7PNZBdi/8AT3AX8xoCj43BBVHD0hPSTEvMtkMpfs8FCqBGhxB+uToIQimA111g==", - "license": "MIT", - "dependencies": { - "citty": "^0.1.6", - "consola": "^3.4.2", - "pathe": "^2.0.3", - "pkg-types": "^2.3.0", - "tinyexec": "^1.0.1" - }, - "bin": { - "nypm": "dist/cli.mjs" - }, - "engines": { - "node": "^14.16.0 || >=16.10.0" - } - }, "node_modules/oauth": { "version": "0.9.15", "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz", @@ -9387,10 +9543,9 @@ } }, "node_modules/object-hash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", - "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", - "dev": true, + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz", + "integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==", "license": "MIT", "engines": { "node": ">= 6" @@ -9549,27 +9704,6 @@ "url": "https://github.com/sponsors/panva" } }, - "node_modules/openid-client/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/openid-client/node_modules/object-hash": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz", - "integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==", - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -9728,6 +9862,7 @@ "version": "1.11.1", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, "license": "BlueOak-1.0.0", "dependencies": { "lru-cache": "^10.2.0", @@ -9740,6 +9875,13 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -9757,20 +9899,20 @@ "license": "MIT" }, "node_modules/perfect-debounce": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", - "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-2.1.0.tgz", + "integrity": "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==", "license": "MIT" }, "node_modules/pg": { - "version": "8.16.3", - "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz", - "integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==", + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.21.0.tgz", + "integrity": "sha512-AUP1EYJuHraQGsVoCQVIcM7TEJVGtDzxWtGFZd8rds9d+CCXlU5Js1rYgfLNvxy9iJrpHjGrRjoi/3BT9fRyiA==", "license": "MIT", "dependencies": { - "pg-connection-string": "^2.9.1", - "pg-pool": "^3.10.1", - "pg-protocol": "^1.10.3", + "pg-connection-string": "^2.13.0", + "pg-pool": "^3.14.0", + "pg-protocol": "^1.14.0", "pg-types": "2.2.0", "pgpass": "1.0.5" }, @@ -9778,7 +9920,7 @@ "node": ">= 16.0.0" }, "optionalDependencies": { - "pg-cloudflare": "^1.2.7" + "pg-cloudflare": "^1.4.0" }, "peerDependencies": { "pg-native": ">=3.0.1" @@ -9790,16 +9932,16 @@ } }, "node_modules/pg-cloudflare": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.2.7.tgz", - "integrity": "sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.4.0.tgz", + "integrity": "sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A==", "license": "MIT", "optional": true }, "node_modules/pg-connection-string": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.9.1.tgz", - "integrity": "sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w==", + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.13.0.tgz", + "integrity": "sha512-EMnU9E2fSULdsbErBbMaXJvFeD9B4+nPcM3f+4lsiCR0BHLPrLVjv3DbyM2hgQQviKJaTWIRRTjKjWlHg3p2ig==", "license": "MIT" }, "node_modules/pg-int8": { @@ -9812,18 +9954,18 @@ } }, "node_modules/pg-pool": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.10.1.tgz", - "integrity": "sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg==", + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.14.0.tgz", + "integrity": "sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==", "license": "MIT", "peerDependencies": { "pg": ">=8.0" } }, "node_modules/pg-protocol": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.10.3.tgz", - "integrity": "sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ==", + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.14.0.tgz", + "integrity": "sha512-n5taZ1kO3s9ngDTVxsEznOqCyToTgz0FLuPq0B33COy5pPpuWJpY3/2oRBVETuOgzdqRXfWpM9HIhp2LBBT1BA==", "license": "MIT" }, "node_modules/pg-types": { @@ -9867,9 +10009,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true, "license": "MIT", "engines": { @@ -9900,13 +10042,13 @@ } }, "node_modules/pkg-types": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz", - "integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.1.tgz", + "integrity": "sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==", "license": "MIT", "dependencies": { - "confbox": "^0.2.2", - "exsolve": "^1.0.7", + "confbox": "^0.2.4", + "exsolve": "^1.0.8", "pathe": "^2.0.3" } }, @@ -9921,9 +10063,9 @@ } }, "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", "dev": true, "funding": [ { @@ -9941,7 +10083,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.11", + "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -9967,6 +10109,28 @@ "postcss": "^8.0.0" } }, + "node_modules/postcss-import/node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/postcss-js": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", @@ -10036,19 +10200,6 @@ } } }, - "node_modules/postcss-load-config/node_modules/lilconfig": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", - "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antonk52" - } - }, "node_modules/postcss-nested": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", @@ -10181,9 +10332,9 @@ } }, "node_modules/prettier": { - "version": "3.7.4", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.7.4.tgz", - "integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==", + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz", + "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==", "dev": true, "license": "MIT", "bin": { @@ -10203,16 +10354,16 @@ "license": "MIT" }, "node_modules/prisma": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/prisma/-/prisma-7.2.0.tgz", - "integrity": "sha512-jSdHWgWOgFF24+nRyyNRVBIgGDQEsMEF8KPHvhBBg3jWyR9fUAK0Nq9ThUmiGlNgq2FA7vSk/ZoCvefod+a8qg==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-7.8.0.tgz", + "integrity": "sha512-yfN4yrw7HV9kEJhoy1+jgah0jafEIQsf7uWouSsM8MvJtlubsk+kM7AIBWZ8+GJl74Yj3c+nbYqBkMOxtsZ3Lw==", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "@prisma/config": "7.2.0", - "@prisma/dev": "0.17.0", - "@prisma/engines": "7.2.0", - "@prisma/studio-core": "0.9.0", + "@prisma/config": "7.8.0", + "@prisma/dev": "0.24.3", + "@prisma/engines": "7.8.0", + "@prisma/studio-core": "0.27.3", "mysql2": "3.15.3", "postgres": "3.4.7" }, @@ -10282,10 +10433,13 @@ } }, "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "license": "MIT" + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } }, "node_modules/punycode": { "version": "2.3.1", @@ -10335,13 +10489,13 @@ "license": "MIT" }, "node_modules/rc9": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz", - "integrity": "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/rc9/-/rc9-3.0.1.tgz", + "integrity": "sha512-gMDyleLWVE+i6Sgtc0QbbY6pEKqYs97NGi6isHQPqYlLemPoO8dxQ3uGi0f4NiP98c+jMW6cG1Kx9dDwfvqARQ==", "license": "MIT", "dependencies": { - "defu": "^6.1.4", - "destr": "^2.0.3" + "defu": "^6.1.6", + "destr": "^2.0.5" } }, "node_modules/react": { @@ -10369,13 +10523,6 @@ "react": "^18.3.1" } }, - "node_modules/react-is": { - "version": "19.2.3", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.3.tgz", - "integrity": "sha512-qJNJfu81ByyabuG7hPFEbXqNcWSU3+eVus+KJs+0ncpGfMyYdvSmxiJxbWR65lYi1I+/0HBcliO029gc4F+PnA==", - "license": "MIT", - "peer": true - }, "node_modules/react-markdown": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz", @@ -10404,9 +10551,9 @@ } }, "node_modules/react-redux": { - "version": "9.2.0", - "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", - "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.3.0.tgz", + "integrity": "sha512-KQopgqFo/p/fgmAs5qz6p5RWaNAzq40WAu7fJIXnQpYxFPbJYtsJPWvGeF2rOBaY/kEuV77AVsX8TsQzKm+A/g==", "license": "MIT", "dependencies": { "@types/use-sync-external-store": "^0.0.6", @@ -10506,12 +10653,12 @@ } }, "node_modules/readdirp": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", - "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", + "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", "license": "MIT", "engines": { - "node": ">= 14.18.0" + "node": ">= 20.19.0" }, "funding": { "type": "individual", @@ -10519,15 +10666,15 @@ } }, "node_modules/recharts": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.6.0.tgz", - "integrity": "sha512-L5bjxvQRAe26RlToBAziKUB7whaGKEwD3znoM6fz3DrTowCIC/FnJYnuq1GEzB8Zv2kdTfaxQfi5GoH0tBinyg==", + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.8.1.tgz", + "integrity": "sha512-mwzmO1s9sFL0TduUpwndxCUNoXsBw3u3E/0+A+cLcrSfQitSG62L32N69GhqUrrT5qKcAE3pCGVINC6pqkBBQg==", "license": "MIT", "workspaces": [ "www" ], "dependencies": { - "@reduxjs/toolkit": "1.x.x || 2.x.x", + "@reduxjs/toolkit": "^1.9.0 || 2.x.x", "clsx": "^2.1.1", "decimal.js-light": "^2.5.1", "es-toolkit": "^1.39.3", @@ -10586,12 +10733,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/regexp-to-ast": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/regexp-to-ast/-/regexp-to-ast-0.5.0.tgz", - "integrity": "sha512-tlbJqcMHnPKI9zSrystikWKwHkBqu2a/Sgw01h3zFjvYrMxEDYHzzoMZnUrbIfpTFEsoRnnviOXNCzFiSc54Qw==", - "license": "MIT" - }, "node_modules/regexp.prototype.flags": { "version": "1.5.4", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", @@ -10642,6 +10783,22 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/rehype-stringify": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/rehype-stringify/-/rehype-stringify-10.0.1.tgz", + "integrity": "sha512-k9ecfXHmIPuFVI61B9DeLPN0qFHfawM6RsuX48hoqlaKSF61RskNjSm1lI8PhBEM0MRdLxVVm4WmTqJQccH9mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-to-html": "^9.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/remark-gfm": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz", @@ -10709,24 +10866,21 @@ } }, "node_modules/remeda": { - "version": "2.21.3", - "resolved": "https://registry.npmjs.org/remeda/-/remeda-2.21.3.tgz", - "integrity": "sha512-XXrZdLA10oEOQhLLzEJEiFFSKi21REGAkHdImIb4rt/XXy8ORGXh5HCcpUOsElfPNDb+X6TA/+wkh+p2KffYmg==", + "version": "2.33.4", + "resolved": "https://registry.npmjs.org/remeda/-/remeda-2.33.4.tgz", + "integrity": "sha512-ygHswjlc/opg2VrtiYvUOPLjxjtdKvjGz1/plDhkG66hjNjFr1xmfrs2ClNFo/E6TyUFiwYNh53bKV26oBoMGQ==", "license": "MIT", - "dependencies": { - "type-fest": "^4.39.1" + "funding": { + "url": "https://github.com/sponsors/remeda" } }, - "node_modules/remeda/node_modules/type-fest": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", - "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", - "license": "(MIT OR CC0-1.0)", + "node_modules/require-from-string": { + "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==", + "license": "MIT", "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=0.10.0" } }, "node_modules/reselect": { @@ -10736,13 +10890,16 @@ "license": "MIT" }, "node_modules/resolve": { - "version": "1.22.11", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", - "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "version": "2.0.0-next.7", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.7.tgz", + "integrity": "sha512-tqt+NBWwyaMgw3zDsnygx4CByWjQEJHOPMdslYhppaQSJUtL/D4JO9CcBBlhPoI8lz9oJIDXkwXfhF4aWqP8xQ==", "dev": true, "license": "MIT", "dependencies": { - "is-core-module": "^2.16.1", + "es-errors": "^1.3.0", + "is-core-module": "^2.16.2", + "node-exports-info": "^1.6.0", + "object-keys": "^1.1.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, @@ -10814,9 +10971,9 @@ } }, "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==", + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", "dev": true, "license": "MIT", "dependencies": { @@ -10828,7 +10985,7 @@ "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", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "dev": true, "license": "ISC", "dependencies": { @@ -10847,9 +11004,9 @@ } }, "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==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -10860,9 +11017,9 @@ } }, "node_modules/robust-predicates": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", - "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.3.tgz", + "integrity": "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==", "license": "Unlicense" }, "node_modules/run-parallel": { @@ -10896,15 +11053,15 @@ "license": "BSD-3-Clause" }, "node_modules/safe-array-concat": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", - "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.4.tgz", + "integrity": "sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", - "get-intrinsic": "^1.2.6", + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "get-intrinsic": "^1.3.0", "has-symbols": "^1.1.0", "isarray": "^2.0.5" }, @@ -10986,9 +11143,9 @@ } }, "node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -11093,14 +11250,14 @@ } }, "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" + "object-inspect": "^1.13.4" }, "engines": { "node": ">= 0.4" @@ -11225,9 +11382,9 @@ "license": "MIT" }, "node_modules/std-env": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.9.0.tgz", - "integrity": "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==", + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", "license": "MIT" }, "node_modules/stop-iteration-iterator": { @@ -11256,6 +11413,7 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, "license": "MIT", "dependencies": { "eastasianwidth": "^0.2.0", @@ -11274,6 +11432,7 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -11288,12 +11447,14 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, "license": "MIT" }, "node_modules/string-width/node_modules/ansi-regex": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -11303,12 +11464,13 @@ } }, "node_modules/string-width/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==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, "license": "MIT", "dependencies": { - "ansi-regex": "^6.0.1" + "ansi-regex": "^6.2.2" }, "engines": { "node": ">=12" @@ -11448,6 +11610,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -11461,6 +11624,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -11593,9 +11757,9 @@ } }, "node_modules/tailwind-merge": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.6.0.tgz", - "integrity": "sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA==", + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.6.1.tgz", + "integrity": "sha512-Oo6tHdpZsGpkKG88HJ8RR1rg/RdnEkQEfMoEk2x1XRI3F1AxeU+ijRXpiVUF4UbLfcxxRGw6TbUINKYdWVsQTQ==", "license": "MIT", "funding": { "type": "github", @@ -11698,17 +11862,14 @@ "jiti": "bin/jiti.js" } }, - "node_modules/tailwindcss/node_modules/lilconfig": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", - "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "node_modules/tailwindcss/node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", "dev": true, "license": "MIT", "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antonk52" + "node": ">= 6" } }, "node_modules/tailwindcss/node_modules/readdirp": { @@ -11724,6 +11885,28 @@ "node": ">=8.10.0" } }, + "node_modules/tailwindcss/node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -11760,24 +11943,15 @@ "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", "license": "MIT" }, - "node_modules/tinyexec": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", - "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", - "license": "MIT", - "engines": { - "node": ">=18" - } - }, "node_modules/tinyglobby": { - "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", "dev": true, "license": "MIT", "dependencies": { "fdir": "^6.5.0", - "picomatch": "^4.0.3" + "picomatch": "^4.0.4" }, "engines": { "node": ">=12.0.0" @@ -11805,9 +11979,9 @@ } }, "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", "engines": { @@ -11890,14 +12064,13 @@ "license": "0BSD" }, "node_modules/tsx": { - "version": "4.21.0", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", - "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "version": "4.22.3", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.3.tgz", + "integrity": "sha512-mdoNxBC/cSQObGGVQ5Bpn5i+yv7j68gk3Nfm3wFjcJg3Z0Mix9jzAFfP12prmm5eVGmDKtp0yyArrs0Q+8gZHg==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "~0.27.0", - "get-tsconfig": "^4.7.5" + "esbuild": "~0.28.0" }, "bin": { "tsx": "dist/cli.mjs" @@ -12017,7 +12190,7 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "devOptional": true, + "dev": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -12047,9 +12220,9 @@ } }, "node_modules/undici-types": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", - "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", "license": "MIT" }, "node_modules/unified": { @@ -12111,9 +12284,9 @@ } }, "node_modules/unist-util-visit": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz", - "integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", + "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", "license": "MIT", "dependencies": { "@types/unist": "^3.0.0", @@ -12140,38 +12313,41 @@ } }, "node_modules/unrs-resolver": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz", - "integrity": "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.12.2.tgz", + "integrity": "sha512-dmlRxBJJayXjqTwC+JtF1HhJmgf3ftQ3YejFcZrf4+KKtJv0qDsK1pjqaaVjG7wJ5NJ6UVP1OqRMQ71Z4C3rxQ==", "dev": true, "hasInstallScript": true, "license": "MIT", "dependencies": { - "napi-postinstall": "^0.3.0" + "napi-postinstall": "^0.3.4" }, "funding": { "url": "https://opencollective.com/unrs-resolver" }, "optionalDependencies": { - "@unrs/resolver-binding-android-arm-eabi": "1.11.1", - "@unrs/resolver-binding-android-arm64": "1.11.1", - "@unrs/resolver-binding-darwin-arm64": "1.11.1", - "@unrs/resolver-binding-darwin-x64": "1.11.1", - "@unrs/resolver-binding-freebsd-x64": "1.11.1", - "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1", - "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1", - "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1", - "@unrs/resolver-binding-linux-arm64-musl": "1.11.1", - "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1", - "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1", - "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1", - "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1", - "@unrs/resolver-binding-linux-x64-gnu": "1.11.1", - "@unrs/resolver-binding-linux-x64-musl": "1.11.1", - "@unrs/resolver-binding-wasm32-wasi": "1.11.1", - "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1", - "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1", - "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" + "@unrs/resolver-binding-android-arm-eabi": "1.12.2", + "@unrs/resolver-binding-android-arm64": "1.12.2", + "@unrs/resolver-binding-darwin-arm64": "1.12.2", + "@unrs/resolver-binding-darwin-x64": "1.12.2", + "@unrs/resolver-binding-freebsd-x64": "1.12.2", + "@unrs/resolver-binding-linux-arm-gnueabihf": "1.12.2", + "@unrs/resolver-binding-linux-arm-musleabihf": "1.12.2", + "@unrs/resolver-binding-linux-arm64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-arm64-musl": "1.12.2", + "@unrs/resolver-binding-linux-loong64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-loong64-musl": "1.12.2", + "@unrs/resolver-binding-linux-ppc64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-riscv64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-riscv64-musl": "1.12.2", + "@unrs/resolver-binding-linux-s390x-gnu": "1.12.2", + "@unrs/resolver-binding-linux-x64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-x64-musl": "1.12.2", + "@unrs/resolver-binding-openharmony-arm64": "1.12.2", + "@unrs/resolver-binding-wasm32-wasi": "1.12.2", + "@unrs/resolver-binding-win32-arm64-msvc": "1.12.2", + "@unrs/resolver-binding-win32-ia32-msvc": "1.12.2", + "@unrs/resolver-binding-win32-x64-msvc": "1.12.2" } }, "node_modules/update-browserslist-db": { @@ -12278,6 +12454,7 @@ "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", "license": "MIT", "bin": { "uuid": "dist/bin/uuid" @@ -12463,9 +12640,9 @@ } }, "node_modules/which-typed-array": { - "version": "1.1.19", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", - "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", + "version": "1.1.20", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz", + "integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==", "dev": true, "license": "MIT", "dependencies": { @@ -12498,6 +12675,7 @@ "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^6.1.0", @@ -12516,6 +12694,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -12533,12 +12712,14 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, "license": "MIT" }, "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==", + "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -12553,6 +12734,7 @@ "version": "6.2.2", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -12565,6 +12747,7 @@ "version": "6.2.3", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -12574,12 +12757,13 @@ } }, "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==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, "license": "MIT", "dependencies": { - "ansi-regex": "^6.0.1" + "ansi-regex": "^6.2.2" }, "engines": { "node": ">=12" @@ -12596,9 +12780,9 @@ "license": "ISC" }, "node_modules/ws": { - "version": "8.19.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", - "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "version": "8.20.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.1.tgz", + "integrity": "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==", "license": "MIT", "engines": { "node": ">=10.0.0" @@ -12645,12 +12829,13 @@ } }, "node_modules/zeptomatch": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/zeptomatch/-/zeptomatch-2.0.2.tgz", - "integrity": "sha512-H33jtSKf8Ijtb5BW6wua3G5DhnFjbFML36eFu+VdOoVY4HD9e7ggjqdM6639B+L87rjnR6Y+XeRzBXZdy52B/g==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/zeptomatch/-/zeptomatch-2.1.0.tgz", + "integrity": "sha512-KiGErG2J0G82LSpniV0CtIzjlJ10E04j02VOudJsPyPwNZgGnRKQy7I1R7GMyg/QswnE4l7ohSGrQbQbjXPPDA==", "license": "MIT", "dependencies": { - "grammex": "^3.1.10" + "grammex": "^3.1.11", + "graphmatch": "^1.1.0" } }, "node_modules/zwitch": { diff --git a/package.json b/package.json index dbf29097..becdd1ea 100644 --- a/package.json +++ b/package.json @@ -6,23 +6,25 @@ "node": "22.x" }, "scripts": { - "dev": "NODE_OPTIONS=\"--dns-result-order=ipv4first --no-network-family-autoselection\" next dev", - "build": "prisma generate && next build", - "typecheck": "prisma generate && rm -f tsconfig.tsbuildinfo && tsc -p tsconfig.json --noEmit", - "build:worker": "tsc -p tsconfig.worker.json", - "preworker": "node -e \"const fs=require('fs'); const cp=require('child_process'); if(fs.existsSync('node_modules/.bin/tsc')){cp.execSync('npm run -s build:worker',{stdio:'inherit'});} else {console.log('Skipping build:worker (tsc not installed)');}\"", - "preworker:server": "node -e \"const fs=require('fs'); const cp=require('child_process'); if(fs.existsSync('node_modules/.bin/tsc')){cp.execSync('npm run -s build:worker',{stdio:'inherit'});} else {console.log('Skipping build:worker (tsc not installed)');}\"", - "start": "NODE_OPTIONS=\"--dns-result-order=ipv4first --no-network-family-autoselection\" next start -p $PORT", - "worker": "NODE_OPTIONS=\"--dns-result-order=ipv4first --no-network-family-autoselection\" node dist-worker/scripts/analysisWorker.js", - "worker:server": "NODE_OPTIONS=\"--dns-result-order=ipv4first --no-network-family-autoselection\" node dist-worker/scripts/workerServer.js", - "worker:dev": "tsx scripts/analysisWorker.ts", - "lint": "next lint", - "format": "prettier --write \"**/*.{ts,tsx,css}\"", - "prisma:generate": "prisma generate", - "prisma:migrate": "prisma migrate dev", - "prisma:studio": "prisma studio", - "test:validation": "tsx scripts/test-validation.ts" - }, + "dev": "cross-env NODE_OPTIONS=\"--dns-result-order=ipv4first --no-network-family-autoselection\" next dev", + "build": "prisma generate && next build", + "typecheck": "prisma generate && rm -f tsconfig.tsbuildinfo && tsc -p tsconfig.json --noEmit", + "build:worker": "tsc -p tsconfig.worker.json", + "preworker": "node -e \"const fs=require('fs'); const cp=require('child_process'); if(fs.existsSync('node_modules/.bin/tsc')){cp.execSync('npm run -s build:worker',{stdio:'inherit'});} else {console.log('Skipping build:worker (tsc not installed)');}\"", + "preworker:server": "node -e \"const fs=require('fs'); const cp=require('child_process'); if(fs.existsSync('node_modules/.bin/tsc')){cp.execSync('npm run -s build:worker',{stdio:'inherit'});} else {console.log('Skipping build:worker (tsc not installed)');}\"", + "start": "cross-env NODE_OPTIONS=\"--dns-result-order=ipv4first --no-network-family-autoselection\" next start -p $PORT", + + "worker": "cross-env NODE_OPTIONS=\"--dns-result-order=ipv4first --no-network-family-autoselection\" node dist-worker/scripts/analysisWorker.js", + +"worker:server": "cross-env NODE_OPTIONS=\"--dns-result-order=ipv4first --no-network-family-autoselection\" node dist-worker/scripts/workerServer.js", + "worker:dev": "tsx scripts/analysisWorker.ts", + "lint": "next lint", + "format": "prettier --write \"**/*.{ts,tsx,css}\"", + "prisma:generate": "prisma generate", + "prisma:migrate": "prisma migrate dev", + "prisma:studio": "prisma studio", + "test:validation": "tsx scripts/test-validation.ts" +}, "dependencies": { "@auth/prisma-adapter": "^2.11.1", "@google/generative-ai": "^0.24.1", @@ -38,10 +40,9 @@ "@types/d3": "^7.4.3", "@types/pg": "^8.16.0", "axios": "^1.13.2", - "bcrypt": "^6.0.0", "bcryptjs": "^3.0.3", "class-variance-authority": "^0.7.0", - "clsx": "^2.0.0", + "clsx": "^2.1.1", "d3": "^7.9.0", "dotenv": "^16.4.5", "google-auth-library": "^10.3.0", @@ -59,7 +60,7 @@ "rehype-sanitize": "^6.0.0", "remark-gfm": "^4.0.1", "sonner": "^1.3.1", - "tailwind-merge": "^2.2.0", + "tailwind-merge": "^2.6.1", "ws": "^8.19.0" }, "devDependencies": { @@ -71,10 +72,12 @@ "@typescript-eslint/eslint-plugin": "^6.14.0", "@typescript-eslint/parser": "^6.14.0", "autoprefixer": "^10.4.16", + "cross-env": "^10.1.0", "eslint": "^8.55.0", "eslint-config-next": "^14.1.0", "postcss": "^8.4.32", "prettier": "^3.1.1", + "rehype-stringify": "^10.0.1", "tailwindcss": "^3.4.0", "tailwindcss-animate": "^1.0.7", "tsx": "^4.21.0", diff --git a/pr_info.json b/pr_info.json new file mode 100644 index 00000000..e69de29b diff --git a/prisma/migrations/20260521143425_add_webhook_indexes/migration.sql b/prisma/migrations/20260521143425_add_webhook_indexes/migration.sql new file mode 100644 index 00000000..21407fcf --- /dev/null +++ b/prisma/migrations/20260521143425_add_webhook_indexes/migration.sql @@ -0,0 +1,5 @@ +-- CreateIndex +CREATE INDEX "github_repos_repo_full_name_enabled_installation_id_idx" ON "github_repos"("repo_full_name", "enabled", "installation_id"); + +-- CreateIndex +CREATE INDEX "github_repos_updated_at_idx" ON "github_repos"("updated_at"); diff --git a/prisma/migrations/20260521161517_add_performance_indexes/migration.sql b/prisma/migrations/20260521161517_add_performance_indexes/migration.sql new file mode 100644 index 00000000..e7b6fe70 --- /dev/null +++ b/prisma/migrations/20260521161517_add_performance_indexes/migration.sql @@ -0,0 +1,5 @@ +-- CreateIndex +CREATE INDEX "file_changes_commit_id_idx" ON "file_changes"("commit_id"); + +-- CreateIndex +CREATE INDEX "repositories_user_id_idx" ON "repositories"("user_id"); diff --git a/prisma/migrations/20260521163328_optimize_worker_query_indexes/migration.sql b/prisma/migrations/20260521163328_optimize_worker_query_indexes/migration.sql new file mode 100644 index 00000000..2ec002d8 --- /dev/null +++ b/prisma/migrations/20260521163328_optimize_worker_query_indexes/migration.sql @@ -0,0 +1,2 @@ +-- CreateIndex +CREATE INDEX "analysis_jobs_status_lock_expires_at_next_run_at_idx" ON "analysis_jobs"("status", "lock_expires_at", "next_run_at"); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 8cf7a71d..72a3c91a 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -60,6 +60,7 @@ model GitHubAccount { @@map("github_accounts") } + model GitHubRepo { id Int @id @default(autoincrement()) userId Int @map("user_id") @@ -73,8 +74,10 @@ model GitHubRepo { pullRequests PullRequest[] @@unique([userId, repoFullName]) + @@index([repoFullName, enabled, installationId]) @@index([installationId]) @@index([enabled]) + @@index([updatedAt]) @@map("github_repos") } @@ -131,6 +134,8 @@ model Account { user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@unique([provider, providerAccountId]) + @@index([userId]) + @@index([userId, provider]) @@map("accounts") } @@ -141,6 +146,8 @@ model Session { expires DateTime user User @relation(fields: [userId], references: [id], onDelete: Cascade) + @@index([userId]) + @@index([expires]) @@map("sessions") } @@ -179,6 +186,7 @@ model Repository { languages Language[] analysisJobs AnalysisJob[] + @@index([userId]) @@map("repositories") } @@ -217,6 +225,7 @@ model AnalysisJob { @@index([lockExpiresAt]) @@index([repositoryId]) @@index([userId]) + @@index([status, lockExpiresAt, nextRunAt]) @@map("analysis_jobs") } @@ -295,6 +304,7 @@ model FileChange { file File? @relation(fields: [fileId], references: [id], onDelete: SetNull) createdAt DateTime @default(now()) @map("created_at") + @@index([commitId]) @@map("file_changes") } diff --git a/scripts/WORKER_REPRO.md b/scripts/WORKER_REPRO.md new file mode 100644 index 00000000..714dcb22 --- /dev/null +++ b/scripts/WORKER_REPRO.md @@ -0,0 +1,49 @@ +Manual repro for worker POST /run endpoint + +This file documents how to exercise the new validation and expected responses. + +Prerequisite: the server is running (it starts when you run the script normally): + +Windows PowerShell / cmd example: + +```bash +node dist/workerServer.js +# or when running ts-node in dev +npx ts-node scripts/workerServer.ts +``` + +1. Valid request (starts background worker loop) + +```bash +curl -X POST http://localhost:8080/run \ + -H "Content-Type: application/json" \ + -d '{"once": true, "workerId": "test-worker-1", "pollIntervalMs": 2000}' + +# Expected response: 202 with JSON {"status":"started"} +``` + +2. Invalid JSON + +```bash +curl -X POST http://localhost:8080/run \ + -H "Content-Type: application/json" \ + -d '{invalid json' + +# Expected response: 400 with JSON {"error":"Invalid JSON body"} +``` + +3. Invalid input types (validation error) + +```bash +curl -X POST http://localhost:8080/run \ + -H "Content-Type: application/json" \ + -d '{"once": "no", "pollIntervalMs": -5}' + +# Expected response: 400 with JSON {"error": "once must be a boolean; pollIntervalMs must be a positive integer"} +``` + +Notes + +- Error responses never include stack traces; they return concise JSON `error` messages. +- The endpoint expects `application/json` Content-Type. +- Use the `once` flag to run one pass (`true`) or omit to run the regular long-lived loop. diff --git a/scripts/analysisWorker.ts b/scripts/analysisWorker.ts index fb8cf067..358f41a8 100644 --- a/scripts/analysisWorker.ts +++ b/scripts/analysisWorker.ts @@ -3,6 +3,11 @@ import os from "os"; import prisma from "../lib/prisma"; import { analysisJobService } from "../lib/services/analysisJobService"; import { repositoryService } from "../lib/services/repositoryService"; +import { + isRateLimitError, + extractRetryAfter, + sanitizeErrorMessage, +} from "../lib/utils/rateLimit"; import type { AnalysisJob } from "@prisma/client"; const POLL_INTERVAL_MS = 2000; @@ -27,7 +32,7 @@ async function runJob( lockMs: number; heartbeatIntervalMs: number; } -) { +): Promise { let heartbeatTimer: NodeJS.Timeout | null = null; let lastProgressWriteAt = 0; let lastProgressPercent: number | undefined; @@ -79,7 +84,7 @@ async function runJob( workerId: params.workerId, lockMs: params.lockMs, }) - .catch((e) => console.error("heartbeat failed", e)); + .catch((e) => console.error("heartbeat failed", sanitizeErrorMessage(e))); }, params.heartbeatIntervalMs); if (job.type !== "repository_analysis") { @@ -96,28 +101,59 @@ async function runJob( jobId: job.id, workerId: params.workerId, }); + return true; } catch (err: any) { - const message = err?.message ? String(err.message) : String(err); - console.error(`Job ${job.id} failed:`, err); + const rateLimited = isRateLimitError(err); + const retryAfter = rateLimited ? extractRetryAfter(err) : null; + const safeMessage = sanitizeErrorMessage(err); + + if (rateLimited) { + console.error( + `Job ${job.id} rate limited (attempt ${job.attempts}/${job.maxAttempts})` + + (retryAfter ? `, retry after ${retryAfter}s` : "") + ); + } else { + console.error(`Job ${job.id} failed: ${safeMessage}`); + } await analysisJobService.markFailed({ jobId: job.id, workerId: params.workerId, - error: message, + error: safeMessage, attempts: job.attempts, maxAttempts: job.maxAttempts, + retryAfter: retryAfter ?? undefined, }); + + const shouldRetry = job.attempts < job.maxAttempts; + if (!shouldRetry && job.type === "repository_analysis") { + await repositoryService.markRepositoryFailed(job.repositoryId, safeMessage); + } + + return false; } finally { if (heartbeatTimer) clearInterval(heartbeatTimer); } } +export interface AnalysisWorkerSummary { + totalJobsScanned: number; + jobsProcessed: number; + jobsSkipped: number; + jobsFailed: number; + executionDurationMs: number; + success: boolean; + budgetExhausted?: boolean; + earlyStopReason?: string; +} + export async function startAnalysisWorkerLoop(opts?: { workerId?: string; pollIntervalMs?: number; heartbeatIntervalMs?: number; lockMs?: number; once?: boolean; + timeBudgetMs?: number; }) { const workerId = opts?.workerId || getWorkerId(); const pollIntervalMs = opts?.pollIntervalMs ?? POLL_INTERVAL_MS; @@ -128,6 +164,16 @@ export async function startAnalysisWorkerLoop(opts?: { console.log(`analysis worker starting: ${workerId}`); let stopping = false; + const startTimeMs = Date.now(); + let totalJobsScanned = 0; + let jobsProcessed = 0; + let jobsSkipped = 0; + let jobsFailed = 0; + + const timeBudgetMs = opts?.timeBudgetMs; + const budgetGraceMs = opts?.budgetGraceMs ?? 10_000; // 10s default buffer + let budgetExhausted = false; + let earlyStopReason: string | undefined; const shutdown = async (signal: string) => { if (stopping) return; @@ -144,31 +190,85 @@ export async function startAnalysisWorkerLoop(opts?: { process.on("SIGTERM", () => void shutdown("SIGTERM")); process.on("SIGINT", () => void shutdown("SIGINT")); + const startTime = Date.now(); + let jobsProcessed = 0; + let jobsSkipped = 0; + while (!stopping) { + if (timeBudgetMs) { + const elapsed = Date.now() - startTimeMs; + const remaining = timeBudgetMs - elapsed; + if (remaining <= budgetGraceMs) { + console.log(`Time budget nearly exhausted (${remaining}ms remaining). Stopping gracefully.`); + budgetExhausted = true; + earlyStopReason = "time_budget_exhausted"; + break; + } + } + try { + if (opts?.timeBudgetMs) { + const elapsed = Date.now() - startTime; + if (elapsed >= opts.timeBudgetMs) { + console.log(`Time budget of ${opts.timeBudgetMs}ms reached (elapsed: ${elapsed}ms). Processed ${jobsProcessed} jobs. Shutting down gracefully...`); + break; + } + } + const job = await analysisJobService.claimNextJob({ workerId, lockMs, }); if (!job) { - if (opts?.once) return; + jobsSkipped++; + if (opts?.once) { + console.log(`No jobs available. Processed ${jobsProcessed} jobs.`); + return; + } await sleep(pollIntervalMs); continue; } + totalJobsScanned++; console.log( `claimed job ${job.id} (attempt ${job.attempts}/${job.maxAttempts})` ); await runJob(job, { workerId, lockMs, heartbeatIntervalMs }); + jobsProcessed++; - if (opts?.once) return; + if (opts?.once) { + console.log(`Finished one-shot run. Processed ${jobsProcessed} jobs.`); + return; + } } catch (e) { - console.error("worker loop error:", e); - if (opts?.once) return; + console.error("worker loop error:", sanitizeErrorMessage(e)); + if (opts?.once) { + return { + totalJobsScanned, + jobsProcessed, + jobsSkipped, + jobsFailed, + executionDurationMs: Date.now() - startTimeMs, + success: false, + budgetExhausted, + earlyStopReason, + }; + } await sleep(pollIntervalMs); } } + + return { + totalJobsScanned, + jobsProcessed, + jobsSkipped, + jobsFailed, + executionDurationMs: Date.now() - startTimeMs, + success: true, + budgetExhausted, + earlyStopReason, + }; } // Run as standalone script diff --git a/scripts/test-validation.ts b/scripts/test-validation.ts index fcacb711..34931796 100644 --- a/scripts/test-validation.ts +++ b/scripts/test-validation.ts @@ -18,6 +18,10 @@ interface TestResult { const results: TestResult[] = []; +import jwt from "jsonwebtoken"; + +const JWT_SECRET = process.env.JWT_SECRET || "fallback-secret-key"; + async function test( endpoint: string, method: string, @@ -30,7 +34,10 @@ async function test( }; if (token) { - headers.Authorization = `Bearer ${token}`; + const actualToken = token === "test-token" + ? jwt.sign({ userId: 1, email: "test@example.com" }, JWT_SECRET) + : token; + headers.Authorization = `Bearer ${actualToken}`; } const response = await fetch(`${BASE_URL}${endpoint}`, { @@ -151,7 +158,61 @@ async function runTests() { "test-token", ); - // Test 11: Invalid repository ID + // Test 10a: Create repository with invalid URL protocol + await test( + "/api/repositories", + "POST", + { name: "test-repo", url: "ftp://github.com/owner/repo" }, + 400, + "test-token", + ); + + // Test 10b: Create repository with invalid name type + await test( + "/api/repositories", + "POST", + { name: 123, url: "https://github.com/owner/repo" }, + 400, + "test-token", + ); + + // Test 10c: Create repository with name too long + await test( + "/api/repositories", + "POST", + { name: "a".repeat(101), url: "https://github.com/owner/repo" }, + 400, + "test-token", + ); + + // Test 10d: Create repository with URL too long + await test( + "/api/repositories", + "POST", + { name: "test-repo", url: "https://" + "a".repeat(2005) }, + 400, + "test-token", + ); + + // Test 10e: Create repository with invalid description type + await test( + "/api/repositories", + "POST", + { name: "test-repo", url: "https://github.com/owner/repo", description: 123 }, + 400, + "test-token", + ); + + // Test 10f: Create repository with description too long + await test( + "/api/repositories", + "POST", + { name: "test-repo", url: "https://github.com/owner/repo", description: "a".repeat(1005) }, + 400, + "test-token", + ); + + // Test 11: Invalid repository ID (alphabetic) await test( "/api/repositories/abc", "GET", @@ -160,6 +221,51 @@ async function runTests() { "test-token", ); + // Test 11a: Invalid repository ID (negative) + await test( + "/api/repositories/-5", + "GET", + null, + 400, + "test-token", + ); + + // Test 11b: Invalid repository ID (float) + await test( + "/api/repositories/3.14", + "GET", + null, + 400, + "test-token", + ); + + // Test 11c: Invalid repository ID for analyze + await test( + "/api/repositories/-1/analyze", + "POST", + null, + 400, + "test-token", + ); + + // Test 11d: Invalid repository ID for readme + await test( + "/api/repositories/-1/readme", + "POST", + null, + 400, + "test-token", + ); + + // Test 11e: Invalid repository ID for stats + await test( + "/api/repositories/-1/stats", + "GET", + null, + 400, + "test-token", + ); + // Test 12: Chat without repositoryId await test( "/api/ai/chat", diff --git a/scripts/workerServer.ts b/scripts/workerServer.ts index 534ccf9d..7f3204f4 100644 --- a/scripts/workerServer.ts +++ b/scripts/workerServer.ts @@ -7,6 +7,7 @@ const port = Number(process.env.PORT || "8080"); function startHealthServer() { const server = http.createServer((req, res) => { + // health endpoints if (req.url === "/" || req.url === "/healthz" || req.url === "/readyz") { res.statusCode = 200; res.setHeader("content-type", "text/plain; charset=utf-8"); @@ -14,6 +15,63 @@ function startHealthServer() { return; } + // Control endpoint: start a worker run with validated input + if (req.method === "POST" && req.url === "/run") { + (async () => { + try { + const ct = String(req.headers["content-type"] || "").toLowerCase(); + if (!ct.startsWith("application/json")) { + res.statusCode = 400; + res.setHeader("content-type", "application/json; charset=utf-8"); + res.end(JSON.stringify({ error: "Content-Type must be application/json" })); + return; + } + + let body = ""; + for await (const chunk of req) { + body += chunk; + if (body.length > 1_000_000) break; // guard + } + + let parsed: any; + try { + parsed = body ? JSON.parse(body) : {}; + } catch (e) { + res.statusCode = 400; + res.setHeader("content-type", "application/json; charset=utf-8"); + res.end(JSON.stringify({ error: "Invalid JSON body" })); + return; + } + + const { ok, errors, opts } = validateRunOptions(parsed); + if (!ok) { + res.statusCode = 400; + res.setHeader("content-type", "application/json; charset=utf-8"); + res.end(JSON.stringify({ error: errors.join("; ") })); + return; + } + + // Start the worker loop in background. Do not expose internal errors to clients. + void startAnalysisWorkerLoop(opts).catch((e) => + console.error("/run: failed to start worker loop", e) + ); + + res.statusCode = 202; + res.setHeader("content-type", "application/json; charset=utf-8"); + res.end(JSON.stringify({ status: "started" })); + return; + } catch (e: any) { + // Unexpected server error: return generic message without stack + console.error("/run: unexpected error", e); + res.statusCode = 500; + res.setHeader("content-type", "application/json; charset=utf-8"); + res.end(JSON.stringify({ error: "internal server error" })); + return; + } + })(); + return; + } + res.statusCode = 404; res.setHeader("content-type", "text/plain; charset=utf-8"); res.end("not found"); @@ -24,6 +82,58 @@ function startHealthServer() { }); } +function validateRunOptions(input: any): { + ok: boolean; + errors: string[]; + opts?: { + workerId?: string; + pollIntervalMs?: number; + heartbeatIntervalMs?: number; + lockMs?: number; + once?: boolean; + }; +} { + const errors: string[] = []; + const opts: any = {}; + + if (input == null || typeof input !== "object") { + errors.push("body must be a JSON object"); + return { ok: false, errors }; + } + + if ("workerId" in input) { + if (typeof input.workerId !== "string" || input.workerId.trim() === "") { + errors.push("workerId must be a non-empty string"); + } else { + opts.workerId = input.workerId.trim(); + } + } + + if ("once" in input) { + if (typeof input.once !== "boolean") errors.push("once must be a boolean"); + else opts.once = input.once; + } + + const intFields: Array<"pollIntervalMs" | "heartbeatIntervalMs" | "lockMs"> = [ + "pollIntervalMs", + "heartbeatIntervalMs", + "lockMs", + ]; + + for (const f of intFields) { + if (f in input) { + const n = Number(input[f]); + if (!Number.isFinite(n) || !Number.isInteger(n) || n <= 0) { + errors.push(`${f} must be a positive integer`); + } else { + opts[f] = n; + } + } + } + + return { ok: errors.length === 0, errors, opts }; +} + async function main() { startHealthServer(); @@ -34,4 +144,4 @@ async function main() { main().catch((e) => { console.error("worker-server fatal:", e); process.exit(1); -}); +}); \ No newline at end of file diff --git a/src/components/ai/AIEmptyState.tsx b/src/components/ai/AIEmptyState.tsx new file mode 100644 index 00000000..a4d875cd --- /dev/null +++ b/src/components/ai/AIEmptyState.tsx @@ -0,0 +1,37 @@ +import { Sparkles } from "lucide-react"; + +interface AIEmptyStateProps { + onGetStarted?: () => void; + title?: string; + description?: string; +} + +export function AIEmptyState({ + onGetStarted, + title = "No AI suggestions yet", + description = "Stage your changes and let AI generate a smart commit message for you.", +}: AIEmptyStateProps) { + return ( +
+
+ +
+
+

{title}

+

+ {description} +

+
+ {onGetStarted && ( + + )} +
+ ); +} diff --git a/src/components/ai/AIRepositoryOverlay.tsx b/src/components/ai/AIRepositoryOverlay.tsx deleted file mode 100644 index a612187c..00000000 --- a/src/components/ai/AIRepositoryOverlay.tsx +++ /dev/null @@ -1,374 +0,0 @@ -import { useState, useRef, useEffect } from "react"; -import { Send, Loader2, X, Minimize2, Maximize2, Sparkles } from "lucide-react"; -import { Card } from "@/components/ui"; -import { geminiService, ChatMessage } from "@/services/gemini"; -import { useToast } from "@/hooks/use-toast"; -import { useAuth } from "@/contexts/AuthContext"; - -interface AIRepositoryOverlayProps { - repository: { - name: string; - description?: string; - languages: { name: string; percentage: number }[]; - stats?: { - commits: number; - contributors: number; - files: number; - branches?: number; - lines?: number; - stars?: number; - forks?: number; - }; - recentCommits?: any[]; - contributors?: any[]; - branches?: any[]; - }; -} - -export function AIRepositoryOverlay({ repository }: AIRepositoryOverlayProps) { - const [isOpen, setIsOpen] = useState(false); - const [isMinimized, setIsMinimized] = useState(false); - const [messages, setMessages] = useState([]); - const [input, setInput] = useState(""); - const [isLoading, setIsLoading] = useState(false); - const [streamingMessage, setStreamingMessage] = useState(""); - const [contextSent, setContextSent] = useState(false); - const messagesEndRef = useRef(null); - const { toast } = useToast(); - const { isAuthenticated, isLoading: isAuthLoading } = useAuth(); - - const scrollToBottom = () => { - messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); - }; - - useEffect(() => { - scrollToBottom(); - }, [messages, streamingMessage]); - - useEffect(() => { - if (isOpen && messages.length === 0) { - const greeting: ChatMessage = { - role: "assistant", - content: `Hi! I'm here to help you understand the **${repository.name}** repository. I can answer questions about the code, architecture, commits, contributors, and more. What would you like to know?`, - timestamp: new Date(), - }; - setMessages([greeting]); - } - }, [isOpen, repository.name]); - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - if (!input.trim() || isLoading) return; - - if (isAuthLoading || !isAuthenticated) { - toast({ - title: "Login required", - description: "Please log in to use the repository assistant.", - variant: "destructive", - }); - return; - } - - const userMessage: ChatMessage = { - role: "user", - content: input, - timestamp: new Date(), - }; - - setMessages((prev) => [...prev, userMessage]); - setInput(""); - setIsLoading(true); - setStreamingMessage(""); - - try { - // Build comprehensive repository context only for first message - const contributorsArray = Array.isArray(repository.contributors) - ? repository.contributors - : []; - const contributorsCount = - repository.stats?.contributors || contributorsArray.length || 0; - const totalLines = repository.stats?.lines || 0; - - // Debug log to check contributor data - console.log("Contributors data:", { - contributorsArray, - contributorsCount, - hasContributors: contributorsArray.length > 0, - }); - - let contextualPrompt = ""; - - // Only send full context on first message - if (!contextSent) { - const context = { - name: repository.name, - description: repository.description, - languages: repository.languages.map((l) => l.name), - stats: { - commits: repository.stats?.commits || 0, - contributors: contributorsCount, - files: repository.stats?.files || 0, - branches: repository.stats?.branches || 0, - lines: totalLines, - stars: repository.stats?.stars || 0, - forks: repository.stats?.forks || 0, - }, - recentActivity: repository.recentCommits - ?.slice(0, 5) - .map((c: any) => ({ - message: c.message, - author: c.author || c.authorName, - date: c.date || c.committedAt, - })), - topContributors: contributorsArray.slice(0, 5).map((c: any) => ({ - name: c.name, - commits: c.commits, - percentage: c.percentage, - })), - branches: repository.branches?.map((b: any) => b.name), - }; - - contextualPrompt = `You are analyzing the ${context.name} repository. - -===== REPOSITORY DATA ===== -Repository: ${context.name} -Description: ${context.description || "N/A"} -Languages: ${context.languages.join(", ")} -Total Commits: ${context.stats.commits} -Total Contributors: ${context.stats.contributors} -Total Files: ${context.stats.files} -Total Branches: ${context.stats.branches} -Lines of Code: ${context.stats.lines.toLocaleString()} -Stars: ${context.stats.stars} -Forks: ${context.stats.forks} - -${ - context.topContributors && context.topContributors.length > 0 - ? `===== TOP CONTRIBUTORS (BY COMMIT COUNT) ===== -${context.topContributors.map((c: any, i: number) => `${i + 1}. ${c.name} - ${c.commits} commits (${c.percentage?.toFixed(1)}% of total)`).join("\n")} -The contributor with the most commits is: ${context.topContributors[0].name} with ${context.topContributors[0].commits} commits.` - : "===== CONTRIBUTORS =====\nContributor information is not yet available for this repository." -} - -${ - context.recentActivity && context.recentActivity.length > 0 - ? `===== RECENT COMMITS ===== -${context.recentActivity.map((c: any, idx: number) => `${idx + 1}. "${c.message}" by ${c.author}`).join("\n")}` - : "" -} - -${context.branches && context.branches.length > 0 ? `===== BRANCHES =====\n${context.branches.join(", ")}` : ""} - -===== INSTRUCTIONS ===== -You MUST answer questions using ONLY the data provided above. Do NOT: -- Make up names, numbers, or any information not listed above -- Suggest running git commands or checking GitHub -- Say you don't have access to the repository - -When asked about contributors or who made the most commits, use the exact names and numbers from the "TOP CONTRIBUTORS" section above. - -User Question: ${input}`; - setContextSent(true); - } else { - // For follow-up questions, include recent conversation history - const recentMessages = messages.slice(-4); // Last 2 exchanges - const conversationHistory = recentMessages - .map( - (msg) => - `${msg.role === "user" ? "User" : "Assistant"}: ${msg.content}` - ) - .join("\n\n"); - - contextualPrompt = `${conversationHistory}\n\n${input}`; - } - - let fullResponse = ""; - const stream = geminiService.chatStream(contextualPrompt); - - for await (const chunk of stream) { - fullResponse += chunk; - setStreamingMessage(fullResponse); - } - - const assistantMessage: ChatMessage = { - role: "assistant", - content: fullResponse, - timestamp: new Date(), - }; - - setMessages((prev) => [...prev, assistantMessage]); - setStreamingMessage(""); - } catch (error) { - console.error("Chat error:", error); - toast({ - title: "Error", - description: - error instanceof Error ? error.message : "Failed to get AI response", - variant: "destructive", - }); - } finally { - setIsLoading(false); - } - }; - - const formatMessage = (content: string) => { - return content.split("\n").map((line, i) => { - // Bold text - line = line.replace(/\*\*(.*?)\*\*/g, "$1"); - // Inline code - line = line.replace( - /`(.*?)`/g, - '$1' - ); - // Bullet points - if (line.trim().startsWith("- ")) { - line = '' + line.substring(2); - } - return ( -
- ); - }); - }; - - if (!isOpen) { - return ( - - ); - } - - return ( -
- - {/* Header */} -
-
- -

Repository Assistant

-
-
- - -
-
- - {/* Chat Area */} - {!isMinimized && ( - <> -
- {messages.map((message, index) => ( -
- {message.role === "assistant" && ( -
- -
- )} -
-
- {formatMessage(message.content)} -
-
-
- ))} - - {streamingMessage && ( -
-
- -
-
-
- {formatMessage(streamingMessage)} -
-
-
- )} - - {isLoading && !streamingMessage && ( -
-
- -
-
-
- Thinking... -
-
-
- )} - -
-
- - {/* Input Area */} -
-
- setInput(e.target.value)} - placeholder="Ask about this repository..." - className="flex-1 bg-white/5 border border-white/10 rounded-lg px-4 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary/50 placeholder:text-muted-foreground" - disabled={isLoading} - /> - -
-
- - )} - -
- ); -} diff --git a/src/components/ai/CodeAnalysisPanel.tsx b/src/components/ai/CodeAnalysisPanel.tsx index 08a4ee94..f693653c 100644 --- a/src/components/ai/CodeAnalysisPanel.tsx +++ b/src/components/ai/CodeAnalysisPanel.tsx @@ -7,7 +7,7 @@ import { Loader2, Sparkles, } from "lucide-react"; -import { Card } from "@/components/ui"; +import { Card, EmptyState } from "@/components/ui"; import { geminiService, CodeAnalysisRequest } from "@/services/gemini"; import { useToast } from "@/hooks/use-toast"; import { useAuth } from "@/contexts/AuthContext"; @@ -300,13 +300,11 @@ export function CodeAnalysisPanel() { {results.length === 0 ? ( -
- -

No analysis yet

-

- Enter your code and click analyze to get started -

-
+ ) : (
{results.map((result, index) => { diff --git a/src/components/analysis/AnalysisDetailSkeleton.tsx b/src/components/analysis/AnalysisDetailSkeleton.tsx new file mode 100644 index 00000000..eff48a13 --- /dev/null +++ b/src/components/analysis/AnalysisDetailSkeleton.tsx @@ -0,0 +1,52 @@ +import { Skeleton } from "@/components/ui/Skeleton"; + +export function AnalysisDetailSkeleton() { + return ( +
+ {/* Header row — job title + status badge + timestamp */} +
+
+ {/* Title */} + {/* Subtitle / repo name */} +
+
+ {/* Status badge */} + {/* Timestamp */} +
+
+ + {/* Metrics row — 3 or 4 stat cards */} +
+ {Array.from({ length: 4 }).map((_, i) => ( +
+ {/* Metric label */} + {/* Metric value */} +
+ ))} +
+ + {/* Main content — two-column layout or single wide panel */} +
+ {/* Primary panel (2/3 width on large screens) */} +
+ {/* Section heading */} + + + + {/* Code/result block */} +
+ + {/* Sidebar (1/3 width on large screens) */} +
+ + {Array.from({ length: 5 }).map((_, i) => ( +
+ + +
+ ))} +
+
+
+ ); +} diff --git a/src/components/analysis/AnalysisFailureState.tsx b/src/components/analysis/AnalysisFailureState.tsx new file mode 100644 index 00000000..357715b8 --- /dev/null +++ b/src/components/analysis/AnalysisFailureState.tsx @@ -0,0 +1,14 @@ +import React from "react"; +import { XCircle } from "lucide-react"; + +export function AnalysisFailureState({ message }: { message: string }) { + return ( +
+
+ +
+

Analysis failed

+

{message}

+
+ ); +} diff --git a/src/components/docs/DocsEmptyState.tsx b/src/components/docs/DocsEmptyState.tsx new file mode 100644 index 00000000..83b15ba4 --- /dev/null +++ b/src/components/docs/DocsEmptyState.tsx @@ -0,0 +1,47 @@ +import React from 'react'; +import Link from 'next/link'; +import { FileText, LucideIcon } from 'lucide-react'; +import { Button } from '@/components/ui/Button'; + +interface DocsEmptyStateProps { + title?: string; + description?: string; + buttonText?: string; + buttonLink?: string; + icon?: LucideIcon; +} + +export function DocsEmptyState({ + title = "No documentation found", + description = "Get started by creating your first document or exploring the setup guide.", + buttonText = "Open Getting Started Guide", + buttonLink = "/docs", + icon: Icon = FileText, +}: DocsEmptyStateProps) { + return ( +
+
+
+

+ {title} +

+

+ {description} +

+ {buttonText && buttonLink && ( + + )} +
+ ); +} diff --git a/src/components/docs/index.ts b/src/components/docs/index.ts new file mode 100644 index 00000000..cc13e8a0 --- /dev/null +++ b/src/components/docs/index.ts @@ -0,0 +1 @@ +export { DocsEmptyState } from './DocsEmptyState'; diff --git a/src/components/layout/Navbar.tsx b/src/components/layout/Navbar.tsx index e601dca5..1b3e04dc 100644 --- a/src/components/layout/Navbar.tsx +++ b/src/components/layout/Navbar.tsx @@ -1,7 +1,9 @@ +"use client"; import React, { useState } from 'react' import Link from 'next/link' import { GitBranch, Menu, X } from 'lucide-react' import { Button } from '@/components/ui' +import { ThemeToggle } from './ThemeToggle' export const Navbar: React.FC = () => { const [isOpen, setIsOpen] = useState(false) @@ -42,6 +44,7 @@ export const Navbar: React.FC = () => { {/* CTA Buttons */}
+ diff --git a/src/components/layout/ThemeToggle.tsx b/src/components/layout/ThemeToggle.tsx new file mode 100644 index 00000000..d3fb1559 --- /dev/null +++ b/src/components/layout/ThemeToggle.tsx @@ -0,0 +1,24 @@ +'use client' + +import { Moon, Sun } from 'lucide-react' +import { useTheme } from '@/context/ThemeContext' + +export const ThemeToggle: React.FC = () => { + const { theme, toggleTheme } = useTheme() + + return ( + + ) +} diff --git a/src/components/repository/BranchVisualization.tsx b/src/components/repository/BranchVisualization.tsx index e0a6a790..e537cee9 100644 --- a/src/components/repository/BranchVisualization.tsx +++ b/src/components/repository/BranchVisualization.tsx @@ -1,5 +1,5 @@ import { GitBranch, Clock, User, CheckCircle, GitCommit } from "lucide-react"; -import { Card } from "@/components/ui"; +import { Card, EmptyState } from "@/components/ui"; import { useState } from "react"; interface Branch { @@ -110,6 +110,16 @@ export function BranchVisualization({ repository }: BranchVisualizationProps) { return "bg-green-500/20 text-green-500 border-green-500/30"; }; + if (branches.length === 0) { + return ( + + ); + } + return (
{/* Header */} diff --git a/src/components/repository/CodeMetrics.tsx b/src/components/repository/CodeMetrics.tsx index 5f46e02e..d5489abc 100644 --- a/src/components/repository/CodeMetrics.tsx +++ b/src/components/repository/CodeMetrics.tsx @@ -8,7 +8,7 @@ import { CheckCircle, Package, } from "lucide-react"; -import { Card } from "@/components/ui"; +import { Card, EmptyState } from "@/components/ui"; interface LanguageStat { name: string; @@ -276,6 +276,16 @@ export function CodeMetrics({ repository }: CodeMetricsProps) { } }; + if (!repository || (!repository.files?.length && !repository.languages?.length)) { + return ( + + ); + } + return (
{/* Header */} @@ -338,11 +348,11 @@ export function CodeMetrics({ repository }: CodeMetricsProps) {
) : ( - -

- No language data available -

-
+ )}
@@ -351,28 +361,36 @@ export function CodeMetrics({ repository }: CodeMetricsProps) {

File Type Distribution

-
- {fileTypeStats.map((fileType) => ( - -
-
- -
-
-

- {fileType.count} -

-

- {fileType.percentage.toFixed(2)}% -

+ {fileTypeStats.length > 0 ? ( +
+ {fileTypeStats.map((fileType) => ( + +
+
+ +
+
+

+ {fileType.count} +

+

+ {fileType.percentage.toFixed(2)}% +

+
-
-

- {fileType.type} -

- - ))} -
+

+ {fileType.type} +

+ + ))} +
+ ) : ( + + )}
{/* Quality metrics */} @@ -588,11 +606,11 @@ export function CodeMetrics({ repository }: CodeMetricsProps) {
) : ( -
-

- No test files detected in repository -

-
+ )} ); diff --git a/src/components/repository/CommitHistory.tsx b/src/components/repository/CommitHistory.tsx index 3376acd2..cf991776 100644 --- a/src/components/repository/CommitHistory.tsx +++ b/src/components/repository/CommitHistory.tsx @@ -1,6 +1,6 @@ -import { FileText, Plus, Minus, GitMerge, Tag } from "lucide-react"; - +import { FileText, Plus, Minus, GitMerge, Tag, GitCommit } from "lucide-react"; import { useState, useMemo } from "react"; +import { EmptyState } from "@/components/ui"; interface FileChange { path: string; @@ -320,7 +320,14 @@ export const CommitHistory = ({ repository }: CommitHistoryProps) => {
{/* Graph Timeline */} -
+ {commits.length === 0 ? ( + + ) : ( +
{graphNodes.map((node, index) => { const { commit, column, color, routes } = node; @@ -601,7 +608,8 @@ export const CommitHistory = ({ repository }: CommitHistoryProps) => { ); })}
-
+
+ )} ); }; diff --git a/src/components/repository/Contributors.tsx b/src/components/repository/Contributors.tsx index 42a3048a..46521bbe 100644 --- a/src/components/repository/Contributors.tsx +++ b/src/components/repository/Contributors.tsx @@ -11,7 +11,7 @@ import { Clock, Zap, } from "lucide-react"; -import { Card } from "@/components/ui"; +import { Card, EmptyState } from "@/components/ui"; import { useState, useRef, useEffect } from "react"; interface Contributor { @@ -125,7 +125,17 @@ export function Contributors({ repository }: ContributorsProps) { {/* Stats overview */} -
+ {contributors.length === 0 ? ( + window.location.reload()} + /> + ) : ( + <> +
@@ -310,6 +320,8 @@ export function Contributors({ repository }: ContributorsProps) { ); })}
+ + )} {/* Selected contributor modal */} {selectedContributor && ( diff --git a/src/components/repository/FileStructure.tsx b/src/components/repository/FileStructure.tsx index 9ca1fe37..a8212a47 100644 --- a/src/components/repository/FileStructure.tsx +++ b/src/components/repository/FileStructure.tsx @@ -17,6 +17,7 @@ import { CardTitle, CardDescription, CardContent, + EmptyState, } from "@/components/ui"; interface FileNode { @@ -209,20 +210,28 @@ export const FileStructure = ({ repository }: FileStructureProps) => { return (
- - - File Structure - - Explore the repository's file system - - *Click on a file for more info* - - -
- -
-
-
+ {!repository?.files || repository.files.length === 0 ? ( + + ) : ( + + + File Structure + + Explore the repository's file system + + *Click on a file for more info* + + +
+ +
+
+
+ )} {/* File Analytics Modal */} {selectedFile && ( diff --git a/src/components/repository/RepositoryOverview.tsx b/src/components/repository/RepositoryOverview.tsx index 1588388c..c842fec6 100644 --- a/src/components/repository/RepositoryOverview.tsx +++ b/src/components/repository/RepositoryOverview.tsx @@ -19,7 +19,7 @@ import { CardTitle, CardDescription, CardContent, - Skeleton, + EmptyState, } from "@/components/ui"; import ReactMarkdown from "react-markdown"; import remarkGfm from "remark-gfm"; @@ -141,9 +141,6 @@ export const RepositoryOverview = ({ })); const hasUsableReadme = Boolean(readmeText && readmeText !== "doesnt exist"); - const isAnalyzing = - repositoryData?.status === "pending" || - repositoryData?.status === "analyzing"; const githubRawBase = (() => { const url = String(repositoryData?.url || ""); @@ -191,14 +188,23 @@ export const RepositoryOverview = ({ }; const readmeSanitizeSchema = (() => { - // Allow common README HTML (like ) while keeping things safe. + // Refined schema for sanitizing README markdown + // Allows common README HTML (like alignment/size attributes for images, collapsible details, etc.) + // while preventing XSS, protocol-based exploits, and DOM clobbering. const schema: any = { ...(defaultSchema as any), - tagNames: Array.from( - new Set([...(defaultSchema as any).tagNames, "img"]), - ), + + // Note: "img" is already in defaultSchema.tagNames, so we don't need to add it manually. + + // Explicitly restrict protocols for attributes to mitigate protocol-based XSS (e.g., javascript:) + protocols: { + ...((defaultSchema as any).protocols || {}), + href: ["http", "https", "mailto"], // Only allow safe URI schemes for links + }, + attributes: { ...((defaultSchema as any).attributes || {}), + // Allow target and rel attributes for links (essential for external links) a: Array.from( new Set([ ...(((defaultSchema as any).attributes?.a as any[]) || []), @@ -206,6 +212,7 @@ export const RepositoryOverview = ({ "rel", ]), ), + // Allow standard image attributes used in READMEs (alignment, sizing, and lazy loading) img: ["src", "alt", "title", "width", "height", "align", "loading"], }, }; @@ -213,6 +220,7 @@ export const RepositoryOverview = ({ return schema; })(); + const formatTimeAgo = (timestamp: string) => { const date = new Date(timestamp); const now = new Date(); @@ -363,24 +371,32 @@ export const RepositoryOverview = ({ - {languages.map((lang: any) => ( -
-
- - {lang.name} - - - {lang.percentage}% - -
-
-
+ {languages.length > 0 ? ( + languages.map((lang: any) => ( +
+
+ + {lang.name} + + + {lang.percentage}% + +
+
+
+
-
- ))} + )) + ) : ( + + )} @@ -395,26 +411,34 @@ export const RepositoryOverview = ({ -
- {recentActivity.map((activity: any, index: number) => ( -
-
- + {recentActivity.length > 0 ? ( +
+ {recentActivity.map((activity: any, index: number) => ( +
+
+ +
+
+

+ {activity.user}{" "} + + {activity.message} + +

+

+ {activity.time} +

+
-
-

- {activity.user}{" "} - - {activity.message} - -

-

- {activity.time} -

-
-
- ))} -
+ ))} +
+ ) : ( + + )}
@@ -431,22 +455,7 @@ export const RepositoryOverview = ({ - {isAnalyzing && !hasUsableReadme ? ( -
- -
- - - -
-
- - - - -
-
- ) : hasUsableReadme ? ( + {hasUsableReadme ? (
-
-
-
-

- Code Dependencies -

-
- -
-
-

- 💡 Drag nodes to reposition • Scroll to zoom • Hover for details -

+ {graphData.nodes.length === 0 ? ( + + ) : ( + <> +
+

+ Code Dependencies +

+
+ +
+
+

+ 💡 Drag nodes to reposition • Scroll to zoom • Hover for details +

+ + )}
{ // Count commits by date commits?.forEach((commit: any) => { const date = new Date(commit.committedAt || commit.createdAt); - const dateStr = date.toISOString().split("T")[0]; - commitsByDate.set(dateStr, (commitsByDate.get(dateStr) || 0) + 1); + if (!isNaN(date.getTime())) { + const dateStr = date.toISOString().split("T")[0]; + commitsByDate.set(dateStr, (commitsByDate.get(dateStr) || 0) + 1); + } }); // Fill in the last 52 weeks @@ -54,6 +57,12 @@ export function CommitActivityHeatmap({ date: string; count: number; } | null>(null); + const hasCommits = Boolean(repository?.commits && repository.commits.length > 0); + + // ========================================================= + // CRITICAL FIX: LATE EXTENDED GUARD FOR PERFORMANCE & SAFETY + // ========================================================= + const hasNoCommits = !repository?.commits || repository.commits.length === 0; useEffect(() => { // Advance the window automatically as time passes (refresh at next local midnight). @@ -75,8 +84,15 @@ export function CommitActivityHeatmap({ useEffect(() => { if (!svgRef.current) return; + if (!hasCommits) return; + if (!svgRef.current || hasNoCommits) return; + if (!svgRef.current || !repository?.commits || repository.commits.length === 0) return; - const data = generateCommitData(repository?.commits || [], now); + if (!repository?.commits || repository.commits.length === 0) { + return; + } + + const data = generateCommitData(repository.commits, now); const svg = d3.select(svgRef.current); svg.selectAll("*").remove(); @@ -136,23 +152,11 @@ export function CommitActivityHeatmap({ // Month labels - derived from the rolling 52-week window const months = [ - "Jan", - "Feb", - "Mar", - "Apr", - "May", - "Jun", - "Jul", - "Aug", - "Sep", - "Oct", - "Nov", - "Dec", + "Jan", "Feb", "Mar", "Apr", "May", "Jun", + "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", ]; - // Collect month label positions (keep unique month+year, so labels slide forward over time) - const monthPositions: Array<{ month: string; x: number; fullDate: Date }> = - []; + const monthPositions: Array<{ month: string; x: number; fullDate: Date }> = []; const seenMonthKeys = new Set(); weeks.forEach((weekNum, weekIndex) => { @@ -176,10 +180,8 @@ export function CommitActivityHeatmap({ } }); - // Sort by x-position (left to right) monthPositions.sort((a, b) => a.x - b.x); - // Draw month labels monthPositions.forEach(({ month, x }) => { g.append("text") .attr("class", "month-label") @@ -266,7 +268,6 @@ export function CommitActivityHeatmap({ } }); - // Animate cells on load cell .attr("opacity", 0) .transition() @@ -293,7 +294,6 @@ export function CommitActivityHeatmap({ .ticks(5) .tickFormat((d) => `${d}`); - // Gradient for legend const defs = svg.append("defs"); const gradient = defs .append("linearGradient") @@ -342,9 +342,8 @@ export function CommitActivityHeatmap({ .attr("fill", "currentColor") .attr("font-size", "10px") .text("More"); - }, [repository, now]); + }, [repository, now, hasCommits]); - // Get commits for selected date const getCommitsForDate = (date: string) => { return ( repository?.commits?.filter((commit: any) => { @@ -360,6 +359,27 @@ export function CommitActivityHeatmap({ ? getCommitsForDate(selectedDate.date) : []; + if (!hasCommits) { + if (hasNoCommits) { + return ( + +
+

Commit Activity

+

+ Contribution activity over the last 52 weeks +

+
+
+ +
+
+ ); + } + return (
@@ -368,6 +388,7 @@ export function CommitActivityHeatmap({ Contribution activity over the last 52 weeks

+
0 || commit.deletions > 0 ? (
{commit.additions > 0 && ( - - +{commit.additions} - + +{commit.additions} )} {commit.deletions > 0 && ( - - -{commit.deletions} - + -{commit.deletions} )}
) : null} @@ -452,25 +469,21 @@ export function CommitActivityHeatmap({
)} -
- +
); } diff --git a/src/components/visualizations/LanguageDistributionChart.tsx b/src/components/visualizations/LanguageDistributionChart.tsx index a9ba74cb..d40ee61c 100644 --- a/src/components/visualizations/LanguageDistributionChart.tsx +++ b/src/components/visualizations/LanguageDistributionChart.tsx @@ -1,6 +1,7 @@ import { useEffect, useRef } from "react"; import * as d3 from "d3"; -import { Card } from "@/components/ui"; +import { Card, EmptyState } from "@/components/ui"; +import { BarChart3 } from "lucide-react"; interface LanguageData { name: string; @@ -214,46 +215,50 @@ export function LanguageDistributionChart({ Interactive breakdown of codebase languages

-
- -
-
- {languageData.length > 0 ? ( - languageData.map((lang) => ( -
-
-
- {lang.name} -
-
- - {lang.lines.toLocaleString()} lines - - - {lang.percentage.toFixed(2)}% - + ) : ( + <> +
+ +
+
+ {languageData.map((lang) => ( +
+
+
+ {lang.name} +
+
+ + {lang.lines.toLocaleString()} lines + + + {lang.percentage.toFixed(2)}% + +
-
- )) - ) : ( -

- No language data available -

- )} -
+ ))} +
+ + )}
(""); const [historyLoading, setHistoryLoading] = useState(false); + const [historyLoaded, setHistoryLoaded] = useState(false); const [historyError, setHistoryError] = useState(null); const [historyRepos, setHistoryRepos] = useState([]); @@ -186,6 +188,7 @@ export default function Contribute() { const repos = Array.isArray(res.data?.repos) ? res.data.repos : []; setHistoryRepos(repos as ReviewHistoryRepo[]); + setHistoryLoaded(true); } catch (e: any) { const message = e?.response?.data?.error || @@ -445,13 +448,15 @@ export default function Contribute() { setHistoryRepoFullName(e.target.value)} + onChange={(e) => { + setHistoryRepoFullName(e.target.value); + setHistoryRepos([]); + setHistoryLoaded(false); + setHistoryError(null); + }} > {connectedRepos.filter((r) => r.enabled).length === 0 && ( @@ -559,13 +569,31 @@ export default function Contribute() {
- {historyError && ( + {historyLoading ? ( +
+ +
+ {Array.from({ length: 3 }).map((_, i) => ( +
+
+ + +
+
+ +
+
+ ))} +
+
+ ) : historyError ? (
{historyError}
- )} - - {historyRepos.length > 0 && ( + ) : historyRepos.length > 0 ? (
{historyRepos.map((repo) => (
@@ -627,6 +655,15 @@ export default function Contribute() {
))}
+ ) : ( + historyLoaded && ( +
+

No stored webhook reviews found for this repository.

+

+ Reviews will appear here automatically when PR webhooks are triggered. +

+
+ ) )} diff --git a/src/pages/Dashboard.tsx b/src/pages/Dashboard.tsx index 3f162a79..9c32429a 100644 --- a/src/pages/Dashboard.tsx +++ b/src/pages/Dashboard.tsx @@ -2,8 +2,9 @@ export const dynamic = "force-dynamic"; -import { useState, useEffect } from "react"; +import { useState, useEffect, useRef } from "react"; import { useRouter } from "next/navigation"; +import { ShortcutHint } from "@/components/ui/ShortcutHint"; import { GitBranch, TrendingUp, @@ -51,10 +52,44 @@ export default function Dashboard() { const [repositories, setRepositories] = useState([]); const [loading, setLoading] = useState(true); const [analyzing, setAnalyzing] = useState(false); + const searchRef = useRef(null); useEffect(() => { - fetchRepositories(); - }, []); + fetchRepositories(); + + const handleKeyDown = (e: KeyboardEvent) => { + const active = document.activeElement; + + const isTyping = + active instanceof HTMLInputElement || + active instanceof HTMLTextAreaElement || + active instanceof HTMLSelectElement || + (active instanceof HTMLElement && active.isContentEditable); + + if (e.key === "/" && !isTyping) { + e.preventDefault(); + searchRef.current?.focus(); + } + + if ( + e.key === "/" && + !e.ctrlKey && + !e.metaKey && + !e.altKey && + !e.shiftKey && + !isTyping +) { + setRepoUrl(""); + searchRef.current?.blur(); + } + }; + + window.addEventListener("keydown", handleKeyDown); + + return () => { + window.removeEventListener("keydown", handleKeyDown); + }; +}, []); const fetchRepositories = async () => { try { @@ -213,6 +248,7 @@ export default function Dashboard() { setRepoUrl(e.target.value)} className="flex-1 bg-background/50" @@ -227,6 +263,8 @@ export default function Dashboard() { {analyzing ? "Analyzing..." : "Analyze Repository"}
+ + @@ -440,4 +478,4 @@ export default function Dashboard() {
); -} +} \ No newline at end of file diff --git a/src/pages/LandingPage.tsx b/src/pages/LandingPage.tsx index 1c36e24a..18ea1f6a 100644 --- a/src/pages/LandingPage.tsx +++ b/src/pages/LandingPage.tsx @@ -1,5 +1,6 @@ -import React, { useEffect, useMemo, useState } from "react"; +import React, { useEffect, useMemo, useRef, useState } from "react"; import { useRouter } from "next/navigation"; +import { ShortcutHint } from "@/components/ui/ShortcutHint"; import { GitBranch, Network, @@ -29,6 +30,7 @@ export default function LandingPage() { const [repoUrl, setRepoUrl] = useState(""); const [isLoading, setIsLoading] = useState(false); const [scoreAnimate, setScoreAnimate] = useState(false); + const searchRef = useRef(null); const mentorMessages = useMemo( () => [ @@ -47,6 +49,33 @@ export default function LandingPage() { [], ); + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + const active = document.activeElement; + + const isTyping = + active instanceof HTMLInputElement || + active instanceof HTMLTextAreaElement || + active instanceof HTMLSelectElement || + (active instanceof HTMLElement && active.isContentEditable); + + if (e.key === "/" && !e.ctrlKey && !e.metaKey && !e.altKey && !e.shiftKey && !isTyping) { + e.preventDefault(); + searchRef.current?.focus(); + } + + if(e.key === "Escape" && (document.activeElement === searchRef.current || !isTyping)) { + setRepoUrl(""); + searchRef.current?.blur(); + } + }; + + window.addEventListener("keydown", handleKeyDown); + + return () => { + window.removeEventListener("keydown", handleKeyDown); + }; +}, []); useEffect(() => { const media = window.matchMedia?.("(prefers-reduced-motion: reduce)"); if (media?.matches) { @@ -374,6 +403,7 @@ export default function LandingPage() { >
Demo UI only — real analysis happens after install/sign up.

+ {/* Demos */} diff --git a/src/pages/Login.tsx b/src/pages/Login.tsx index f1e1dd33..0d321a7b 100644 --- a/src/pages/Login.tsx +++ b/src/pages/Login.tsx @@ -13,6 +13,7 @@ import { CardHeader, CardContent, toast, + Skeleton, } from "@/components/ui"; import { useAuth } from "@/contexts/AuthContext"; import { signIn } from "next-auth/react"; @@ -25,6 +26,7 @@ export default function Login() { const [password, setPassword] = useState(""); const [isLoading, setIsLoading] = useState(false); const [isGoogleLoading, setIsGoogleLoading] = useState(false); + const [isPageLoading, setIsPageLoading] = useState(true); const RepoGraph = ({ className, @@ -104,6 +106,11 @@ export default function Login() { const from = searchParams?.get("from") || "/dashboard"; + useEffect(() => { + const timer = setTimeout(() => setIsPageLoading(false), 400); + return () => clearTimeout(timer); + }, []); + useEffect(() => { const error = searchParams?.get("error"); if (!error) return; @@ -235,150 +242,189 @@ export default function Login() { {/* Login Card */} - - -
- -
- - GitVerse - - -

Welcome Back

-

Sign in to your account

-
- - -
-
- -
- - setEmail(e.target.value)} - className="pl-10" - required - /> + {isPageLoading ? ( + +
+
+
+ +
- -
- -
- - setPassword(e.target.value)} - className="pl-10" - required - /> +
+
+ + +
+
+ + +
+
+ + +
+ +
+ +
+ +
+
+ +
+
- -
- + + ) : ( + <> + - Forgot password? +
+ +
+ + GitVerse + -
+

Welcome Back

+

Sign in to your account

+ - - + +
+
+ +
+ + setEmail(e.target.value)} + className="pl-10" + required + /> +
+
-
-
-
-
-
-
- - Or continue with - +
+ +
+ + setPassword(e.target.value)} + className="pl-10" + required + /> +
+
+ +
+ + + Forgot password? + +
+ + + + +
+
+
+
+
+
+ + Or continue with + +
+
-
-
- + -
- Don't have an account?{" "} - - Sign up - -
- +
+ Don't have an account?{" "} + + Sign up + +
+ + + )}
); diff --git a/src/pages/RepositoryAnalysis.tsx b/src/pages/RepositoryAnalysis.tsx index a095c6f6..8b0b3b25 100644 --- a/src/pages/RepositoryAnalysis.tsx +++ b/src/pages/RepositoryAnalysis.tsx @@ -2,7 +2,7 @@ export const dynamic = "force-dynamic"; -import { useState, useEffect } from "react"; +import { useState, useEffect, useRef } from "react"; import { useParams } from "next/navigation"; import { DashboardLayout } from "@/components/layout/DashboardLayout"; import { RepositoryOverview } from "@/components/repository/RepositoryOverview"; @@ -21,12 +21,24 @@ import { ArrowLeft, Trash2, Activity, + AlertCircle, + Clock, + RefreshCw, } from "lucide-react"; import Link from "next/link"; import { useRouter } from "next/navigation"; import axios from "axios"; import { useToast } from "@/hooks/use-toast"; +import { EmptyState } from "@/components/ui"; import { buildApiUrl } from "@/services/apiConfig"; +import { RepositoryAnalysisSkeleton } from "@/components/ui/RepositoryAnalysisSkeleton"; + +// How long before we stop polling and show a "stuck" error (8 minutes) +const ANALYSIS_TIMEOUT_MS = 8 * 60 * 1000; +// Start polling every 2s, back off to 5s max +const POLL_INTERVAL_INITIAL_MS = 2000; +const POLL_INTERVAL_MAX_MS = 5000; +const POLL_INTERVAL_STEP_MS = 500; type TabType = | "overview" @@ -46,21 +58,9 @@ const tabs: Tab[] = [ { id: "overview", label: "Overview", icon: }, { id: "files", label: "Files", icon: }, { id: "commits", label: "Commits", icon: }, - { - id: "contributors", - label: "Contributors", - icon: , - }, - { - id: "mentor", - label: "AI Mentor", - icon: , - }, - { - id: "insights", - label: "Insights", - icon: , - }, + { id: "contributors", label: "Contributors", icon: }, + { id: "mentor", label: "AI Mentor", icon: }, + { id: "insights", label: "Insights", icon: }, ]; export default function RepositoryAnalysis() { @@ -68,6 +68,7 @@ export default function RepositoryAnalysis() { const id = params?.id as string; const router = useRouter(); const { toast } = useToast(); + const [activeTab, setActiveTab] = useState("overview"); const [repository, setRepository] = useState(null); const [loading, setLoading] = useState(true); @@ -76,37 +77,100 @@ export default function RepositoryAnalysis() { const [showDeleteDialog, setShowDeleteDialog] = useState(false); const [isDeleting, setIsDeleting] = useState(false); + // Timeout / stuck state + const [analysisTimedOut, setAnalysisTimedOut] = useState(false); + const [analysisError, setAnalysisError] = useState(null); + const [elapsedSeconds, setElapsedSeconds] = useState(0); + + const pollingStartedAt = useRef(null); + // Tracks last time progress changed — prevents falsely timing out active jobs + const lastProgressAt = useRef(null); + const elapsedTimer = useRef(null); + + // ── Elapsed seconds ticker ──────────────────────────────────────── + // Tracks last time progress changed prevents falsely timing out active jobs + const lastProgressAt = useRef(null); + const elapsedTimer = useRef(null); + + // Elapsed seconds ticker + useEffect(() => { + if (isAnalyzing && !analysisTimedOut) { + elapsedTimer.current = setInterval(() => { + if (pollingStartedAt.current) { + setElapsedSeconds( + Math.floor((Date.now() - pollingStartedAt.current) / 1000) + ); + } + }, 1000); + } else { + if (elapsedTimer.current) clearInterval(elapsedTimer.current); + } + return () => { + if (elapsedTimer.current) clearInterval(elapsedTimer.current); + }; + }, [isAnalyzing, analysisTimedOut]); + + // ── Initial fetch ───────────────────────────────────────────────── + // Initial fetch useEffect(() => { fetchRepository(); }, [id]); + // ── Job polling ─────────────────────────────────────────────────── + // Job polling useEffect(() => { - // Poll job status (lightweight) while analyzing. const repoStatus = repository?.status as string | undefined; const jobStatus = job?.status as string | undefined; - const shouldShowAnalyzing = + const shouldAnalyze = repoStatus === "pending" || repoStatus === "analyzing" || jobStatus === "QUEUED" || jobStatus === "PROCESSING"; - setIsAnalyzing(Boolean(shouldShowAnalyzing)); + setIsAnalyzing(Boolean(shouldAnalyze)); const jobId = job?.id || repository?.latestJob?.id; if (!jobId) return; - if (jobStatus === "DONE" || jobStatus === "FAILED") return; + if (analysisTimedOut) return; + + // Record when we started polling (only once per analysis) + if (!pollingStartedAt.current) { + pollingStartedAt.current = Date.now(); + } + if (!lastProgressAt.current) { + lastProgressAt.current = Date.now(); + } let stopped = false; - let intervalMs = 2000; + let intervalMs = POLL_INTERVAL_INITIAL_MS; const poll = async () => { if (stopped) return; + + // ── Timeout guard ────────────────────────────────────────── + // Timeout guard + if ( + lastProgressAt.current && + Date.now() - lastProgressAt.current > ANALYSIS_TIMEOUT_MS + ) { + stopped = true; + setAnalysisTimedOut(true); + setIsAnalyzing(false); + setAnalysisError( + "Analysis has been queued for over 8 minutes without progress. " + + "The background worker may not be running. Please try again later " + + "or contact the maintainer." + ); + return; + } + await fetchJob(jobId); if (stopped) return; + setTimeout(poll, intervalMs); - intervalMs = Math.min(5000, intervalMs + 500); + intervalMs = Math.min(POLL_INTERVAL_MAX_MS, intervalMs + POLL_INTERVAL_STEP_MS); }; poll(); @@ -114,11 +178,13 @@ export default function RepositoryAnalysis() { return () => { stopped = true; }; - }, [repository?.status, repository?.latestJob?.id, job?.id, job?.status]); + // analysisTimedOut included so Check Again restarts polling + }, [repository?.status, repository?.latestJob?.id, job?.id, job?.status, analysisTimedOut]); + // ── Data fetchers ───────────────────────────────────────────────── + // Data fetchers const fetchRepository = async () => { if (!id) return; - try { const token = localStorage.getItem("gitverse_token"); const response = await axios.get(buildApiUrl(`/api/repositories/${id}`), { @@ -126,11 +192,9 @@ export default function RepositoryAnalysis() { }); const repo = response.data.repository || response.data; setRepository(repo); - if (response.data.latestJob) { setJob(response.data.latestJob); } - console.log("Repository data:", response.data); } catch (error) { console.error("Error fetching repository:", error); } finally { @@ -140,25 +204,37 @@ export default function RepositoryAnalysis() { const fetchJob = async (jobId: string) => { if (!jobId) return; - try { const token = localStorage.getItem("gitverse_token"); const response = await axios.get( buildApiUrl(`/api/analysis-jobs/${jobId}`), - { - headers: { Authorization: `Bearer ${token}` }, - }, + { headers: { Authorization: `Bearer ${token}` } } ); const nextJob = response.data.job || response.data; - setJob(nextJob); + // Use functional setJob so we always compare against the latest job + // state, avoiding the stale-closure bug where the polling loop holds + // an old snapshot of job and never sees progress-only updates. + setJob((prevJob: any) => { + const prevPercent = prevJob?.progressPercent ?? null; + const prevMessage = prevJob?.progressMessage ?? null; + const nextPercent = nextJob?.progressPercent ?? null; + const nextMessage = nextJob?.progressMessage ?? null; + if (nextPercent !== prevPercent || nextMessage !== prevMessage) { + lastProgressAt.current = Date.now(); + } + return nextJob; + }); if (nextJob?.status === "DONE") { - // Job finished — refresh repository once to load results. + pollingStartedAt.current = null; await fetchRepository(); } if (nextJob?.status === "FAILED") { + pollingStartedAt.current = null; + setIsAnalyzing(false); + setAnalysisError(nextJob?.error || "The repository analysis failed."); toast({ title: "Analysis failed", description: nextJob?.error || "The repository analysis failed.", @@ -170,28 +246,26 @@ export default function RepositoryAnalysis() { } }; + // ---------------- Delete ---------------- + // Delete const handleDeleteRepository = async () => { if (!id) return; setIsDeleting(true); - try { const token = localStorage.getItem("gitverse_token"); await axios.delete(buildApiUrl(`/api/repositories/${id}`), { headers: { Authorization: `Bearer ${token}` }, }); - toast({ title: "Repository deleted", description: "The repository has been successfully deleted.", }); - router.push("/dashboard"); } catch (error: any) { console.error("Error deleting repository:", error); toast({ title: "Error", - description: - error.response?.data?.error || "Failed to delete repository", + description: error.response?.data?.error || "Failed to delete repository", variant: "destructive", }); } finally { @@ -200,48 +274,48 @@ export default function RepositoryAnalysis() { } }; + // ── Tab content ─────────────────────────────────────────────────── + // Tab content const renderContent = () => { switch (activeTab) { - case "overview": - return ; - case "files": - return ; - case "commits": - return ; - case "contributors": - return ; - case "mentor": - return ; - case "insights": - return ; - default: - return ; + case "overview": return ; + case "files": return ; + case "commits": return ; + case "contributors": return ; + case "mentor": return ; + case "insights": return ; + default: return ; } }; + // ── Helpers ─────────────────────────────────────────────────────── + // Helpers + const formatElapsed = (secs: number) => { + if (secs < 60) return `${secs}s`; + return `${Math.floor(secs / 60)}m ${secs % 60}s`; + }; + + const progressPercent = job?.progressPercent ?? 0; + const progressMessage = job?.progressMessage || "Queued"; + + // ── Render ──────────────────────────────────────────────────────── + // Render return (
{loading ? ( -
-

Loading repository...

-
+ ) : !job ? ( -
- -
-

No analysis jobs found

-

- Run your first analysis to get started -

-
- -
+ router.push("/dashboard")} + /> ) : ( <> - {/* Header with back button */} + {/* Header */}

- Status:{" "} - {repository.status} + Status: {repository.status}

- {isAnalyzing && ( + {isAnalyzing && !analysisTimedOut && ( - + â—‰ + Analyzing... )}
- {/* Delete button */}
- {isAnalyzing ? ( -
+ {/* ── Analyzing spinner (with timeout awareness) ── */} + {/* Analyzing spinner (with timeout awareness) */} + {isAnalyzing && !analysisTimedOut ? ( +
-
+
-

- Analyzing Repository -

+

Analyzing Repository

- We're analyzing the repository structure, commits, - contributors, and more. + We're analyzing structure, commits, contributors, and more.

-

- {job?.progressPercent != null || job?.progressMessage - ? `${job?.progressPercent ?? 0}% — ${job?.progressMessage || "Working"}` - : "This may take a few moments depending on the repository size..."} + + {/* Progress bar */} +

+
+ {progressMessage} + {progressPercent}% +
+
+
+
+
+ + {/* Elapsed time */} +

+ + {elapsedSeconds > 0 + ? `Running for ${formatElapsed(elapsedSeconds)}` + : "Starting up..."}

+ + {/* Warn if queued too long (>60s with no progress) */} + {elapsedSeconds > 60 && progressPercent === 0 && ( +
+

+ + Still queued after {formatElapsed(elapsedSeconds)}. + The worker runs every 5 minutes via GitHub Actions — + The worker runs every 5 minutes via GitHub Actions - + it should pick this up shortly. +

+
+ )}
+
@@ -309,9 +412,53 @@ export default function RepositoryAnalysis() {
+ + ) : analysisTimedOut || analysisError ? ( + /* ── Timeout / error state ── */ + /* Timeout / error state */ +
+
+
+ +
+
+
+

+ {analysisTimedOut ? "Analysis Timed Out" : "Analysis Failed"} +

+

+ {analysisError} +

+
+
+ + +
+
+ ) : ( + /* ── Done — show tabs ── */ + /* Done - show tabs */ <> - {/* Tab navigation */}
{tabs.map((tab) => ( @@ -333,8 +480,6 @@ export default function RepositoryAnalysis() { ))}
- - {/* Content */}
{renderContent()}
)} @@ -356,19 +501,15 @@ export default function RepositoryAnalysis() {
-

- Delete Repository -

+

Delete Repository

Are you sure you want to delete{" "} {repository?.name}? This action cannot be undone and will permanently remove all - repository data, including commits, contributors, and - analysis results. + repository data, including commits, contributors, and analysis results.

-
diff --git a/src/pages/SearchPage.tsx b/src/pages/SearchPage.tsx index a7cd9d0a..2efeb485 100644 --- a/src/pages/SearchPage.tsx +++ b/src/pages/SearchPage.tsx @@ -150,9 +150,14 @@ export default function SearchPage() { {/* Repository Grid/List */} {loading ? ( -
- Loading repositories... -
+
+ + Loading repositories... +
) : sortedRepositories.length === 0 ? ( searchQuery ? ( { + const timer = setTimeout(() => setIsPageLoading(false), 400); + return () => clearTimeout(timer); + }, []); + const RepoGraph = ({ className, style, @@ -113,6 +119,7 @@ export default function Signup() { const [agreedToTerms, setAgreedToTerms] = useState(false); const [isLoading, setIsLoading] = useState(false); const [isGoogleLoading, setIsGoogleLoading] = useState(false); + const [isPageLoading, setIsPageLoading] = useState(true); const handleGoogleSignIn = async () => { setIsGoogleLoading(true); @@ -231,230 +238,278 @@ export default function Signup() { {/* Signup Card */} - - -
- -
- - GitVerse - - -

- Create Account -

-

- Start exploring repositories today -

-
- - -
-
- -
- - setName(e.target.value)} - className="pl-10" - required - /> + {isPageLoading ? ( + +
+
+
+ +
- -
- -
- - setEmail(e.target.value)} - className="pl-10" - required - /> +
+
+ +
-
- -
- -
- - setPassword(e.target.value)} - className="pl-10" - required - /> +
+ +
-

- Must be at least 8 characters -

-
- -
- -
- - setConfirmPassword(e.target.value)} - className="pl-10" - required - /> +
+ + + +
+
+ + +
+
+ + +
+ +
+ +
+ +
+
+ +
+
- - {repoUrl && ( -
+ ) : ( + <> + + -
- -
-

- Repository Ready -

-

- {repoUrl} -

+
+ +
+ + GitVerse + + +

+ Create Account +

+

+ Start exploring repositories today +

+ + + + +
+ +
+ + setName(e.target.value)} + className="pl-10" + required + />
-
- )} - +
+ + setEmail(e.target.value)} + className="pl-10" + required + /> +
+
+ +
- Privacy Policy - - - + +
+ + setPassword(e.target.value)} + className="pl-10" + required + /> +
+

+ Must be at least 8 characters +

+
+ +
+ +
+ + setConfirmPassword(e.target.value)} + className="pl-10" + required + /> +
+
- - + {repoUrl && ( +
+
+ +
+

+ Repository Ready +

+

+ {repoUrl} +

+
+
+
+ )} -
-
-
-
-
-
- - Or continue with - + + + + + +
+
+
+
+
+
+ + Or continue with + +
+
-
-
- + -
- Already have an account?{" "} - - Sign in - -
- +
+ Already have an account?{" "} + + Sign in + +
+ + + )}
); diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index ec1fbba8..b615854e 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -1,4 +1,5 @@ import type { AppProps } from 'next/app' +import { SessionProvider } from 'next-auth/react' import { ThemeProvider } from '@/context/ThemeContext' import { AuthProvider } from '@/contexts/AuthContext' import { Toaster } from '@/components/ui/toaster' @@ -6,11 +7,13 @@ import '@/app/globals.css' export default function App({ Component, pageProps }: AppProps) { return ( - - - - - - + + + + + + + + ) } diff --git a/src/utils/cn.ts b/src/utils/cn.ts new file mode 100644 index 00000000..b68e5127 --- /dev/null +++ b/src/utils/cn.ts @@ -0,0 +1,6 @@ +import { clsx } from "clsx"; +import { twMerge } from "tailwind-merge"; + +export function cn(...inputs: any[]) { + return twMerge(clsx(inputs)); +} \ No newline at end of file diff --git a/src/utils/rateLimit.ts b/src/utils/rateLimit.ts new file mode 100644 index 00000000..6bf957f7 --- /dev/null +++ b/src/utils/rateLimit.ts @@ -0,0 +1,46 @@ +const rateLimitMap = new Map(); +let lastCleanupAt = 0; + +const DEFAULT_WINDOW_MS = 900_000; +const DEFAULT_MAX_REQUESTS = 5; + +export function checkRateLimit(ip: string) { + // 1. Safely parse environment variables with fallbacks + const parsedWindowMs = Number.parseInt(process.env.RATE_LIMIT_WINDOW_MS ?? '', 10); + const parsedMaxRequests = Number.parseInt(process.env.RATE_LIMIT_MAX_REQUESTS ?? '', 10); + + const windowMs = Number.isInteger(parsedWindowMs) && parsedWindowMs > 0 + ? parsedWindowMs + : DEFAULT_WINDOW_MS; + + const maxRequests = Number.isInteger(parsedMaxRequests) && parsedMaxRequests > 0 + ? parsedMaxRequests + : DEFAULT_MAX_REQUESTS; + + const now = Date.now(); + + // 2. Periodically prune expired entries to bound memory growth + if (now - lastCleanupAt >= windowMs) { + for (const [key, value] of rateLimitMap.entries()) { + if (value.expiresAt <= now) rateLimitMap.delete(key); + } + lastCleanupAt = now; + } + + const record = rateLimitMap.get(ip); + + // 3. Apply rate limit logic + if (!record || record.expiresAt < now) { + rateLimitMap.set(ip, { count: 1, expiresAt: now + windowMs }); + return { success: true }; + } + + if (record.count >= maxRequests) { + const retryAfterSeconds = Math.ceil((record.expiresAt - now) / 1000); + return { success: false, retryAfter: retryAfterSeconds }; + } + + record.count += 1; + rateLimitMap.set(ip, record); + return { success: true }; +} \ No newline at end of file diff --git a/test-sanitize.mjs b/test-sanitize.mjs new file mode 100644 index 00000000..c87af40e --- /dev/null +++ b/test-sanitize.mjs @@ -0,0 +1,82 @@ +import { defaultSchema } from 'rehype-sanitize'; +import { unified } from 'unified'; +import remarkParse from 'remark-parse'; +import remarkRehype from 'remark-rehype'; +import rehypeRaw from 'rehype-raw'; +import rehypeSanitize from 'rehype-sanitize'; +import rehypeStringify from 'rehype-stringify'; +import remarkGfm from 'remark-gfm'; + +// Refined schema for sanitizing README markdown +const readmeSanitizeSchema = (() => { + const schema = { + ...defaultSchema, + // Note: "img" is already included in defaultSchema.tagNames, so we don't need to add it again. + + // Explicitly restrict protocols for attributes to mitigate protocol-based XSS (e.g. javascript:) + protocols: { + ...(defaultSchema.protocols || {}), + href: ["http", "https", "mailto"], // Only allow safe link protocols + }, + + attributes: { + ...(defaultSchema.attributes || {}), + // Allow target and rel attributes for links (for external links) + a: Array.from( + new Set([ + ...((defaultSchema.attributes?.a) || []), + "target", + "rel", + ]), + ), + // Allow standard image attributes used in READMEs (e.g. alignment and lazy loading) + img: ["src", "alt", "title", "width", "height", "align", "loading"], + }, + }; + return schema; +})(); + + +const processor = unified() + .use(remarkParse) + .use(remarkRehype, { allowDangerousHtml: true }) + .use(rehypeRaw) + .use(rehypeSanitize, readmeSanitizeSchema) + .use(rehypeStringify); + +const maliciousMarkdown = ` +# Comprehensive HTML/Markdown Sanitization Test + +## COLLAPSIBLE DETAILS +
+ Click to view details + Here is some hidden content. +
+ +## TABLES +| Header 1 | Header 2 | +|---|---| +| Cell 1 | Cell 2 | + +## ALIGNMENT & DECORATION +

Centered text

+
Injected Style (should be stripped)
+ +## DANGEROUS HTML TAGS + + + + + + + +## DANGEROUS ATTRIBUTES +Test Image +Malicious Link +
DOM Clobbering / Class Test
+`; + +processor.process(maliciousMarkdown).then((file) => { + console.log("--- Sanitize Output ---"); + console.log(String(file)); +}).catch(console.error);