diff --git a/README.md b/README.md index 79dc06b0..65aa8bba 100644 --- a/README.md +++ b/README.md @@ -1,129 +1,375 @@ -# Crowd Source FAQ (Yaksha FAQ Portal) +

+ + +

-Full-stack FAQ portal with semantic vector search, AI-powered community moderation, and an expert promotion layer. Built to handle 1 million registered users. +

Crowd Source FAQ (CSFAQ)

-GitHub: https://github.com/vicharanashala/crowd-source-faq -Full reference: [`docs/`](docs/README.md) · [Contributing](./CONTRIBUTING.md) · [Code of Conduct](./CODE_OF_CONDUCT.md) · [License](./LICENSE) +

+ AI-Powered Knowledge Management & Community FAQ Platform +

+ +

+ +![React](https://img.shields.io/badge/React-61DAFB?style=flat-square&logo=react&logoColor=black) +![TypeScript](https://img.shields.io/badge/TypeScript-3178C6?style=flat-square&logo=typescript&logoColor=white) +![Node.js](https://img.shields.io/badge/Node.js-339933?style=flat-square&logo=node.js&logoColor=white) +![Express](https://img.shields.io/badge/Express-000000?style=flat-square&logo=express&logoColor=white) +![MongoDB](https://img.shields.io/badge/MongoDB-47A248?style=flat-square&logo=mongodb&logoColor=white) +![Vite](https://img.shields.io/badge/Vite-646CFF?style=flat-square&logo=vite&logoColor=white) +![Tailwind](https://img.shields.io/badge/TailwindCSS-06B6D4?style=flat-square&logo=tailwindcss&logoColor=white) + +

+ +

+ Repository • + Installation • + Our Contributions • + Contributors +

--- -## Vision +## Table of Contents + +- [Overview](#overview) +- [Key Features](#key-features) +- [Phase 1 Feature Enhancements](#phase-1-feature-enhancements) +- [Bug Fixes](#bug-fixes) +- [Technology Stack](#technology-stack) +- [System Architecture](#system-architecture) +- [Project Structure](#project-structure) +- [Installation](#installation) +- [Environment Variables](#environment-variables) +- [Development Workflow](#development-workflow) +- [Testing](#testing) +- [Challenges & Limitations](#challenges--limitations) +- [Future Enhancements](#future-enhancements) +- [Contributors](#contributors) +- [Contributing](#contributing) +- [Acknowledgements](#acknowledgements) +- [License](#license) -**Automate the FAQ lifecycle end-to-end. Zero people in the loop. Reduce the operational FAQ culture.** +--- -Every question a user has has been asked before — and most will be asked again. The right answer should be there before the user finishes typing. The platform achieves this through four zero-touch pillars: +## Overview -- **Zero-touch ingestion** — Zoom meetings, webhooks, and manual uploads feed the knowledge base without human scheduling, categorising, or approval. -- **Zero-touch answering** — A 24-hour scheduler matches unanswered posts against the knowledge base; high-confidence matches auto-post, low-confidence escalate to humans. -- **Zero-touch quality control** — Approved FAQs are re-evaluated every 6 hours; drift, contradictions, and staleness are detected and flagged automatically. -- **Zero-touch user lifecycle** — Deletion is anonymisation, not destruction. Reputation, attribution, and audit history persist. +Crowd Source FAQ (CSFAQ) is an AI-powered knowledge management platform that lets users search FAQs, participate in community discussions, ask questions, and get intelligent, source-cited answers through semantic search and AI-assisted retrieval. -The platform is the operator. People handle exceptions, not the steady state. +The platform combines community-driven knowledge sharing with AI-powered search to reduce repetitive questions, improve response quality, and give both users and administrators a single, organized, self-maintaining knowledge base. + +This repository reflects our team's contributions during **Phase 1** of the AI internship programme, built on top of the existing CSFAQ (Yaksha FAQ Portal) codebase. --- -## About +## Key Features + + + + + + + +
+ +**User** +- AI-powered FAQ Search +- Community Discussion Forum +- Ask Questions +- My Questions Dashboard +- Saved Knowledge +- Draft Persistence +- Internship Roadmap +- Welcome Package +- Program Portal +- Support Ticket System +- Golden Ticket Module +- Email Notifications + + + +**AI** +- Semantic Search +- Intelligent Knowledge Retrieval +- AI Answer Ranking Optimisation +- Multi-source Response Selection +- Context-aware Search + + + +**Admin** +- FAQ Management +- Community Moderation +- Manual FAQ Promotion +- User Management +- AI Configuration +- Feature Flag Management +- Knowledge Management +- Support Dashboard +- Program & Batch Management +- Analytics Dashboard + +
+ +**Search Enhancements:** Real-time search · Dynamic filtering · Multi-category search · Keyboard shortcuts · Recent searches · Search suggestions · Responsive search interface + +--- + +## Phase 1 Feature Enhancements + +During Phase 1 of the internship, our team designed, built, and shipped the following enhancements into the existing production codebase. + +### 🗺️ Internship Roadmap +New interns struggled to understand the sequence of internship activities and where to find resources — everything was scattered across sections. We built **"The Internship Express,"** a centralized, transit-map-style visual timeline covering the full journey: Registration (Samagama) → Onboarding → Mandatory Sessions → Phase 1 (Crowd Source FAQ) → Phase 2 (Mentor Projects) → Completion — complete with milestone tracking and quick links to Samagama, ViBe, and Matrix Mystics. +> **Impact:** Simplifies onboarding, improves resource accessibility, enables visual progress tracking. + +### 📧 Email Notifications for Resolved Questions +Previously, users could only find out their question was answered by checking the in-app bell icon. We built a `sendEmail.ts` utility (Nodemailer, env-configured), extended `notificationDispatcher.ts` with an optional `message` field without breaking ~12 existing call sites, and wired the real question + answer content into a branded HTML email. Verified end-to-end with Mailtrap. +> **Impact:** Users are notified the instant their question is resolved — no manual checking required. + +### 📝 Draft Persistence for Community Posts +Users lost their in-progress questions if they closed the "Ask Question" dialog, refreshed, or navigated away. We added automatic `localStorage` draft saving with **Keep Draft** / **Discard Draft** on reopen, and fixed a bug where autosave was silently overwriting stored drafts on component init — all without touching duplicate detection, posting, categories, or attachments. +> **Impact:** No more lost work; drafts persist reliably across refreshes and dialog reopenings. + +### 👤 My Questions Dashboard +There was no way to see just your own questions without scrolling the entire community feed. We added a dedicated view pulling from existing post data — purely additive, zero changes to existing routes or logic. +> **Impact:** Fast, personal access to your own submissions. + +### ⭐ Manual FAQ Promotion +Turning a great community answer into an official FAQ required manual database work. We built a backend endpoint and Admin Community UI integration to promote posts to FAQs in one click. +> **Impact:** Faster knowledge-base curation, no manual DB operations, no schema changes required. -Samagama (internally "Yaksha FAQ Portal") turns an organisation's accumulated conversations into a searchable, self-maintaining FAQ. It combines hybrid vector + keyword search with a community Q&A board and a fully automated ingestion pipeline that pulls transcripts from Zoom, extracts Q&A with AI, and indexes them for retrieval in seconds. +### 🔍 Enhanced Community Search +Community search lacked speed and memory. We added real-time client-side filtering, keyboard shortcuts (`/` and `Ctrl/Cmd+K`), persistent Recent Searches, a clear button, a result counter, and better empty states — while removing redundant/duplicate UI code. +> **Impact:** Faster, more accessible content discovery. -Built for organisations whose community generates more questions than a human team can answer — student cohorts, open-source projects, internal forums, customer-success communities. Target scale: 1 million registered users with constant conversational input. +### 🤖 AI Answer Ranking Optimisation +The AI assistant pulls from FAQs, Community Q&A, and Zoom lecture notes — each scored on different scales, which skewed answer selection. We introduced a **score-normalization layer** to compare all sources fairly on a common scale, backed by automated unit tests. +> **Impact:** Fairer, more accurate multi-source answer selection. -See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) for the architecture deep-dive and [docs/PIPELINES.md](docs/PIPELINES.md) for pipeline internals. +### 🎛️ Search Bar & Search Experience Improvements +The search bar lacked flexible filters and consistent behavior. We redesigned it with a segmented filter (FAQ / All / Community Posts), multi-select category filters, unified Enter-key/button submission, an inline clear (X) button, and improved dropdown/loading/empty states — all client-side, no extra API calls. +> **Impact:** Instant filtering, cleaner UI, no unnecessary navigation. + +### 🛠️ Bug Fixes & Performance Improvements +Stability improvements, UI fixes, and optimization across modules (see [Bug Fixes](#bug-fixes) below for the two headline fixes). + +| Feature | Description | +|---------|-------------| +| Internship Roadmap | Interactive roadmap with milestones, learning resources, and progress tracking | +| Email Notifications | Automatic emails when admins resolve user questions | +| Draft Persistence | Auto-save community drafts with restore/discard | +| My Questions | Dashboard of all questions created by the logged-in user | +| Manual FAQ Promotion | Convert community discussions into official FAQs | +| Enhanced Community Search | Real-time filtering, categories, recent searches, keyboard shortcuts | +| AI Answer Ranking Optimisation | Normalized, fair answer selection across knowledge sources | +| Search Bar Improvements | Segmented search, multi-select categories, better dropdown UX | +| Bug Fixes & Performance | Stability, UI, and edit/delete fixes across modules | + +--- + +## Bug Fixes + +| Issue | Root Cause | Fix | +|---|---|---| +| **Unable to edit/delete own posts** | Incorrect ownership validation blocked rightful owners from modifying their own posts | Corrected ownership-check logic in the Community module while preserving existing authorization rules | +| **Search feedback popup layout overlap** | "Did this answer your question?" popup's text and buttons collided in a narrow fixed-width container | Fixed the flex layout and shortened button labels so the prompt and both buttons fit on one line, verified across multiple searches | --- -## Tech Stack +## Technology Stack | Layer | Technologies | -|---|---| -| Frontend | React 18, Vite, TypeScript, Tailwind CSS, Framer Motion, Axios, Recharts, React Router 6, Vitest | -| Backend | Node.js, Express 4, TypeScript (ESM), Mongoose 8, JWT, bcryptjs, Helmet, CORS, Morgan, Multer, Zod, express-rate-limit, dotenv, OpenAI SDK, Vitest | -| Database & Storage | MongoDB Atlas (with Vector Search), Upstash Redis (optional), LRU cache, Cloudinary | -| Search & AI | Xenova/transformers (768-dim local embeddings), Atlas Vector Search, $text search, Reciprocal Rank Fusion | -| AI Providers | Anthropic, OpenAI, XAI, MiniMax (per-pipeline configurable) | -| DevOps | Sentry, Ngrok (local webhook tunnel), Twilio (SMS), SMTP, Vitest | +|--------|--------------| +| Frontend | React 18, TypeScript, Vite, Tailwind CSS, Framer Motion, Recharts, React Router 6 | +| Backend | Node.js, Express.js, Mongoose | +| Database | MongoDB (Atlas Vector Search) | +| Authentication | JWT, bcryptjs | +| AI | Semantic Search, AI Answer Ranking, Hybrid Retrieval | +| Email | Nodemailer | +| Storage | Cloudinary | +| Version Control | Git & GitHub | + +--- + +## System Architecture + +```text + Users + │ + ▼ + React + TypeScript (Frontend) + │ + REST API Requests + │ + ▼ + Express.js Backend + │ + ┌─────────────┼─────────────┐ + ▼ ▼ ▼ + MongoDB AI Services External Services + (Database) Knowledge Engine SMTP • Cloudinary • Zoom +``` --- -## Quick Start +## Project Structure + +```text +crowd-source-faq +│ +├── apps +│ ├── frontend # React + Vite SPA +│ └── backend # Express + TypeScript API +│ +├── packages +├── docs +├── public +└── README.md +``` + +--- + +## Installation + +Clone the repository + +```bash +git clone https://github.com/sharanu-patil33/crowd-source-faq.git +cd crowd-source-faq +``` + +Install dependencies ```bash -./run.sh # Full-stack runner: env setup, ngrok, backend + frontend +pnpm install ``` -`run.sh` prompts for `MONGODB_URI` and `JWT_SECRET` on first run, then saves them to `apps/backend/.env.local`. The script will not overwrite existing values. Session logs are written to `logs/session_*.txt`. +**Quick start (alternative):** run `./run.sh` from the project root — it handles env setup, ngrok, and starts both backend and frontend in one step. It will prompt for `MONGODB_URI` and `JWT_SECRET` on first run and save them to `apps/backend/.env.local` without overwriting existing values. --- -## Key Features +## Running the Project + +```bash +pnpm dev +``` + +| Service | URL | +|---|---| +| Frontend | http://localhost:5173 | +| Backend | http://localhost:6767 | -Eight flagship capabilities define this platform: +--- -- **Zoom transcript ingestion with per-user OAuth** — Each user connects their own Zoom account via OAuth. Webhook-fired downloads parse VTT transcripts, extract Q&A pairs via AI, and dual-publish: `ZoomInsight` (admin-reviewed) and `TranscriptKnowledge` (auto-approved, immediately vector-searchable). Includes retry + dead-letter queue for failed meetings and admin backfill for historical meetings. See [docs/PIPELINES.md#4-zoom-ingestion-pipeline](docs/PIPELINES.md). -- **AI auto-answer pipeline for community posts** — A scheduler (every 24h) — and a one-click **Run AI** button in the admin dashboard — finds unanswered posts and searches **all three knowledge sources in parallel** (FAQ + Community Q&A + Transcript Knowledge). Best match above ≥0.85 confidence auto-posts; 0.60–0.84 queues for human review; below 0.60 (or sensitive content) escalates. When no direct match exists, the LLM is given the top gathered context to synthesize an answer. Per-pipeline AI provider configurable. See [docs/PIPELINES.md#1-auto-answer-pipeline](docs/PIPELINES.md). -- **FAQ audit pipeline** — A scheduler (every 6h) re-evaluates approved FAQs against the live knowledge base (TranscriptKnowledge + ZoomInsights). Uses AI to judge correctness and emits verdicts: `correct` (≥0.80), `drift_detected` (0.60–0.79), `contradiction` (<0.60), or `stale`. Flagged FAQs land in `/admin/faqs/review` with `reviewStatus='pending_review'`, `flagType='auto'`, and an incremented `reviewCycle`. See [docs/PIPELINES.md#2-faq-audit-pipeline](docs/PIPELINES.md). -- **FAQ freshness & staleness detection** — Every approved FAQ carries a `freshness_tier` (`evergreen` / `seasonal` / `volatile`) and a per-tier review interval. A daily cron auto-flags FAQs whose last-verified date exceeds the interval, opening a peer-review window on `/admin/faqs/review`. Anyone can vote `still_accurate` / `needs_update`; the threshold auto-verifies, otherwise it escalates to a moderator. `FreshnessBadge` on the public FAQ card surfaces verified-vs-under-review status. See [docs/PIPELINES.md#7-faq-freshness-pipeline](docs/PIPELINES.md). -- **Golden Ticket — Spurti Points escalation** — A premium user-driven escalation channel. Users spend Spurti Points (SP) to bump a time-sensitive query to the top of the admin queue (higher SP = higher priority). SP is consumed on submission; a 48h cooldown blocks repeat submissions. Admins resolve or reject — no penalty, no ban, just a single unified cooldown rule. Includes a live Escalation Queue (right column on `/golden`) sorted by SP spend, anonymous to non-admin viewers. Toggleable from `/admin/features`. -- **Batch management + public guest FAQ portal** — FAQs, categories, and analytics are scoped to a `Batch` (cohort, term, program). A first-class `Category` model replaces the old free-text field. Guests land on `/explore/select` to pick a batch, then browse the public FAQ at `/faq` with no account required. Admin can create/archive batches and promote FAQs between them. See [docs/BATCH_MANAGEMENT_PLAN.md](docs/BATCH_MANAGEMENT_PLAN.md). -- **Schema-driven context fields per support category** — Each support category (`internet`, `camera`, `microphone`, `device`, `power`, `other`) has an admin-editable schema of context fields (text, textarea, number, date, boolean, dropdown). Admins add, edit, reorder, or archive fields from `/admin/support/categories` without redeploying. The frontend renders dynamic inputs from the live schema. See [docs/SCHEMA_DRIVEN_CONTEXT_PLAN.md](docs/SCHEMA_DRIVEN_CONTEXT_PLAN.md). -- **Soft-delete with anonymization** — Deleting a user never hard-deletes their records. The account is anonymised: `isDeleted=true`, `deletedAt` timestamp, `name` becomes `Deleted User`, `email` is rewritten to a non-routable placeholder, `password` is replaced with a random UUID to break login. All posts, comments, votes, reputation logs, and audit trail entries remain intact — preserving referential integrity, attribution history, and regulatory compliance. +## Environment Variables -Other capabilities: semantic hybrid search, community Q&A board, reputation system + badges + leaderboard, SpillTheTea event-driven notifications, per-user Zoom OAuth, RAG-powered `/ask-ai` assistant with image + file attachments, soft user lifecycle, experimental feature flags, support tickets (troubleshoot → admin triage → resolution). +| Variable | Required | Purpose | +|---|---|---| +| `MONGODB_URI` | ✅ | Database connection | +| `JWT_SECRET` | ✅ | Auth token signing | +| `ANTHROPIC_API_KEY` / `OPENAI_API_KEY` / `XAI_API_KEY` / `MINIMAX_API_KEY` | At least one | AI provider access | +| `MAIL_HOST` / `MAIL_PORT` / `MAIL_USER` / `MAIL_PASS` | Optional | Email notifications | +| `CLOUDINARY_*` | Optional | Image uploads | +| `UPSTASH_REDIS_*` | Optional | Caching | --- -## Admin Dashboard +## Development Workflow + +```text +Fork Repository → Create Feature Branch → Implement Feature → Commit Changes + → Push Branch → Open Pull Request → Code Review → Merge into Main + → Final Pull Request to Official Repository +``` + +--- -The admin panel at `/admin` (mounted at `/api/admin/*`) provides telemetry, moderation, and operational control. Key areas: +## Testing -- **Telemetry & analytics** — live stats, FAQ growth, top categories, search insights, user-activity charts, activity feed, failed-query analytics, unresolved-search tracker -- **Operational pages** — AdminDashboard, AdminFAQs, FaqReview, AdminFAQAudit, AdminAutoAnswerQueue, AdminCommunity, AdminUsers, AdminModeration, AdminZoomMeetings, AdminZoomInsights, AdminLeaderboard, AdminUnresolvedSearch, AdminAISettings, AdminSettings, AdminLogin -- **Moderation** — every ban, suspend, warn, and soft-delete recorded in `ModerationLog`; every reputation change (+2 upvote, +5 accepted answer, -2/-5 on removal) recorded in `ReputationLog` -- **AI pipeline visibility** — unified `PipelineResult` collection (30-day TTL) for both auto-answer and audit outcomes; Zoom health endpoint reports OAuth/API circuit state, cache hit rate, failing-meetings count, dead-letter count, pending-retry count; Prometheus metrics at `/api/metrics` (search latency, cache hits, RAG duration, queue depth) +The following areas were manually and automatically verified: -For the full admin route map and per-page behaviour, see [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md). +- User Authentication +- FAQ Module +- Community Module (including edit/delete ownership fix) +- AI Search & Answer Ranking +- Internship Roadmap +- Draft Persistence (save, restore, discard) +- Email Notifications (verified via Mailtrap) +- Search Optimisation +- Admin Dashboard +- Performance & Stability --- -## User Experience +## Challenges & Limitations + +**Challenges** +- Understanding a large, multi-module existing codebase before implementing new features +- Integrating changes without regressions across interconnected modules +- Maintaining UI/coding consistency across multiple contributors +- Extensive scenario testing to avoid breaking existing behavior +- Understanding the existing AI retrieval/ranking pipeline before improving it + +**Limitations** +- Dependence on external services (email, AI providers) affects overall reliability +- Continued scalability optimization needed as users, posts, and FAQs grow +- Platform is intentionally an evolving system — built for continuous enhancement -The user-facing app (`/`, `/faq`, `/community`, `/saved`, `/account`, `/leaderboard`) gives authenticated users full participation in the knowledge loop: +--- -- **Discover** — hybrid semantic + keyword search at `/api/search` (public), semantic suggestions at `/api/search/suggest`, category browsing -- **Ask the community** — post creation with Zod validation, debounced duplicate detection against the FAQ base (banner + block on match), auto-normalised tags, Cloudinary image attachments -- **Engage** — upvotes with reputation-farming prevention (reverses on removal), bookmarks at `/saved`, nested comment threads with optimistic UI, accept-answer (locks verified/expert comments from edit), edit/delete own comments, share via clipboard, report and flag-outdated -- **Notifications** — in-app bell, SpillTheTea event stream (`post_answered`, `post_deleted`, etc.), per-event-type settings, email + SMS delivery -- **Reputation** — points for accepted answers, badges at thresholds, expert promotion by peer vote, public leaderboard -- **AI assistant** — RAG-powered `/ask-ai` (5/day anonymous quota via localStorage, unlimited for authenticated users), sources cited, **accepts file and image attachments (max 4 files, 10 MB each) — images sent as vision input, text files inlined into the prompt** -- **Zoom integration** — per-user OAuth from `/account`, manual `.vtt` / `.txt` / raw-text upload, last-synced status card, no admin required -- **Search feedback** — "Report missing FAQ" on zero results, admin-promotable to FAQ +## Future Enhancements -For per-route behaviour and field schemas, see [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md). +- AI-powered personalization (recommendations based on user activity/interests) +- More advanced semantic search & intelligent query suggestions +- Richer analytics/reporting dashboards for admins +- Multilingual support +- Mobile application +- Newer/better AI models with improved context understanding +- General performance optimisation --- -## Project Structure +## Contributors -``` -Crowd Source FAQ/ -├── apps/ -│ ├── backend/ # Express + TypeScript API -│ └── frontend/ # React + Vite SPA -├── docs/ # Full documentation -└── run.sh # Local dev runner (env setup, ngrok, backend + frontend) -``` +This project was developed collaboratively during the internship programme. + +| Name | Role | +|------|------| +| Sharanagouda Policepatil| Team Lead | +| Akshay Vishwakarma | Team Member | +| Anil Dudi | Team Member | +| Ashish Patra | Team Member | +| Ayushi Karn | Team Member | +| Chandana V | Team Member | +| Durgesh Manoj Mahajan | Team Member | +| Rohith N R | Team Member | +| Samruddhi Bhaskar Chate | Team Member | + +Team Lead Responsibilities: repository & branch management, pull request reviews, merge conflict resolution, feature integration, testing & validation, final project integration, final PR submission to the official repository. --- -## Environment Variables +## Contributing + +1. Fork the repository +2. Create a new feature branch +3. Commit your changes +4. Push the branch +5. Open a Pull Request -Required: `MONGODB_URI`, `JWT_SECRET` -Optional: at least one AI provider key (`ANTHROPIC_API_KEY` / `OPENAI_API_KEY` / `XAI_API_KEY` / `MINIMAX_API_KEY`), Zoom OAuth credentials, `CLOUDINARY_*`, `SENTRY_DSN`, Twilio + SMTP for notifications, `UPSTASH_REDIS_*` +Please follow the project's coding standards and review guidelines before submitting a Pull Request. -See [docs/ARCHITECTURE.md#10-env-variables-reference](docs/ARCHITECTURE.md#10-env-variables-reference) for the full list. +--- + +## Acknowledgements + +This project was developed as part of an internship programme focused on collaborative software development, AI-assisted knowledge management, and modern GitHub workflows. It provided hands-on experience in feature development, collaborative version control, pull request reviews, code integration, testing, and maintaining a production-scale application. --- -## License +## Repository + +**GitHub Repository:** https://github.com/sharanu-patil33/crowd-source-faq -[MIT](./LICENSE) © 2026 vicharanashala +--- \ No newline at end of file diff --git a/apps/backend/package.json b/apps/backend/package.json index 3a8e48b9..51256c03 100644 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -56,6 +56,7 @@ "mongoose": "^8.0.3", "morgan": "^1.10.1", "multer": "^2.1.1", + "nodemailer": "^9.0.3", "obscenity": "^0.4.6", "openai": "^6.39.0", "pdf-parse": "^2.4.5", @@ -74,6 +75,7 @@ "@types/morgan": "^1.9.10", "@types/multer": "^2.1.0", "@types/node": "^25.9.1", + "@types/nodemailer": "^8.0.1", "@types/range-parser": "^1.2.7", "@types/uuid": "^10.0.0", "nodemon": "^3.0.2", diff --git a/apps/backend/src/modules/admin/admin.controller.ts b/apps/backend/src/modules/admin/admin.controller.ts index 81e6006d..eb45206c 100644 --- a/apps/backend/src/modules/admin/admin.controller.ts +++ b/apps/backend/src/modules/admin/admin.controller.ts @@ -678,6 +678,107 @@ export const getCommunityPosts = async (req: Request, res: Response): Promise => { + try { + const post = await CommunityPost.findById(req.params.id); + if (!post) { + res.status(404).json({ message: 'Post not found.' }); + return; + } + // Per-program scope check — same pattern as deleteCommunityPost above. + if (post.batchId && req.programContext?.batchId && + post.batchId.toString() !== req.programContext.batchId) { + res.status(404).json({ message: 'Post not found.' }); + return; + } + + const answer = (post.answer ?? '').trim(); + if (!answer) { + res.status(400).json({ message: 'This post has no answer yet — nothing to promote.' }); + return; + } + + // Idempotent: don't create a duplicate FAQ if this post was already promoted. + const existing = await FAQ.findOne({ sourceCommunityPostId: post._id }); + if (existing) { + res.status(409).json({ message: 'This post has already been promoted to an FAQ.', faq: existing }); + return; + } + + // Best-effort embedding — never block the promotion on it. + let embedding: number[] | undefined; + try { + const text = `Question: ${post.title}. Answer: ${answer}`; + const timeout = new Promise((_, reject) => + setTimeout(() => reject(new Error('generateEmbedding timeout (5s)')), 5000) + ); + embedding = (await Promise.race([generateEmbedding(text), timeout])) as any; + } catch (embErr) { + console.warn('[admin] promoteCommunityPostToFAQ: embedding skipped:', (embErr as Error).message); + } + + const now = new Date(); + const faq = await FAQ.create({ + question: sanitizeHtml(post.title), + answer: sanitizeHtml(answer), + category: 'Community', + tags: post.tags ?? [], + status: 'approved', + embedding, + batchId: post.batchId ?? null, + createdBy: post.author, + trustLevel: 'expert', // straight to "Admin Approved" — an admin made the call directly + sourceType: 'community_promotion', + sourceCommunityPostId: post._id, + promotedAt: now, + objectionStatus: 'none', + promotionMetadata: { + upvotesAtPromotion: post.upvotes?.length ?? 0, + communityAnswerAuthorId: post.answerAuthorId ?? null, + promotedBy: req.user!._id, + }, + }); + + // Fast-forward the post's lifecycle straight to converted_to_faq — + // skip the community_accepted / ai_validated / admin_accepted stages + // since none of those gates applied here. + post.lifecycle ??= { status: 'open', statusHistory: [] } as any; + const fromStatus = post.lifecycle.status ?? 'open'; + post.lifecycle.statusHistory ??= []; + (post.lifecycle.statusHistory as any).push({ + from: fromStatus, + to: 'converted_to_faq', + changedBy: req.user!._id, + changedAt: now, + note: 'Promoted directly to FAQ by admin (discretionary, no eligibility checks)', + }); + post.lifecycle.status = 'converted_to_faq'; + post.lifecycle.convertedToFaqAt = now; + await post.save(); + + await Promise.all([ + invalidateCache().catch((e) => console.error('[admin] invalidateCache after promote:', e)), + ]); + invalidatePublicCaches(); + + await logAction(req.user!._id.toString(), 'promote_community_post_to_faq', faq._id.toString(), 'faq', faq.question); + + res.json({ message: 'Post promoted to FAQ.', faq }); + } catch (error) { + res.status(500).json({ message: 'Server error', error: (error as Error).message }); + } +}; + // DELETE /api/admin/community/:id export const deleteCommunityPost = async (req: Request, res: Response): Promise => { try { @@ -703,4 +804,4 @@ export const deleteCommunityPost = async (req: Request, res: Response): Promise< } catch (error) { res.status(500).json({ message: 'Server error', /* error: process.env.NODE_ENV === 'development' ? (error as Error).message : undefined */ }); } -}; +}; \ No newline at end of file diff --git a/apps/backend/src/modules/admin/admin.routes.ts b/apps/backend/src/modules/admin/admin.routes.ts index 2e7f60bc..2616fc86 100644 --- a/apps/backend/src/modules/admin/admin.routes.ts +++ b/apps/backend/src/modules/admin/admin.routes.ts @@ -17,6 +17,7 @@ import { getUserActivityChart, getCommunityPosts, deleteCommunityPost, + promoteCommunityPostToFAQ, } from './admin.controller.js'; import { getCommunityPendingFAQs, @@ -175,6 +176,10 @@ router.patch('/faq/:id', updateFAQ); router.patch('/faqs/:id', updateFAQ); router.delete('/faq/:id', deleteFAQ); router.delete('/community/:id', deleteCommunityPost); +// Admin-discretion: instantly promote a community post's Q&A to FAQ, +// no eligibility checks (contrast with /community-promotions/* below, +// which is the gated auto-pipeline). +router.post('/community/:id/promote-to-faq', promoteCommunityPostToFAQ); // FAQ promotion management (trust levels) — from promotionService router.get('/faqs/community-pending', getCommunityPendingFAQs); @@ -190,4 +195,4 @@ router.get('/community-promotions/queue', getPromotionQueue); router.get('/queue/stats', getQueueStats); router.get('/queue/jobs/:id', getQueueJob); -export default router; +export default router; \ No newline at end of file diff --git a/apps/backend/src/modules/ai/__tests__/rag.score-normalization.test.ts b/apps/backend/src/modules/ai/__tests__/rag.score-normalization.test.ts new file mode 100644 index 00000000..9dae5cba --- /dev/null +++ b/apps/backend/src/modules/ai/__tests__/rag.score-normalization.test.ts @@ -0,0 +1,52 @@ +/** + * rag.score-normalization.test — regression guard for the cross-source + * score scale bug in the /ask-ai RAG pipeline. + * + * Bug: runRag() in rag.service.ts merges hits from three retrieval + * sources and sorts them by `.score` to decide citation order AND + * which sources survive the MAX_CONTEXT_CHARS truncation. FAQ/Community + * hits carry a Reciprocal Rank Fusion score (max ~0.033, see + * searchFaqs()/searchCommunity()); Knowledge hits carry an independent + * cosine-similarity-based score already normalized to [0, 1] (see + * scoreAndSort() in knowledge-base.service.ts). Sorting these unmodified + * meant a barely-relevant Knowledge hit (score just over its 0.05 floor) + * always outranked even a rank-#1-in-both-searches FAQ/Community hit — + * silently degrading answer quality. + * + * Fix: normalizeRrfScore() rescales the RRF score onto the same [0, 1] + * ceiling as the Knowledge score before the merged sources are sorted. + * These tests pin that behavior down with pure-function assertions — + * no Mongo/Atlas Search/HTTP required. + */ + +import { describe, it, expect } from 'vitest'; +import { normalizeRrfScore, RRF_SCORE_CEILING } from '../rag.service.js'; + +describe('normalizeRrfScore', () => { + it('maps the RRF ceiling (rank #1 in both vector + text search) to exactly 1', () => { + const bestPossibleRrf = 1 / 60 + 1 / 60; // rank 0 in both lists + expect(normalizeRrfScore(bestPossibleRrf)).toBeCloseTo(1, 10); + }); + + it('maps a single rank-#1 hit (only in one of the two lists) to 0.5', () => { + const singleListTopRank = 1 / 60; + expect(normalizeRrfScore(singleListTopRank)).toBeCloseTo(0.5, 10); + }); + + it('never exceeds 1 even if given an out-of-range value', () => { + expect(normalizeRrfScore(RRF_SCORE_CEILING * 5)).toBe(1); + }); + + it('demonstrates the pre-fix bug is resolved: a top-ranked FAQ hit now outranks a barely-passing Knowledge hit', () => { + const topFaqRrfScore = 1 / 60 + 1 / 60; // best possible FAQ RRF score + const weakKnowledgeScore = 0.06; // just above the 0.05 floor in scoreAndSort() + + // Before the fix, sources were compared as raw scores: + const rawFaqScore = topFaqRrfScore; // ~0.0333 + expect(rawFaqScore < weakKnowledgeScore).toBe(true); // this was the bug — FAQ always lost + + // After the fix, scores are normalized onto a comparable [0, 1] scale: + const normalizedFaqScore = normalizeRrfScore(topFaqRrfScore); + expect(normalizedFaqScore > weakKnowledgeScore).toBe(true); // FAQ now correctly outranks it + }); +}); diff --git a/apps/backend/src/modules/ai/rag.service.ts b/apps/backend/src/modules/ai/rag.service.ts index 18ff3467..5cb08c3c 100644 --- a/apps/backend/src/modules/ai/rag.service.ts +++ b/apps/backend/src/modules/ai/rag.service.ts @@ -33,7 +33,12 @@ export interface RagSource { snippet: string; /** URL the client can deep-link to. */ url: string; - /** Confidence in [0, 1] (vector cosine + keyword overlap). */ + /** + * Confidence in [0, 1], normalized across sources so scores are + * comparable for merged sort/truncation (see normalizeRrfScore() + * below for how FAQ/Community RRF scores are rescaled onto this + * same ceiling as the Knowledge cosine-based score). + */ score: number; } @@ -45,6 +50,20 @@ export interface RagResult { } const TOP_K_PER_SOURCE = 4; + +/** + * FAQ/Community hits are scored via Reciprocal Rank Fusion — the sum of + * 1/(60+rank) across the vector-search and text-search result lists. Its + * theoretical ceiling is 1/60 + 1/60 = 1/30 (a document ranked #1 in both + * lists). Knowledge hits (see scoreAndSort() in knowledge-base.service.ts) + * are scored on an independent, already-normalized [0, 1] cosine-similarity + * scale. Exported as a pure function so the rescaling can be unit tested + * without standing up Mongo/Atlas Search. + */ +export const RRF_SCORE_CEILING = 2 / 60; // = 1/30 +export function normalizeRrfScore(raw: number): number { + return Math.min(1, raw / RRF_SCORE_CEILING); +} const MAX_CONTEXT_CHARS = 14000; // leave headroom under typical 16k context windows interface FaqHit { _id: unknown; question: string; answer: string; category?: string; trustLevel?: string; score: number } @@ -269,6 +288,20 @@ export async function runRag(question: string, attachments: RagAttachment[] = [] ]); // Normalize each source into the common shape. + // + // IMPORTANT: faqHits/postHits carry a Reciprocal Rank Fusion score + // (sum of 1/(60+rank) across vector+text search) whose theoretical + // ceiling is 1/60 + 1/60 = 1/30 ≈ 0.033. knowledgeHits carry a + // cosine-similarity-based score already normalized to [0, 1] + // (see scoreAndSort() in knowledge-base.service.ts). Sorting these + // scores together unmodified means a barely-relevant Knowledge hit + // (score just over its 0.05 floor) always outranks even a + // rank-1-in-both-searches FAQ/Community hit (score capped at 0.033) — + // silently degrading citation order and, worse, causing strong + // FAQ/Community sources to be dropped first by the MAX_CONTEXT_CHARS + // truncation in buildContext(). Rescale RRF scores onto the same + // [0, 1] ceiling as the knowledge score so all three sources are + // comparable before the merged sort (see normalizeRrfScore() above). const sources: RagSource[] = [ ...faqHits.map((h) => ({ id: `faq:${String(h._id)}`, @@ -276,7 +309,7 @@ export async function runRag(question: string, attachments: RagAttachment[] = [] title: h.question, snippet: h.answer.slice(0, 600), url: `/faq/${String(h._id)}`, - score: h.score, + score: normalizeRrfScore(h.score), })), ...postHits.map((h) => ({ id: `community:${String(h._id)}`, @@ -284,7 +317,7 @@ export async function runRag(question: string, attachments: RagAttachment[] = [] title: h.title, snippet: h.body.slice(0, 600), url: `/community?post=${String(h._id)}`, - score: h.score, + score: normalizeRrfScore(h.score), })), ...knowledgeHits.map((h) => ({ id: `knowledge:${h._id}`, @@ -295,11 +328,11 @@ export async function runRag(question: string, attachments: RagAttachment[] = [] // or to the admin KB if we know the meeting. For now, a stable // deep-link to a future /knowledge/:id is best-effort. url: `/community?post=${h._id}`, - score: h.score, + score: h.score, // already normalized to [0, 1] in scoreAndSort() })), ]; - // Re-rank by score so the LLM sees the strongest sources first. + // Re-rank by (now comparable) score so the LLM sees the strongest sources first. sources.sort((a, b) => b.score - a.score); // If we found nothing at all, skip the LLM call — just say "no answer". diff --git a/apps/backend/src/modules/community/post-lifecycle.controller.ts b/apps/backend/src/modules/community/post-lifecycle.controller.ts index d7648974..aed41946 100644 --- a/apps/backend/src/modules/community/post-lifecycle.controller.ts +++ b/apps/backend/src/modules/community/post-lifecycle.controller.ts @@ -108,7 +108,8 @@ export const resolvePost = async (req: Request, res: Response): Promise => recipientId: post.author, eventType: 'post_resolved', link: `/community?post=${post._id}`, - title: 'Your question was resolved!', + title: post.title, + message: `Your question "${post.title}" has been answered: ${post.answer}`, }).catch((err) => { communityLog.warn(`[post] Failed to dispatch post resolved notification: ${(err as Error).message}`); }); diff --git a/apps/backend/src/scripts/testEmail.ts b/apps/backend/src/scripts/testEmail.ts new file mode 100644 index 00000000..ae044e9d --- /dev/null +++ b/apps/backend/src/scripts/testEmail.ts @@ -0,0 +1,20 @@ +/** + * scripts/testEmail.ts + * One-off script to verify the email utility works before wiring it + * into the real notification flow. Run with: + * pnpm --filter yaksha-faq-backend exec tsx src/scripts/testEmail.ts + */ +import 'dotenv/config'; +import { sendEmail } from '../utils/email/sendEmail.js'; + +async function main() { + const result = await sendEmail({ + to: 'test@example.com', // Mailtrap catches all emails regardless of the "to" address + subject: 'Test email from CSFAQ local setup', + text: 'If you see this in your Mailtrap inbox, the email utility works!', + }); + console.log('Email sent successfully:', result); + process.exit(result ? 0 : 1); +} + +main(); \ No newline at end of file diff --git a/apps/backend/src/services/notifications.service.ts b/apps/backend/src/services/notifications.service.ts index 77e6bd12..31da65ea 100644 --- a/apps/backend/src/services/notifications.service.ts +++ b/apps/backend/src/services/notifications.service.ts @@ -25,6 +25,11 @@ import NotificationOutbox from '../models/NotificationOutbox.js'; import { logger } from '../utils/http/logger.js'; import { startSession } from 'mongoose'; +// === EMAIL FEATURE — new imports === +import { sendEmail } from '../utils/email/sendEmail.js'; +import User from '../modules/auth/user.model.js'; +// === END === + // ─── Public types ───────────────────────────────────────────────────────── export type DispatchEventType = @@ -150,12 +155,73 @@ class NotificationsService { link: input.link, read: false, }); + // === EMAIL FEATURE START === + if (input.eventType === 'question_answered' || input.eventType === 'post_resolved') { + const user = await User.findById(recipientId).select('email name').lean(); + if (user?.email) { + void sendEmail({ + to: user.email, + subject: title, + text: message, + html: `
+ +
+ Yaksha FAQ +
+ + +
+

Hi there,

+

+ Your question "${title}" just got a response. +

+ + +
+

${message}

+
+ +

+ Read the full thread here:
+ + ${process.env.CLIENT_URL ?? ''}${input.link} + +

+ +

The Yaksha FAQ Team

+
+ + + +
+

+ You're receiving this because you have an active question on Yaksha FAQ Portal. +

+

+ Yaksha FAQ Portal · Vicharanashala Lab, IIT Ropar +

+
+
`, + }); + } + } + // === EMAIL FEATURE END === return { delivered: true, viaOutbox: false, notificationId: doc._id }; } catch (err) { const error = err as Error; logger.warn( `[notifications] direct dispatch failed for ${String(recipientId)}: ${error.message}. ` + - `Routing to outbox.`, + `Routing to outbox.`, ); // Direct dispatch failed — schedule for the drain. try { @@ -177,7 +243,7 @@ class NotificationsService { // caller's operation must continue. logger.error( `[notifications] OUTBOX write also failed for ${String(recipientId)}: ` + - `${(outboxErr as Error).message}. Notification permanently lost.`, + `${(outboxErr as Error).message}. Notification permanently lost.`, ); return { delivered: false, viaOutbox: false }; } @@ -204,7 +270,7 @@ class NotificationsService { await NotificationOutbox.deleteMany({ _id: { $in: ids } }); logger.warn( `[notifications] outbox cap exceeded (${total} > ${OUTBOX_MAX_ROWS}). ` + - `Dropped ${ids.length} oldest pending rows.`, + `Dropped ${ids.length} oldest pending rows.`, ); } @@ -259,7 +325,7 @@ class NotificationsService { // Give up — log a hard error and drop the row. logger.error( `[notifications] outbox row ${String(row._id)} exceeded ${OUTBOX_MAX_ATTEMPTS} attempts. ` + - `Dropping. Last error: ${(err as Error).message}`, + `Dropping. Last error: ${(err as Error).message}`, ); await NotificationOutbox.deleteOne({ _id: row._id }); dropped++; @@ -287,7 +353,7 @@ class NotificationsService { if (delivered > 0 || dropped > 0) { logger.info( `[notifications] drain: ${attempted} attempted, ${delivered} delivered, ` + - `${rescheduled} rescheduled, ${dropped} dropped`, + `${rescheduled} rescheduled, ${dropped} dropped`, ); } return { attempted, delivered, rescheduled, dropped }; diff --git a/apps/backend/src/utils/email/sendEmail.ts b/apps/backend/src/utils/email/sendEmail.ts new file mode 100644 index 00000000..9a1fe861 --- /dev/null +++ b/apps/backend/src/utils/email/sendEmail.ts @@ -0,0 +1,64 @@ +/** + * utils/email/sendEmail.ts + * + * Small, self-contained email-sending utility using nodemailer. + * Added as a new capability alongside the existing in-app-only + * notifications.service.ts — does not modify any existing logic. + * + * Usage: + * import { sendEmail } from '../utils/email/sendEmail.js'; + * await sendEmail({ + * to: 'user@example.com', + * subject: 'Your question was answered', + * text: 'Someone replied to your question on Yaksha FAQ.', + * }); + */ +import nodemailer from 'nodemailer'; +import { logger } from '../http/logger.js'; + +interface SendEmailInput { + to: string; + subject: string; + text: string; + html?: string; +} + +let transporter: nodemailer.Transporter | null = null; + +function getTransporter(): nodemailer.Transporter | null { + if (!process.env.MAIL_HOST || !process.env.MAIL_USER || !process.env.MAIL_PASS) { + logger.warn('[email] MAIL_HOST/MAIL_USER/MAIL_PASS not fully configured — email sending disabled.'); + return null; + } + if (!transporter) { + transporter = nodemailer.createTransport({ + host: process.env.MAIL_HOST, + port: Number(process.env.MAIL_PORT ?? 2525), + auth: { + user: process.env.MAIL_USER, + pass: process.env.MAIL_PASS, + }, + }); + } + return transporter; +} + +export async function sendEmail(input: SendEmailInput): Promise { + const t = getTransporter(); + if (!t) return false; + + try { + await t.sendMail({ + from: '"Yaksha FAQ Portal" ', + to: input.to, + subject: input.subject, + text: input.text, + html: input.html, + }); + logger.info(`[email] sent to ${input.to}: ${input.subject}`); + return true; + } catch (err) { + logger.error(`[email] failed to send to ${input.to}: ${(err as Error).message}`); + return false; + } +} \ No newline at end of file diff --git a/apps/backend/src/utils/http/notificationDispatcher.ts b/apps/backend/src/utils/http/notificationDispatcher.ts index 57673bf2..dbb7f71f 100644 --- a/apps/backend/src/utils/http/notificationDispatcher.ts +++ b/apps/backend/src/utils/http/notificationDispatcher.ts @@ -91,6 +91,11 @@ interface DispatchOptions { * When omitted a sensible default is derived from eventType. */ title?: string; + /** + * Optional custom message override (e.g. real question title + answer). + * Falls back to random text-bank message when omitted. + */ + message?: string; } /** @@ -107,17 +112,18 @@ export const dispatchNotification = async ({ eventType, link, title, + message, }: DispatchOptions): Promise => { const bank = notificationTextBank[eventType]; if (!bank || bank.length === 0) return; // Unknown eventType — no-op silently - const message = bank[Math.floor(Math.random() * bank.length)]; + const finalMessage = message ?? bank[Math.floor(Math.random() * bank.length)]; await notificationsService.dispatch({ recipientId, eventType, link, title, - message, + message: finalMessage, }); }; diff --git a/apps/frontend/src/admin/pages/AdminCommunity.tsx b/apps/frontend/src/admin/pages/AdminCommunity.tsx index 40dcbf1c..0bd5b72c 100644 --- a/apps/frontend/src/admin/pages/AdminCommunity.tsx +++ b/apps/frontend/src/admin/pages/AdminCommunity.tsx @@ -47,6 +47,7 @@ export default function AdminCommunity() { const [categoryFilter, setCategoryFilter] = useState(''); const [toast, setToast] = useState(null); const [viewPost, setViewPost] = useState(null); + const [promoting, setPromoting] = useState(null); const debouncedSearch = useDebounce(search, 350); const showToast = (msg: string, type: Toast['type'] = 'success') => { setToast({ msg, type }); setTimeout(() => setToast(null), 3000); }; @@ -67,6 +68,21 @@ export default function AdminCommunity() { useEffect(() => { fetchPosts(); }, [fetchPosts]); useEffect(() => { setPage(1); }, [debouncedSearch, statusFilter, categoryFilter]); const handleDelete = async (id: string) => { if (!confirm('Delete this post?')) return; try { await adminApi.delete(`/admin/community/${id}`); showToast('Deleted', 'error'); fetchPosts(); } catch { showToast('Delete failed', 'error'); } }; + const handlePromote = async (post: CommunityPost) => { + if (!post.answer?.trim()) { showToast('This post has no answer yet', 'warn'); return; } + if (!confirm(`Promote "${post.title}" to FAQ now? This publishes it immediately — no review needed.`)) return; + setPromoting(post._id); + try { + await adminApi.post(`/admin/community/${post._id}/promote-to-faq`); + showToast('Promoted to FAQ', 'success'); + setViewPost(null); + fetchPosts(); + } catch (err: any) { + showToast(err?.response?.data?.message || 'Promote failed', 'error'); + } finally { + setPromoting(null); + } + }; return (
@@ -195,6 +211,16 @@ export default function AdminCommunity() { e.stopPropagation()}>
+ {post.answer?.trim() && ( + + )}
@@ -259,6 +285,15 @@ export default function AdminCommunity() { {new Date(viewPost.createdAt).toLocaleString('en-IN')}
+ {viewPost.answer?.trim() && ( + + )}
@@ -267,4 +302,4 @@ export default function AdminCommunity() { ); -} +} \ No newline at end of file diff --git a/apps/frontend/src/components/community/CreatePostDialog.tsx b/apps/frontend/src/components/community/CreatePostDialog.tsx index 2a94008c..4852f97f 100644 --- a/apps/frontend/src/components/community/CreatePostDialog.tsx +++ b/apps/frontend/src/components/community/CreatePostDialog.tsx @@ -103,7 +103,8 @@ interface CreatePostDialogProps { prefillTitle?: string; } -export default function CreatePostDialog({ onClose, onCreated, prefillTitle = '' }: CreatePostDialogProps) { + + export default function CreatePostDialog({ onClose, onCreated, prefillTitle = '' }: CreatePostDialogProps) { const { user } = useAuth(); const { openModal } = useAuthModal(); // Guard: if rendered without an authenticated user, close the dialog and @@ -150,27 +151,12 @@ export default function CreatePostDialog({ onClose, onCreated, prefillTitle = '' setAttachments((prev) => prev.filter((a) => a.objectPath !== objectPath)); }; - // Restore draft from sessionStorage on mount - const [title, setTitle] = useState(() => { - try { - const draft = sessionStorage.getItem(DRAFT_KEY); - if (draft) { - const { t } = JSON.parse(draft); - return t || prefillTitle || ''; - } - } catch { void 0 } - return prefillTitle || ''; - }); - const [body, setBody] = useState(() => { - try { - const draft = sessionStorage.getItem(DRAFT_KEY); - if (draft) { - const { b } = JSON.parse(draft); - return b || ''; - } - } catch { void 0 } - return ''; - }); + // Restore draft from localStorage on mount + //My code Start + const [title, setTitle] = useState(''); + const [body, setBody] = useState(''); + console.log("Component rendered", title, body); + //My code end const [loading, setLoading] = useState(false); // Synchronous re-entry guard. The button's `disabled` prop already // prevents the second click once React re-renders, but between the @@ -191,21 +177,54 @@ export default function CreatePostDialog({ onClose, onCreated, prefillTitle = '' // Toast state const [toast, setToast] = useState<{ msg: string; type: 'success' | 'warn' | 'info' } | null>(null); +// My code start + const [showDraftPrompt, setShowDraftPrompt] = useState(false); +// My code end const showToast = (msg: string, type: 'success' | 'warn' | 'info' = 'info') => { setToast({ msg, type }); setTimeout(() => setToast(null), 3500); }; +// My code start +// Check for existing draft when dialog opens + useEffect(() => { + try { + const draft = localStorage.getItem(DRAFT_KEY); + if (draft) { + const parsed = JSON.parse(draft); + + if ( + (parsed.t && parsed.t.trim() !== '') || + (parsed.b && parsed.b.trim() !== '') + ) { + setShowDraftPrompt(true); + } + } + } catch (err) { + console.error('Failed to load draft:', err); + } +}, []); +// My code end + // Save draft on field changes const handleTitleChange = (val: string) => { - setTitle(val); - try { sessionStorage.setItem(DRAFT_KEY, JSON.stringify({ t: val, b: body })); } catch { void 0 } - }; - const handleBodyChange = (val: string) => { - setBody(val); - try { sessionStorage.setItem(DRAFT_KEY, JSON.stringify({ t: title, b: val })); } catch { void 0 } - }; + setTitle(val); + try { + localStorage.setItem(DRAFT_KEY, JSON.stringify({ t: val, b: body })); + } catch {} +}; + +const handleBodyChange = (val: string) => { + setBody(val); + try { + localStorage.setItem(DRAFT_KEY, JSON.stringify({ t: title, b: val })); + } catch {} +}; + //My Code starts + // Auto-save draft whenever title or body changes + + //My Code End useEffect(() => { const dialog = dialogRef.current; @@ -232,6 +251,10 @@ export default function CreatePostDialog({ onClose, onCreated, prefillTitle = '' } return () => dialog.removeEventListener('close', handleClose); }, [onClose]); + //My code Start + // Show draft prompt when dialog opens + + //MY code end useEffect(() => { if (duplicateCheckTimerRef.current) clearTimeout(duplicateCheckTimerRef.current); @@ -308,7 +331,7 @@ export default function CreatePostDialog({ onClose, onCreated, prefillTitle = '' { headers: { 'Idempotency-Key': idempotencyKey } }, ); // Clear draft on success - try { sessionStorage.removeItem(DRAFT_KEY); } catch { void 0 } + try { localStorage.removeItem(DRAFT_KEY); } catch { void 0 } // Show toast with duplicate check result const dupCount = duplicateMatch?.matches?.length ?? 0; if (dupCount > 0) { @@ -349,15 +372,17 @@ export default function CreatePostDialog({ onClose, onCreated, prefillTitle = '' // The "+" pick-file button is already disabled while attaching // (so the user can see the spinner), but the submit button was // not — this commit closes that gap. - const isSubmitDisabled = - !title.trim() || - !body.trim() || - tags.length === 0 || - hasHighConfidenceFaqMatch || - checkingDuplicates || - loading || - attaching; +console.log("title =", title, typeof title); +console.log("body =", body, typeof body); +const isSubmitDisabled = + !(title ?? "").trim() || + !(body ?? "").trim() || + tags.length === 0 || + hasHighConfidenceFaqMatch || + checkingDuplicates || + loading || + attaching; return ( {error}

)} +// My code Start +{showDraftPrompt && ( +
+
+

+ Keep this draft? +

+ +

+ We saved your progress automatically. +

+ +
+ + + + +
+
+
+)} +//My code ends {toast && (
(null); @@ -131,6 +131,9 @@ export default function ThreadDetail({ postId, onClose }: ThreadDetailProps) { const isPrivileged = userRole === 'admin' || userRole === 'moderator'; const topLevelComments = post?.comments ?? []; + const postAuthorId = typeof post?.author === 'string' ? post.author : post?.author?._id; + const isPostAuthor = currentUserId !== '' && currentUserId === postAuthorId; + /** Safe navigation: external URLs open in new tab, internal ones use SPA routing. */ const navigateTo = (url: string) => { if (url.startsWith('http') || url.startsWith('//')) { @@ -429,9 +432,10 @@ export default function ThreadDetail({ postId, onClose }: ThreadDetailProps) { /> {/* Author / privileged actions */} - {(currentUserId === post.author?._id || isPrivileged) && ( + {(isPostAuthor || isPrivileged) && ( <>
+ {isPrivileged && ( + )} {!isEditing && (
{loading && ( @@ -68,14 +99,16 @@ export default function SearchDropdown({
)) )} - {!loading && items.length === 0 && ( + {!loading && displayItems.length === 0 && (

- No matches yet. Keep typing or browse a category. + {activeCategories.length > 0 + ? "No matches found in the selected categories." + : "No matches yet. Keep typing or browse a category."}

)} - {!loading && items.map((item, idx) => ( + {!loading && displayItems.map((item, idx) => (
- -
- {/* 1.12 (LOW) — empty-state for the categories column when +{/* 1.12 (LOW) — empty-state for the categories column when no categories are available. Merged with PR #144's style_config.ts refactor: the `group` Tailwind class on the button below is required for the icon's `group-hover:opacity-100` to actually fire. */} -

- Categories -

+
+
+

Categories

+ {activeCategories.length > 0 && ( + + )} +
+ {categories.length === 0 ? (

@@ -109,21 +150,42 @@ export default function SearchDropdown({

) : (
- {categories.slice(0, 7).map((name) => ( - - ))} + {categories.slice(0, 7).map((name) => { + const isActive = activeCategories.includes(name); + + return ( + + ); + })}
)}
+ {!loading && displayItems.length > 0 && ( +
+ +
+ )} + ); -} +} \ No newline at end of file diff --git a/apps/frontend/src/components/faq/SearchFeedback.tsx b/apps/frontend/src/components/faq/SearchFeedback.tsx index 31f8e8b9..c21665d4 100644 --- a/apps/frontend/src/components/faq/SearchFeedback.tsx +++ b/apps/frontend/src/components/faq/SearchFeedback.tsx @@ -86,24 +86,40 @@ export default function SearchFeedback({ searchQuery, resultFaqId }: SearchFeedb
{phase === 'prompt' ? ( -
-

Did this answer your question?

- - -
+
+

+ Did this answer your question? +

+ +
+ + + +
+
) : (
diff --git a/apps/frontend/src/components/search/SearchArea.tsx b/apps/frontend/src/components/search/SearchArea.tsx new file mode 100644 index 00000000..11764f60 --- /dev/null +++ b/apps/frontend/src/components/search/SearchArea.tsx @@ -0,0 +1,190 @@ +/* +Handles 'SearchBar' component and 'SearchDropdown' simultaneously. +Adds darker background to avoid overlapping with background. +Filter features to help a better search +*/ + +import React, { useState, useEffect } from 'react'; +import SearchBar from '../../components/search/SearchBar'; +import SearchDropdown from '../../components/faq/SearchDropdown'; +import { FAQItem } from '../../components/faq/faqUtils'; +import type { SearchResult } from '../../types/ui'; + +interface SearchAreaProps { + searchQuery: string; + placeholder?: string; + onSearchChange: (value: string) => void; + onSubmit?: (query: string) => void; + onClear: () => void; + onResults?: (results: SearchResult[] | null) => void; + onLoading?: (loading: boolean) => void; + onError?: (error: string | null) => void; + dropdownItems?: any[]; + categories?: string[]; + onSelectQuestion?: (item: FAQItem) => void; + onSelectCategory?: (name: string) => void; + searchLoading?: boolean; + children?: React.ReactNode; +} + +export default function SearchArea({ + searchQuery, + placeholder = "Search...", + onSearchChange, + onSubmit, + onClear, + onResults, + onLoading, + onError, + dropdownItems = [], + categories = [], + onSelectQuestion, + onSelectCategory, + searchLoading = false, + children +}: SearchAreaProps) { + const [isDropdownOpen, setIsDropdownOpen] = useState(false); + + const [activeTab, setActiveTab] = useState<'faq' | 'all' | 'community'>('all'); + + useEffect(() => { + if (searchQuery.trim().length > 0) { + setIsDropdownOpen(true); + } else { + setIsDropdownOpen(false); + setActiveTab('all'); + } + }, [searchQuery]); + + const handleClear = () => { + setIsDropdownOpen(false); + setActiveTab('all'); + onClear(); + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter') { + e.preventDefault(); + setIsDropdownOpen(false); + if (onSubmit) onSubmit(searchQuery.trim()); + } + }; + + + const filteredDropdownItems = dropdownItems.filter((item) => { + if (activeTab === 'all') return true; + + const itemSource = item.source || 'faq'; + return itemSource === activeTab; + }); + + return ( +
+ {/* The Global Backdrop */} + {isDropdownOpen && ( +
setIsDropdownOpen(false)} + aria-hidden="true" + /> + )} + + {/* Elevated Container */} +
+ + {isDropdownOpen && ( +
+
+ + + + + + +
+
+ )} + + {/* The Search Input */} + {})} + onLoading={onLoading || (() => {})} + onError={onError || (() => {})} + placeholder={placeholder} + disableSuggestions={true} + onSubmit={(q) => { + setIsDropdownOpen(false); + if (onSubmit) onSubmit(q); + }} + onClear={handleClear} + /> + {/* The Dropdown Panel */} + {isDropdownOpen && ( + { + setIsDropdownOpen(false); + if (onSelectQuestion) onSelectQuestion(item); + }} + onSelectCategory={(cat) => { + setIsDropdownOpen(false); + if (onSelectCategory) onSelectCategory(cat); + }} + onClear={handleClear} + loading={searchLoading} + /> + )} +
+ + {/* Render the full page results underneath when dropdown is CLOSED */} + {!isDropdownOpen && children} +
+ ); +} \ No newline at end of file diff --git a/apps/frontend/src/components/search/SearchBar.tsx b/apps/frontend/src/components/search/SearchBar.tsx index 1a13a061..780057f1 100644 --- a/apps/frontend/src/components/search/SearchBar.tsx +++ b/apps/frontend/src/components/search/SearchBar.tsx @@ -31,6 +31,8 @@ interface SearchBarProps { className?: string; disableSuggestions?: boolean; variant?: 'default' | 'compact'; + onSubmit?: (query: string) => void; + onClear?: () => void; } const SearchBar = React.forwardRef(function SearchBar( @@ -46,6 +48,8 @@ const SearchBar = React.forwardRef(function Se className = '', disableSuggestions = false, variant = 'default', + onSubmit, + onClear, }, ref ) { @@ -60,10 +64,7 @@ const SearchBar = React.forwardRef(function Se const query = isControlled ? (value ?? '') : internalQuery; const suggestDebounceRef = useRef | null>(null); const searchDebounceRef = useRef | null>(null); - // 1.6 — tracks the suggestionError auto-dismiss timer so we can - // clear it on the next click / unmount. const suggestErrorTimerRef = useRef | null>(null); - const wrapperRef = useRef(null); const handleSearch = async (searchQuery: string) => { @@ -82,9 +83,7 @@ const SearchBar = React.forwardRef(function Se }); onResults(res.data.results ?? null); } catch (err: any) { - if (axios.isCancel(err)) { - return; // Ignore cancelled requests - } + if (axios.isCancel(err)) return; onResults([]); onError?.('Search failed. Please check your connection and try again.'); } finally { @@ -106,11 +105,11 @@ const SearchBar = React.forwardRef(function Se setSuggestions([]); } }; - - // v2 — Suggestions stay live as the user types (250ms debounce). Search +// v2 — Suggestions stay live as the user types (250ms debounce). Search // results also stream live as the user types (300ms debounce) — but they // appear INSIDE the glassmorphic dropdown bubble on the host page, not as // a page swap. Enter skips the wait and fires immediately. + const handleChange = (e: ChangeEvent) => { const val = e.target.value; if (isControlled) { @@ -119,19 +118,15 @@ const SearchBar = React.forwardRef(function Se setInternalQuery(val); } - // Live suggestions under the input. if (!disableSuggestions) { if (suggestDebounceRef.current) clearTimeout(suggestDebounceRef.current); suggestDebounceRef.current = setTimeout(() => fetchSuggestions(val), 250); } - // Live results — same source the post-Enter flow uses, so the - // dropdown and the in-page panel can never disagree on counts. if (searchDebounceRef.current) clearTimeout(searchDebounceRef.current); if (val.trim().length >= 3) { searchDebounceRef.current = setTimeout(() => handleSearch(val), 300); } else { - // Below threshold — wipe results so the dropdown's empty state shows. onResults(null); onError?.(null); } @@ -141,20 +136,20 @@ const SearchBar = React.forwardRef(function Se if (searchDebounceRef.current) clearTimeout(searchDebounceRef.current); setShowSuggestions(false); handleSearch(query); + onSubmit?.(query); }; const handleSubmit = (e: FormEvent) => { e.preventDefault(); runSearchNow(); }; - + // 1.6 (LOW) — clear any stale suggestionError on every click so it + // doesn't linger indefinitely if the user stopped typing. The + // 4-second auto-dismiss below still applies for fresh errors. const handleSuggestionClick = async (faqId: string) => { setShowSuggestions(false); setSuggestions([]); setSuggestionError(null); - // 1.6 (LOW) — clear any stale suggestionError on every click so it - // doesn't linger indefinitely if the user stopped typing. The - // 4-second auto-dismiss below still applies for fresh errors. if (suggestErrorTimerRef.current) { clearTimeout(suggestErrorTimerRef.current); suggestErrorTimerRef.current = null; @@ -174,8 +169,6 @@ const SearchBar = React.forwardRef(function Se navigate(`/faq/${faqId}`); }; - // 1.6 — clear pending auto-dismiss timer on unmount so we don't - // try to setState after the component is gone. useEffect(() => { return () => { if (suggestErrorTimerRef.current) { @@ -185,10 +178,8 @@ const SearchBar = React.forwardRef(function Se }; }, []); - // Close suggestions on outside click const handleBlur = (e: React.FocusEvent) => { onBlur?.(); - // Delay so click on suggestion registers first setTimeout(() => { if (wrapperRef.current && !wrapperRef.current.contains(document.activeElement)) { setShowSuggestions(false); @@ -219,10 +210,35 @@ const SearchBar = React.forwardRef(function Se onFocus={onFocus} onBlur={handleBlur} placeholder={placeholder} - className={variant === 'compact' ? searchInputCompact : searchInputDefault} + className={`${variant === 'compact' ? searchInputCompact : searchInputDefault} ${variant === 'default' ? 'pr-36' : 'pr-10'}`} autoComplete="off" /> + {/*Cross Mark (Clear Button) */} + {query.trim().length > 0 && ( + + )} + {variant === 'default' && (
)} - {/* Suggestion click error */} {suggestionError && (
{suggestionError} diff --git a/apps/frontend/src/pages/CommunityPage.tsx b/apps/frontend/src/pages/CommunityPage.tsx index eaf00ecb..07e2aaea 100644 --- a/apps/frontend/src/pages/CommunityPage.tsx +++ b/apps/frontend/src/pages/CommunityPage.tsx @@ -16,13 +16,9 @@ import CreatePostDialog from '../components/community/CreatePostDialog'; import { buttonCommunityAsk } from '../styles/style_config'; // ─── Main Page ──────────────────────────────────────────────────────────────── + export default function CommunityPage() { const { user } = useAuth(); - // Active program from ProgramContext — used to scope the community feed - // to the chosen program so summership questions don't leak into - // winternship (and vice versa). Falls back to `undefined` when no - // program is selected yet, in which case we deliberately send NO - // batchId (matches previous behaviour while the picker takes over). const { currentBatch } = useBatch(); const activeBatchId = currentBatch?._id ?? undefined; const gate = useAuthGate(); @@ -30,6 +26,7 @@ export default function CommunityPage() { () => setShowCreate(true), 'Sign in to ask a question in the community.' ); + const [posts, setPosts] = useState([]); const [total, setTotal] = useState(0); const [nextCursor, setNextCursor] = useState(null); @@ -43,6 +40,7 @@ export default function CommunityPage() { const [filter, setFilter] = useState('all'); const [sort, setSort] = useState('newest'); const [showAllPrograms, setShowAllPrograms] = useState(false); + const [search, setSearch] = useState(() => { const params = new URLSearchParams(window.location.search); return params.get('search') || ''; @@ -55,24 +53,55 @@ export default function CommunityPage() { const [showCreate, setShowCreate] = useState(false); const [createPrefillTitle, setCreatePrefillTitle] = useState(''); - // Backend uses cursor-based pagination. The previous version sent `?page=2` - // which the backend silently ignored — so every "Load more" call returned - // the FIRST batch and we got duplicates. Send the cursor instead. - // - // v1.69 — program scoping: always send the active program's batchId - // when the user is NOT on "All Programs Feed". Without it, the - // backend returns every post across every program because the - // community routes don't have the programScope middleware attached + // ── Keyboard Shortcut & Recent Searches ────────────────────────────────── + const searchInputRef = useRef(null); + const [recentSearches, setRecentSearches] = useState(() => { + try { + return JSON.parse(localStorage.getItem('csfaq_recent_searches') || '[]'); + } catch { + return []; + } + }); + + const saveToRecent = (term: string) => { + const q = term.trim(); + if (!q || q.length < 2) return; + setRecentSearches((prev) => { + const updated = [q, ...prev.filter((item) => item.toLowerCase() !== q.toLowerCase())].slice(0, 4); + localStorage.setItem('csfaq_recent_searches', JSON.stringify(updated)); + return updated; + }); + }; + + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if ( + (e.key === '/' || ((e.metaKey || e.ctrlKey) && e.key === 'k')) && + document.activeElement?.tagName !== 'INPUT' && + document.activeElement?.tagName !== 'TEXTAREA' + ) { + e.preventDefault(); + searchInputRef.current?.focus(); + } + }; + window.addEventListener('keydown', handleKeyDown); + return () => window.removeEventListener('keydown', handleKeyDown); + }, []); +// community routes don't have the programScope middleware attached // and the controllers fall back to "no batchId filter". The // explicit-`undefined`-when-no-program-selected case preserves the // legacy behaviour while the program picker takes over. + // + // "mine" is a client-only filter value (see displayedPosts below) — + // the backend only understands all/answered/unanswered/open, so we + // translate it to 'all' before sending and filter by author locally. const fetchPosts = useCallback((reset = false) => { if (reset) setLoading(true); else setLoadingMore(true); api.get('/community', { params: { limit: 20, - filter, + filter: filter === 'mine' ? 'all' : filter, sort, batchId: showAllPrograms ? 'all' : activeBatchId, ...(reset ? {} : nextCursor ? { cursor: nextCursor } : {}), @@ -80,7 +109,7 @@ export default function CommunityPage() { }) .then((res) => { const incoming = res.data.posts || []; - setPosts((prev) => reset ? incoming : [...prev, ...incoming]); + setPosts((prev) => (reset ? incoming : [...prev, ...incoming])); setTotal(res.data.total || 0); setHasMore(res.data.hasMore ?? false); setNextCursor(res.data.nextCursor ?? null); @@ -101,10 +130,7 @@ export default function CommunityPage() { useEffect(() => { const params = new URLSearchParams(window.location.search); - // ?ask=true — open the create dialog. The navbar's "Ask Question" button - // already gates this behind the auth modal, so by the time we get here - // (after the gate's pending-action replay) the user is authenticated. - // We still double-check user here as a safety net. + // ?ask=true — open the create dialog. if (params.get('ask') === 'true') { if (user) { const prefilledTitle = params.get('title') || ''; @@ -112,8 +138,6 @@ export default function CommunityPage() { setShowCreate(true); window.history.replaceState({}, '', window.location.pathname); } else { - // Not authenticated — clear the param so the gate can re-trigger it - // after login if the user chooses to sign in. window.history.replaceState({}, '', window.location.pathname); } } @@ -125,59 +149,45 @@ export default function CommunityPage() { if (found) { setSelectedPostId(postId); } else { - // Post not in list — fetch individually api.get(`/community/${postId}`) .then((res) => { const post = res.data; if (post && post._id) { setSelectedPostId(postId); - // Prepend to posts so it's in cache - setPosts(prev => [post, ...prev]); + setPosts((prev) => [post, ...prev]); } }) - .catch(() => { - // Post not found or access denied — silently fail, don't open thread - }); + .catch(() => {}); } window.history.replaceState({}, '', window.location.pathname); } - }, [posts, user, window.location.search]); + }, [posts, user]); - // Reset cursor + posts when filter/sort changes so we paginate the - // newly-filtered set from the beginning. activeBatchId is also in this - // list — switching programs in the header must wipe the visible feed. + // Reset cursor + posts when filter/sort changes so we paginate the newly-filtered set useEffect(() => { setNextCursor(null); setPosts([]); }, [filter, sort, showAllPrograms, activeBatchId]); - // 2-D (MEDIUM) — previously this page had TWO effects both keyed on - // [filter, sort, ...] that each fired `fetchPosts(true)` in the same - // React commit, racing against each other. The earlier one (above) - // handled showAllPrograms, this one below handled the search-active - // branch. Merge them into one effect whose body is the union of - // both branches, removing the duplicate dispatch. // When filter or sort changes — refresh posts (if no search active) or re-filter existing results useEffect(() => { if (search.trim()) { - // Search is active — re-apply filter/sort client-side to existing searchResults - setSearchResults(prev => { + setSearchResults((prev) => { if (!prev.length) return prev; let filtered = [...prev]; - if (filter === 'answered') filtered = filtered.filter(p => p.status === 'answered'); - else if (filter === 'unanswered') filtered = filtered.filter(p => p.status === 'unanswered'); + if (filter === 'answered') filtered = filtered.filter((p) => p.status === 'answered'); + else if (filter === 'unanswered') filtered = filtered.filter((p) => p.status === 'unanswered'); if (sort === 'newest') filtered.sort((a, b) => new Date(b.createdAt ?? 0).getTime() - new Date(a.createdAt ?? 0).getTime()); else if (sort === 'oldest') filtered.sort((a, b) => new Date(a.createdAt ?? 0).getTime() - new Date(b.createdAt ?? 0).getTime()); - else if (sort === 'popular') filtered.sort((a, b) => ((b.upvotes?.length ?? 0)) - ((a.upvotes?.length ?? 0))); - else if (sort === 'discussed') filtered.sort((a, b) => ((b.comments?.length ?? 0)) - ((a.comments?.length ?? 0))); + else if (sort === 'popular') filtered.sort((a, b) => (b.upvotes?.length ?? 0) - (a.upvotes?.length ?? 0)); + else if (sort === 'discussed') filtered.sort((a, b) => (b.comments?.length ?? 0) - (a.comments?.length ?? 0)); return filtered; }); return; } fetchPosts(true); }, [filter, sort, showAllPrograms, activeBatchId]); - - // ── Infinite scroll — fetch the next page when the sentinel enters view ──── +// ── Infinite scroll — fetch the next page when the sentinel enters view ──── const sentinelRef = useRef(null); useEffect(() => { if (!sentinelRef.current) return; @@ -189,11 +199,11 @@ export default function CommunityPage() { fetchPosts(false); } }, - { rootMargin: '300px 0px' } // start loading 300px before the sentinel + { rootMargin: '300px 0px' } ); observer.observe(sentinelRef.current); return () => observer.disconnect(); - }, [hasMore, loading, loadingMore, nextCursor, filter, sort, showAllPrograms, activeBatchId]); + }, [hasMore, loading, loadingMore, nextCursor, filter, sort, showAllPrograms, activeBatchId, fetchPosts]); const runSemanticSearch = useCallback(async (q: string) => { setSearchLoading(true); @@ -210,10 +220,6 @@ export default function CommunityPage() { } }, [showAllPrograms, activeBatchId]); - // v2 — search is now Enter-only. We still keep the trimmed query handy - // for downstream effects (filter/sort re-apply on existing results). - // The auto-fire debounce was removed so results don't pop while the user - // is still typing. useEffect(() => { const q = search.trim(); if (!q || q.length < 3) { @@ -243,7 +249,7 @@ export default function CommunityPage() { const handleShareCommunity = async () => { const url = window.location.origin + '/csfaq/community'; - try { await navigator.clipboard.writeText(url); } catch { /* ignore */ } + try { await navigator.clipboard.writeText(url); } catch {} setToast('Community link copied'); setTimeout(() => setToast(''), 2500); }; @@ -255,36 +261,65 @@ export default function CommunityPage() { }; const visible = (() => { - if (search.trim()) return searchResults; + let list = posts; + + if (search.trim()) { + const q = search.toLowerCase().trim(); + const localFiltered = posts.filter( + (p) => + p.title?.toLowerCase().includes(q) || + p.content?.toLowerCase().includes(q) || + p.author?.name?.toLowerCase().includes(q) + ); + list = searchResults.length > 0 ? searchResults : localFiltered; + } - return [...posts].sort((a, b) => { + return [...list].sort((a, b) => { if (sort === 'newest') return new Date(b.createdAt ?? 0).getTime() - new Date(a.createdAt ?? 0).getTime(); - if (sort === 'oldest') return new Date(a.createdAt ?? 0).getTime() - new Date(b.createdAt ?? 0).getTime(); - if (sort === 'popular') return ((b.upvotes?.length ?? 0)) - ((a.upvotes?.length ?? 0)); - if (sort === 'discussed') return ((b.comments?.length ?? 0)) - ((a.comments?.length ?? 0)); + if (sort === 'oldest') return new Date(a.createdAt ?? 0).getTime() - new Date(a.createdAt ?? 0).getTime(); + if (sort === 'popular') return (b.upvotes?.length ?? 0) - (a.upvotes?.length ?? 0); + if (sort === 'discussed') return (b.comments?.length ?? 0) - (a.comments?.length ?? 0); return 0; }); })(); + // "My Questions" — client-side filter matched against the logged-in + // user's id. Post shape isn't fully known here, so check a few common + // author-field names defensively. Confirm the real field against your + // Post type / API response and trim this down to the one that's real. + const isMyPost = (p: Post) => + !!user && ( + (p as any).authorId === user._id || + (p as any).userId === user._id || + (p as any).author?._id === user._id + ); + const displayedPosts = filter === 'all' ? visible - : visible.filter((p) => p.status === filter); + : filter === 'mine' + ? visible.filter(isMyPost) + : visible.filter((p) => p.status === filter); const answeredCount = posts.filter((p) => p.status === 'answered').length; const unansweredCount = posts.filter((p) => p.status !== 'answered').length; - return (
+ {/* Small drop-in keyframe for the post-list fade/slide-in. + Scoped with a unique class name so it can't clash with + anything else in the app; no tailwind.config changes needed. */} + +
- {/* v1.69 — Phase 12: persistent "browsing program" pill - so the user always knows which program's community - feed they're scrolling. The pill reads from - BatchContext. The actual data fetch below already - uses currentBatch._id for the ?batchId=... filter - — this commit is a UX improvement, not a backend - change. */}
@@ -348,34 +383,86 @@ export default function CommunityPage() {
- - {!loading && total > 0 && ( -
+ {/* Search Bar, Quick Tags, Recent Searches, and Results Counter */} +
+
setSearch(e.target.value)} onKeyDown={(e) => { - if (e.key === 'Enter' && !e.shiftKey && search.trim().length >= 3) { - runSemanticSearch(search.trim()); + if (e.key === 'Enter' && search.trim()) { + saveToRecent(search.trim()); + if (!e.shiftKey && search.trim().length >= 3) { + runSemanticSearch(search.trim()); + } } }} - placeholder="Search questions…" - className="w-full pl-9 pr-4 py-2.5 rounded-xl border border-border bg-card text-sm text-ink placeholder-ink-faint focus:outline-none focus:ring-2 focus:ring-accent/25 transition-all" + placeholder="Search community discussions..." + className="w-full pl-9 pr-16 py-2.5 rounded-xl border border-border bg-card text-sm text-ink placeholder-ink-faint focus:outline-none focus:ring-2 focus:ring-accent/25 transition-all" /> + {!search && ( + + / + + )} + {search.trim() && ( + + )} {searchLoading && ( -
+
)}
- )} - {!loading && !error && ( + {/* Quick Clickable Search Tags */} + + + {/* Recent Searches (persisted via localStorage) */} + {recentSearches.length > 0 && !search && ( +
+ Recent: + {recentSearches.map((term) => ( + + ))} +
+ )} + + {/* Search Results Counter & Clear Link */} + {search.trim() && ( +
+ + Showing results for "{search.trim()}" ({displayedPosts.length} found) + + +
+ )} +
+ + {!loading && !error && (
@@ -383,6 +470,7 @@ export default function CommunityPage() { { key: 'all', label: 'All' }, { key: 'unanswered', label: 'Unanswered' }, { key: 'open', label: 'Open' }, + ...(user ? [{ key: 'mine', label: 'My Questions' }] : []), ].map(({ key, label }) => ( + )} +
)} {!loading && !searchLoading && !error && displayedPosts.length > 0 && ( -
+
{displayedPosts.map((post) => ( - {/* Thread detail — full-page overlay replaces the list view. - z-30 (below the navbar's z-50) so the navbar floats on top of - the page-cover background. The inner modal at z-[60] escapes - this stacking context via its higher z-index and sits above - the navbar. */} {selectedPostId && (
); -} +} \ No newline at end of file diff --git a/apps/frontend/src/pages/FAQPage.tsx b/apps/frontend/src/pages/FAQPage.tsx index 8f154e79..bf12ee26 100644 --- a/apps/frontend/src/pages/FAQPage.tsx +++ b/apps/frontend/src/pages/FAQPage.tsx @@ -11,7 +11,7 @@ import React, { useEffect, useMemo, useRef, useState, useCallback } from 'react' import { useNavigate, useParams, useSearchParams } from 'react-router-dom'; import Footer from '../components/layout/Footer'; import UserActiveProgramIndicator from '../components/layout/UserActiveProgramIndicator'; -import SearchBar from '../components/search/SearchBar'; +import SearchArea from '@/components/search/SearchArea'; import { HomeDoodles } from '../components/ui/PageDoodles'; import api from '../utils/api'; import type { TrendingQuery } from '../types/ui'; @@ -320,31 +320,21 @@ export default function FAQPage() { {/* ─── SEARCH BAR ───────────────────────────────────────────── */} -
-
- setSearchResults(res as unknown as FAQItem[])} - onLoading={setSearchLoading} - onError={(err) => setError(err || '')} - placeholder="Ask anything about your internship..." - disableSuggestions={true} - /> - - {showDropdown && ( - - )} -
+
+ setSearchResults(res as unknown as FAQItem[])} + onLoading={setSearchLoading} + onError={(err) => setError(err || '')} + onClear={handleClearSearch} + dropdownItems={dropdownItems} + categories={categories} + onSelectQuestion={handleQuestionOpen} + onSelectCategory={handleCategoryOpen} + searchLoading={searchLoading} + > +
{/* ─── CATEGORY FILTER PILLS ─────────────────────────────────── */} @@ -607,9 +597,6 @@ export default function FAQPage() {
- {searchActive && searchResults && searchResults.length > 0 && ( - - )}
); } \ No newline at end of file diff --git a/apps/frontend/src/pages/HomePage.tsx b/apps/frontend/src/pages/HomePage.tsx index f283e178..6148e7c9 100644 --- a/apps/frontend/src/pages/HomePage.tsx +++ b/apps/frontend/src/pages/HomePage.tsx @@ -23,6 +23,7 @@ import React, { useEffect, useMemo, useRef, useState, useCallback } from 'react' import { useNavigate, useParams } from 'react-router-dom'; import Footer from '../components/layout/Footer'; import SearchBar from '../components/search/SearchBar'; +import SearchArea from '@/components/search/SearchArea'; import { HomeDoodles } from '../components/ui/PageDoodles'; import api from '../utils/api'; import { useBatch } from '../context/BatchContext'; @@ -634,31 +635,21 @@ export default function HomePage() { )} {/* ─── SEARCH BAR ─── */} -
-
- setSearchResults(res as unknown as FAQItem[])} - onLoading={setSearchLoading} - onError={(err) => setError(err || '')} - placeholder="Ask anything about your internship..." - disableSuggestions={true} - /> - - {showDropdown && ( - - )} -
+
+ setSearchResults(res as unknown as FAQItem[])} + onLoading={setSearchLoading} + onError={(err) => setError(err || '')} + onClear={handleClearSearch} + dropdownItems={dropdownItems} + categories={categories} + onSelectQuestion={handleQuestionOpen} + onSelectCategory={handleCategoryOpen} + searchLoading={searchLoading} + > +
@@ -926,9 +917,6 @@ export default function HomePage() {
- {searchActive && searchResults && searchResults.length > 0 && ( - - )}
); } \ No newline at end of file diff --git a/apps/frontend/src/pages/InternshipRoadmapPage.tsx b/apps/frontend/src/pages/InternshipRoadmapPage.tsx new file mode 100644 index 00000000..4eede773 --- /dev/null +++ b/apps/frontend/src/pages/InternshipRoadmapPage.tsx @@ -0,0 +1,851 @@ +"use client"; + +import React, { useEffect, useRef, useState, useCallback } from "react"; +import { + motion, + useScroll, + useSpring, + useTransform, + useMotionValueEvent, + AnimatePresence, +} from "framer-motion"; + +// --- Colors & Data Configuration --- +const colors = { + emerald: "#10B981", + crimson: "#EF4444", + amber: "#F59E0B", + cyan: "#06B6D4", + purple: "#A855F7", + board: "#0B132B", +}; + +const portalLinks = { + registration: "https://samagama.in", + discussion: "https://vicharanashala.discourse.group/", + vibe: "https://vibe.vicharanashala.ai/student/course-registration/6a14258a4fa5339bade5d733", + matrix: "https://sudarshansudarshan.github.io/codershigh/matrixmystics/", + matrixEndorsement: "https://samagama.in/spa", + phase1: "https://github.com/vicharanashala/crowd-source-faq", +}; + +const registrationChecklist = [ + "Log in to samagama.in", + "Upload & verify: No Objection Certificate (NOC)", + "Upload & verify: Offer Letter", + "Upload & verify: Participation Agreement", + "Upload & verify: Honor Code", + "Submit your Zoom ID", + "Submit your GitHub ID", +]; + +const standupChecklist = [ + "Join the Zoom meeting using daily email link", + "Attend session using laptop with camera ON", + "Participate in all polls during the session", +]; + +const vibeCourses = ["Onboarding", "Fundamentals of AI", "MERN Stack Development"]; + +const vibeWeeks = [ + { week: "Week 1", items: ["Onboarding", "70% Fundamentals of AI"] }, + { week: "Week 2", items: ["100% Fundamentals of AI", "50% MERN"] }, + { week: "Week 3", items: ["80% MERN"] }, + { week: "Week 4", items: ["100% All Courses"] }, +]; + +const phase1Steps = [ + "Form a team of 10 members", + "Team Lead forks official repository", + "Add all team members as collaborators", + "Develop and test assigned feature", + "Submit Pull Request link on Samagama", +]; + +const phase2Mentors = [ + { project: "ViBe", mentor: "Minakshi Madam" }, + { project: "Spandan", mentor: "Rohit Sir" }, + { project: "PyBe", mentor: "Prakash Sir" }, + { project: "Tenali", mentor: "Jinal Madam" }, + { project: "Spurti", mentor: "Sakshi Madam" }, + { project: "FLN", mentor: "Pavani Madam" }, +]; + +const completionSteps = [ + "Attend daily mentor meetings", + "Develop and present approved features", + "Submit Pull Requests for final sign-off", +]; + +const overviewStops = [ + { label: "Start", dot: colors.emerald, id: "start" }, + { label: "Register", dot: colors.emerald, id: "stop-1" }, + { label: "Stand-up", dot: colors.crimson, id: "stop-2" }, + { label: "ViBe", dot: colors.amber, id: "stop-3" }, + { label: "Matrix", dot: colors.amber, id: "stop-4" }, + { label: "Phase 1", dot: colors.cyan, id: "stop-5" }, + { label: "Phase 2", dot: colors.cyan, id: "stop-6" }, + { label: "Finish", dot: colors.purple, id: "stop-end" }, +]; + +function initials(name: string) { + return name + .replace(/madam|sir/gi, "") + .trim() + .split(/\s+/) + .map((w) => w[0]) + .join("") + .toUpperCase(); +} + +// --- Icons --- +function CheckIcon({ color = "currentColor" }: { color?: string }) { + return ( + + + + ); +} + +function LinkIcon() { + return ( + + + + + ); +} + +// --- Canvas Ocean Wave Background --- +function OceanBackground() { + const canvasRef = useRef(null); + + useEffect(() => { + const canvas = canvasRef.current; + if (!canvas) return; + const ctx = canvas.getContext("2d"); + if (!ctx) return; + + let animId: number; + let width = (canvas.width = window.innerWidth); + let height = (canvas.height = window.innerHeight); + + const handleResize = () => { + if (!canvas) return; + width = canvas.width = window.innerWidth; + height = canvas.height = window.innerHeight; + }; + window.addEventListener("resize", handleResize); + + const particles = Array.from({ length: 35 }).map(() => ({ + x: Math.random() * width, + y: Math.random() * height, + radius: Math.random() * 2.5 + 1, + alpha: Math.random() * 0.5 + 0.1, + speedY: Math.random() * 0.3 + 0.1, + speedX: (Math.random() - 0.5) * 0.2, + })); + + let step = 0; + + const render = () => { + step += 0.006; + ctx.clearRect(0, 0, width, height); + + const grad = ctx.createLinearGradient(0, 0, 0, height); + grad.addColorStop(0, "#020617"); + grad.addColorStop(0.5, "#07172F"); + grad.addColorStop(1, "#0B2545"); + ctx.fillStyle = grad; + ctx.fillRect(0, 0, width, height); + + const waves = [ + { color: "rgba(14, 165, 233, 0.07)", speed: 1, heightOffset: 0.2, length: 0.004, amp: 45 }, + { color: "rgba(6, 182, 212, 0.1)", speed: 1.4, heightOffset: 0.55, length: 0.007, amp: 30 }, + { color: "rgba(59, 130, 246, 0.05)", speed: 0.8, heightOffset: 0.85, length: 0.003, amp: 55 }, + ]; + + waves.forEach((w) => { + ctx.beginPath(); + ctx.fillStyle = w.color; + const waveY = height * w.heightOffset; + ctx.moveTo(0, height); + ctx.lineTo(0, waveY); + + for (let x = 0; x <= width; x += 12) { + const y = + Math.sin(x * w.length + step * w.speed) * w.amp + + Math.cos(x * 0.002 + step * 0.4) * (w.amp * 0.4) + + waveY; + ctx.lineTo(x, y); + } + + ctx.lineTo(width, height); + ctx.closePath(); + ctx.fill(); + }); + + particles.forEach((p) => { + p.y -= p.speedY; + p.x += p.speedX; + if (p.y < 0) p.y = height; + if (p.x < 0) p.x = width; + if (p.x > width) p.x = 0; + + ctx.beginPath(); + ctx.arc(p.x, p.y, p.radius, 0, Math.PI * 2); + ctx.fillStyle = `rgba(56, 189, 248, ${p.alpha})`; + ctx.fill(); + }); + + animId = requestAnimationFrame(render); + }; + + render(); + return () => { + window.removeEventListener("resize", handleResize); + cancelAnimationFrame(animId); + }; + }, []); + + return ; +} + +// --- Interactive Engine Unit --- +function ExpressTrainEngine({ + isMoving, + headlightOn, + onWhistle, + whistleActive, +}: { + isMoving: boolean; + headlightOn: boolean; + onWhistle: () => void; + whistleActive: boolean; +}) { + return ( +
+ {/* Whistle Steam Plume Effect */} + + {whistleActive && ( + + + 💨 CHOON! CHOON! + + + )} + + + {/* Dynamic Motion Smoke Puff */} +
+ {isMoving && ( + <> + + + + )} +
+ + {/* Train Engine Vector Body */} + + + + {/* Headlight Beam Cone */} + {headlightOn && ( + + )} + + {/* Engine Shell */} + + + + + {/* Engine Bonnet */} + + + + + {/* Steam Pipe Chimney */} + + + + {/* Headlight Bulb */} + + + + {/* Wheels / Pistons */} + + + + + + + + + + + + +
+ ); +} + +// --- Glassmorphism Milestone Card --- +function GlassCard({ + accent, + children, + className = "", + glow = false, +}: { + accent?: string; + children: React.ReactNode; + className?: string; + glow?: boolean; +}) { + return ( + +
+
{children}
+ + ); +} + +// --- Eyebrow Tag --- +function Eyebrow({ color, children }: { color: string; children: React.ReactNode }) { + return ( +

+ + {children} +

+ ); +} + +// --- Interactive Link Button --- +function PortalLink({ href, color, label = "Portal Link" }: { href?: string; color: string; label?: string }) { + if (!href) { + return ( +
+ + {label} — Pending Link +
+ ); + } + return ( + + + {label} ↗ + + ); +} + +// --- Main Interactive Roadmap --- +export default function InternshipRoadmapPage() { + const containerRef = useRef(null); + const trackRef = useRef(null); + + const [trainY, setTrainY] = useState(0); + const [isMoving, setIsMoving] = useState(false); + const [headlightOn, setHeadlightOn] = useState(true); + const [whistleActive, setWhistleActive] = useState(false); + const [activeStop, setActiveStop] = useState("start"); + + const scrollTimeoutRef = useRef(null); + + // Native Scroll bindings + const { scrollYProgress } = useScroll({ + target: containerRef, + offset: ["start start", "end end"], + }); + + const smoothProgress = useSpring(scrollYProgress, { + stiffness: 140, + damping: 26, + restDelta: 0.001, + }); + + // Calculate position along railway line + useEffect(() => { + const updatePosition = () => { + if (!trackRef.current) return; + const trackHeight = trackRef.current.getBoundingClientRect().height; + const progress = smoothProgress.get(); + const newY = Math.max(0, Math.min(trackHeight - 80, progress * trackHeight)); + setTrainY(newY); + + const totalStops = overviewStops.length; + const idx = Math.min(totalStops - 1, Math.floor(progress * totalStops)); + setActiveStop(overviewStops[idx].id); + }; + + const unsub = smoothProgress.on("change", () => { + updatePosition(); + setIsMoving(true); + + if (scrollTimeoutRef.current) clearTimeout(scrollTimeoutRef.current); + scrollTimeoutRef.current = setTimeout(() => setIsMoving(false), 160); + }); + + window.addEventListener("resize", updatePosition); + updatePosition(); + + return () => { + unsub(); + window.removeEventListener("resize", updatePosition); + if (scrollTimeoutRef.current) clearTimeout(scrollTimeoutRef.current); + }; + }, [smoothProgress]); + + // Audio / Visual Whistle Trigger + const triggerWhistle = useCallback(() => { + setWhistleActive(true); + setTimeout(() => setWhistleActive(false), 800); + }, []); + + // Keyboard Navigation Controls (Drive train with Arrow Keys or 'W' Whistle) + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === "ArrowDown") { + window.scrollBy({ top: 120, behavior: "smooth" }); + } else if (e.key === "ArrowUp") { + window.scrollBy({ top: -120, behavior: "smooth" }); + } else if (e.key.toLowerCase() === "w") { + triggerWhistle(); + } else if (e.key.toLowerCase() === "h") { + setHeadlightOn((prev) => !prev); + } + }; + window.addEventListener("keydown", handleKeyDown); + return () => window.removeEventListener("keydown", handleKeyDown); + }, [triggerWhistle]); + + const progressPercent = useTransform(smoothProgress, [0, 1], [0, 100]); + const [progressText, setProgressText] = useState("0%"); + + useMotionValueEvent(progressPercent, "change", (v) => { + setProgressText(`${Math.round(v)}%`); + }); + + const scrollToSection = (id: string) => { + const el = document.getElementById(id); + if (el) { + el.scrollIntoView({ behavior: "smooth" }); + } + }; + + return ( +
+ + + {/* Floating HUD Controller Deck */} +
+
+ + +
+ + +
+
+ + {/* Main Roadmap Wrapper */} +
+ + {/* Departure Board Header */} + +
+ + + LINE 01 · AI INTERNSHIP EXPRESS + + KEYBOARD DRIVE ENABLED +
+ +

+ The Internship Express +

+

+ ORIGIN: Registration  →  TERMINUS:{" "} + Internship Completion +

+ + + + {/* Progress Bar */} +
+ PROGRESS +
+ +
+ {progressText} +
+ + {/* Mini Interactive Station Line Map */} +
+ {overviewStops.map((stop, i) => { + const isActive = activeStop === stop.id; + return ( + + ); + })} +
+
+ + {/* Railway Journey Grid */} +
+ + {/* Railway Tracks */} +
+
+
+
+
+
+ + {/* Glowing Active Track */} + +
+ + {/* Interactive Dynamic Train Unit */} +
+ +
+ + {/* START MILESTONE */} +
+ + Platform 01 · Departure +

Internship Journey Begins

+

+ Welcome to the AI Internship. Drive along the tracks to navigate through mandatory setup, daily stand-up requirements, and team projects. +

+ +
+
+ + {/* STOP 1: Registration */} +
+ + Stop 1 · Boarding Pass +

Internship Registration

+

+ Complete all mandatory document uploads and profile verification steps. +

+
    + {registrationChecklist.map((item) => ( +
  • + + + + {item} +
  • + ))} +
+ +
+
+ + {/* STOP 2: Mandatory Stand-ups */} +
+ + Stop 2 · Ticket Inspection +

Mandatory Daily Stand-up Sessions

+

+ Active engagement in daily sessions is necessary to remain eligible for project allocation. +

+ +
+ {[ + { value: "85%", label: "Stand-up Attendance" }, + { value: "85%", label: "Poll Participation" }, + { value: "50%", label: "Poll Accuracy" }, + ].map((m) => ( +
+ {m.value} + {m.label} +
+ ))} +
+ +
    + {standupChecklist.map((item) => ( +
  • + + {item} +
  • + ))} +
+
+
+ + {/* STOP 3: ViBe Learning Platform */} +
+ + Stop 3 · Knowledge Hub +

ViBe Learning Platform

+

+ Complete coursework according to the week-wise plan below: +

+ +
+ {vibeCourses.map((c) => ( + + {c} + + ))} +
+ +
+ {vibeWeeks.map((w) => ( +
+ {w.week} +
    + {w.items.map((it) => ( +
  • + ▸ {it} +
  • + ))} +
+
+ ))} +
+ + +
+
+ + {/* STOP 4: Matrix Mystics */} +
+ + Stop 4 · Peer Assessment +

Matrix Mystics

+ +
+
+ 53 + Math Problems +
+
+ 2 Months + Time Limit +
+
+ +
+ + +
+
+
+ + {/* STOP 5: Phase 1 */} +
+ + Stop 5 · Engineering Sprint +

Phase 1 – Crowd Source FAQ

+

+ Form teams of 10 and submit open-source contributions. +

+ +
+ {phase1Steps.map((step, idx) => ( +
+ + {idx + 1} + + {step} +
+ ))} +
+ + +
+
+ + {/* STOP 6: Phase 2 Mentor Projects */} +
+ + Stop 6 · Industry Mentorship +

Phase 2 – Mentor Projects

+ +
+ + + + + + + + + {phase2Mentors.map((row) => ( + + + + + ))} + +
ProjectMentor
{row.project} + + {initials(row.mentor)} + + {row.mentor} +
+
+
+
+ + {/* FINAL STOP */} +
+ + Final Destination · Completion +

Internship Completion

+

+ Submit PRs approved by your mentor to successfully graduate. +

+ +
    + {completionSteps.map((step) => ( +
  • + + {step} +
  • + ))} +
+
+
+ +
+
+
+ ); +} \ No newline at end of file diff --git a/apps/frontend/src/pages/WelcomePackagePage.tsx b/apps/frontend/src/pages/WelcomePackagePage.tsx index 3bc4b145..781b9345 100644 --- a/apps/frontend/src/pages/WelcomePackagePage.tsx +++ b/apps/frontend/src/pages/WelcomePackagePage.tsx @@ -1,5 +1,5 @@ import React, { useState, useEffect, useRef } from 'react'; -import { Link } from 'react-router-dom'; +import { Link, useNavigate } from 'react-router-dom'; import { motion, AnimatePresence } from 'framer-motion'; import OrientationTab from '../components/welcome/OrientationTab'; import ProjectTimelineTab from '../components/welcome/ProjectTimelineTab'; @@ -25,6 +25,7 @@ import { spatialNavPill } from '../styles/style_config'; export default function WelcomePackagePage() { const { user } = useAuth(); const { currentProgram } = useProgram(); + const navigate = useNavigate(); // v1.69 — Conditional layout state: WelcomePackagePage fetches // orientation + resources in parallel and uses the loaded state @@ -193,6 +194,19 @@ export default function WelcomePackagePage() { ))}
+
+

🚀 Internship Roadmap

+

+ Start your internship journey with a step-by-step roadmap. +

+ + +
diff --git a/apps/frontend/src/routes/AppRoutes.tsx b/apps/frontend/src/routes/AppRoutes.tsx index 82e78c2c..f9c22362 100644 --- a/apps/frontend/src/routes/AppRoutes.tsx +++ b/apps/frontend/src/routes/AppRoutes.tsx @@ -6,6 +6,7 @@ import Spinner from '../components/ui/Spinner'; import ErrorBoundary from '../components/ui/ErrorBoundary'; import { FeatureGate } from '../components/support/FeatureGate'; + /** * Per-route error boundary. Without this, a single page crash unmounts * the whole React tree and leaves a blank SPA. Phase 4 §4.5 outstanding @@ -26,6 +27,7 @@ import MainLayout from '../components/layout/MainLayout'; import AskAIButton from '../components/askai/AskAIButton'; import AccountRoute from './guards/AccountRoute'; import AdminRoute from './guards/AdminRoute'; +import InternshipRoadmapPage from "../pages/InternshipRoadmapPage"; // User pages const AccountPage = lazy(() => import('../pages/AccountPage')); @@ -188,6 +190,17 @@ export default function AppRoutes() { } /> + + {/* Uses MainLayout like the rest of the site */} + + + + } + /> + {/* v1.87 — Sign My Tee. The wizard + share page are auth-protected (they're the owner's surface). The sign-page is public so a non-logged-in visitor can also sign via a typed signerName. */} @@ -205,7 +218,7 @@ export default function AppRoutes() { path="/tee/sign/:shareId" element={} /> - + } ); -} +} \ No newline at end of file diff --git a/docs/sentry-setup.md b/docs/sentry-setup.md index d46702d7..5edcbca1 100644 --- a/docs/sentry-setup.md +++ b/docs/sentry-setup.md @@ -43,11 +43,11 @@ SENTRY_DEBUG=false # set true in dev for verbose logs ### Frontend (apps/frontend/.env) ```bash -VITE_SENTRY_DSN=https://@.ingest.sentry.io/ +fefe-project-id> VITE_SENTRY_TRACES_SAMPLE_RATE=0.1 VITE_SENTRY_ENV=development -VITE_SENTRY_RELEASE=v1.0.0+1 -``` +VITE_SENTRY_RELEASE=v1.0.0-key>@.ingest.sentry.io/<+1 +```VITE_SENTRY_DSN=https://< Both envs use `git rev-parse --short HEAD` for `SENTRY_RELEASE` if you want auto-versioning. diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..ca1c81ab --- /dev/null +++ b/package-lock.json @@ -0,0 +1,3815 @@ +{ + "name": "crowd-source-faq", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "crowd-source-faq", + "version": "0.1.0", + "dependencies": { + "sharp": "^0.34.5" + }, + "devDependencies": { + "@playwright/test": "^1.44.1", + "@types/node": "^25.9.1", + "@types/range-parser": "^1.2.7", + "@typescript-eslint/eslint-plugin": "^7.1.0", + "@typescript-eslint/parser": "^7.1.0", + "eslint": "^8.57.0", + "mongodb-memory-server": "^9.2.0", + "mongoose": "^8.0.3", + "prettier": "^3.2.5", + "tsx": "^4.22.3", + "turbo": "^2.5.0", + "typescript": "^6.0.3" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.2.tgz", + "integrity": "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "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": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", + "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "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": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", + "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { + "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": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@mongodb-js/saslprep": { + "version": "1.4.12", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.4.12.tgz", + "integrity": "sha512-QAfAMwNgnYxZ2C6D1HgeP7Gc4i/uvJRim415PCIL9ptRxWMNbWeLBYb2/9R4pGKny/s1FVu2JA2cxCUBUOggrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "sparse-bitfield": "^3.0.3" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@playwright/test": { + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.61.1.tgz", + "integrity": "sha512-8nKv6+0RJSL9FE4jYOEGXnPeM/Hg12qZpmqzZjRh3qM0Y7c3z1mrOTfFLids72RDQYVh9WpLEfR5WdpNX4fkig==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.61.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@turbo/darwin-64": { + "version": "2.10.5", + "resolved": "https://registry.npmjs.org/@turbo/darwin-64/-/darwin-64-2.10.5.tgz", + "integrity": "sha512-ENvPwy3x5yS7MwNYHeWjqOBXkwIMp39Pd+/zXC6PoiNzF8EIvvLZOZZ+ny6L9x4WgS5vxUii2LM5gM+zjPdnWw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@turbo/darwin-arm64": { + "version": "2.10.5", + "resolved": "https://registry.npmjs.org/@turbo/darwin-arm64/-/darwin-arm64-2.10.5.tgz", + "integrity": "sha512-rqROo9zsF/P9RqsdtbLD1nFJicjSrYyvQ9kNJC38AbxA3pAs6VAlATvtvOFx7bqOv6vicf20SP9kF33avJjy2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@turbo/linux-64": { + "version": "2.10.5", + "resolved": "https://registry.npmjs.org/@turbo/linux-64/-/linux-64-2.10.5.tgz", + "integrity": "sha512-RoSSiNFUxi27zLJuM9F6GyWWjHgLch9t6nwD6K0FkXRirZkTLlzIj6IhFnK8H9++nefLtdFqylE4vGjZAv6AAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@turbo/linux-arm64": { + "version": "2.10.5", + "resolved": "https://registry.npmjs.org/@turbo/linux-arm64/-/linux-arm64-2.10.5.tgz", + "integrity": "sha512-4ZComcpzmHGmVynQqvvi+iZOSq/tBvY1SltXB8g4NZRsrA01W8E+yRL8RNM+PLoyWsrCnJa8xa+DkWkv+xg4iQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@turbo/windows-64": { + "version": "2.10.5", + "resolved": "https://registry.npmjs.org/@turbo/windows-64/-/windows-64-2.10.5.tgz", + "integrity": "sha512-eL2Iyj4DbMINq1Sr1w0iAi6nAiZOF16KSlRGwCJpVh+IWZeY33MAsLHVOBMj1xoFtncVJXclCVpTPL2nBoYkFg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@turbo/windows-arm64": { + "version": "2.10.5", + "resolved": "https://registry.npmjs.org/@turbo/windows-arm64/-/windows-arm64-2.10.5.tgz", + "integrity": "sha512-sog+wP+8YSJrdWZ/rUJg8xghVTrwoG+BrSlDQpnK5fzSgJHn1INRWXbVWRH0d3vX8dBI01E3yxXRre9Dn+OXQA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/node": { + "version": "25.9.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.5.tgz", + "integrity": "sha512-OScDchr2fwuUmWdf4kZ9h7PcJiYDVInhJizG/biAq3cAvqwYktuy/TYGGdZNMtNTFUP7rnb0NU4TUdm82kt4Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": ">=7.24.0 <7.24.7" + } + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/webidl-conversions": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", + "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/whatwg-url": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-8.2.2.tgz", + "integrity": "sha512-FtQu10RWgn3D9U4aazdwIE2yzphmTJREDqNdODHrbrZmmMqI0vMheC/6NE/J1Yveaj8H+ela+YwWTjq5PGmuhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/webidl-conversions": "*" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.18.0.tgz", + "integrity": "sha512-94EQTWZ40mzBc42ATNIBimBEDltSJ9RQHCC8vc/PDbxi4k8dVwUAv4o98dk50M1zB+JGFxp43FP7f8+FP8R6Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "7.18.0", + "@typescript-eslint/type-utils": "7.18.0", + "@typescript-eslint/utils": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^7.0.0", + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.18.0.tgz", + "integrity": "sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/scope-manager": "7.18.0", + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/typescript-estree": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.18.0.tgz", + "integrity": "sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.18.0.tgz", + "integrity": "sha512-XL0FJXuCLaDuX2sYqZUUSOJ2sG5/i1AAze+axqmLnSkNEVMVYLF+cbwlB2w8D1tinFuSikHmFta+P+HOofrLeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "7.18.0", + "@typescript-eslint/utils": "7.18.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.18.0.tgz", + "integrity": "sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.18.0.tgz", + "integrity": "sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.18.0.tgz", + "integrity": "sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "7.18.0", + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/typescript-estree": "7.18.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.18.0.tgz", + "integrity": "sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "7.18.0", + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.3.tgz", + "integrity": "sha512-60YRaenCQcVjYEKOcG824+DRGGIQ3VKErcBoAEDJZz5bKIs2ZG+X/H9Nk+Q6EVkwJk5QNApxbrc5QtBSwtrXAg==", + "dev": true, + "license": "ISC" + }, + "node_modules/acorn": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "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" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/async-mutex": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.4.1.tgz", + "integrity": "sha512-WfoBo4E/TbCX1G95XTjbWTE3X2XLG0m1Xbv2cwOtuPdyH9CZvnaA5nCt1ucjaKEgW2A5IF71hxrRhr83Je5xjA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/b4a": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.1.tgz", + "integrity": "sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } + }, + "node_modules/balanced-match": { + "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/bare-events": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.9.1.tgz", + "integrity": "sha512-Z0oHEHAFDZkffN8Qc39zNZjQlMDkPJRyyyZieU1VH7u8c5S+qHZ2S8ixdKIAxEjfHO7FJxXmJWgteOghVanIsg==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "bare-abort-controller": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + } + } + }, + "node_modules/bare-fs": { + "version": "4.7.4", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.7.4.tgz", + "integrity": "sha512-y1kC+ffIx/tPLdTE693uNjHfzTfr+ravR5tvWlMXe25nELbkqV400S71qHDwbkAQ1FVEZobB1NFRzFbCCcyBCQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.5.4", + "bare-path": "^3.0.0", + "bare-stream": "^2.6.4", + "bare-url": "^2.2.2", + "fast-fifo": "^1.3.2" + }, + "engines": { + "bare": ">=1.16.0" + }, + "peerDependencies": { + "bare-buffer": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + } + } + }, + "node_modules/bare-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.1.1.tgz", + "integrity": "sha512-JprUlveX3QjApC1cTpsUOiscADftCGVWkzitbHsRqv84hzYwYHw2mbluddsq5TvI8mH/8Ov1f4BiMAdcB0oYnQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/bare-stream": { + "version": "2.13.3", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.3.tgz", + "integrity": "sha512-Kc+brLqvEqGkjyfiwJmImAOqLZL7OsoLKuavx+hJjgVV3nLTOjloJyPMFxjUPerGGHrNH0fLU06jjykMLWrERQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.8.1", + "streamx": "^2.25.0", + "teex": "^1.0.1" + }, + "peerDependencies": { + "bare-abort-controller": "*", + "bare-buffer": "*", + "bare-events": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + }, + "bare-buffer": { + "optional": true + }, + "bare-events": { + "optional": true + } + } + }, + "node_modules/bare-url": { + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.4.5.tgz", + "integrity": "sha512-K+y9xF1tN+CdPu4qWwr0QiK1Al07eFPGYK5M2pDXcmHdMdgC/tT/bpmMe1hrmRHaidKLkXrC+cRNYf3XVDUhSQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-path": "^3.0.0" + } + }, + "node_modules/brace-expansion": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz", + "integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/bson": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/bson/-/bson-5.5.1.tgz", + "integrity": "sha512-ix0EwukN2EpC0SRWIj/7B5+A6uQMQy6KMREI9qQqvgpkV2frH63T0UDVd1SYedL6dNCmDBYB3QtXi4ISk9YT+g==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=14.20.1" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/color-convert": { + "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" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "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/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", + "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "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": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "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": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/events-universal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", + "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.7.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-cache-dir": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", + "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", + "dev": true, + "license": "MIT", + "dependencies": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/avajs/find-cache-dir?sponsor=1" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "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.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "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": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", + "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/node_modules/minimatch": { + "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": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ip-address": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-yaml": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", + "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/kareem": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.6.3.tgz", + "integrity": "sha512-C3iHfuGUXK2u8/ipq9LfjFfXFxAZMQJJq7vLS45r3D9Y2xQ/m4S8zaR4zMLFWh9AsNPXmcFfUDhTEO8UIC/V6Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/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/memory-pager": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", + "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", + "dev": true, + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/mongodb": { + "version": "5.9.2", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-5.9.2.tgz", + "integrity": "sha512-H60HecKO4Bc+7dhOv4sJlgvenK4fQNqqUIlXxZYQNbfEWSALGAwGoyJd/0Qwk4TttFXUOHJ2ZJQe/52ScaUwtQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bson": "^5.5.0", + "mongodb-connection-string-url": "^2.6.0", + "socks": "^2.7.1" + }, + "engines": { + "node": ">=14.20.1" + }, + "optionalDependencies": { + "@mongodb-js/saslprep": "^1.1.0" + }, + "peerDependencies": { + "@aws-sdk/credential-providers": "^3.188.0", + "@mongodb-js/zstd": "^1.0.0", + "kerberos": "^1.0.0 || ^2.0.0", + "mongodb-client-encryption": ">=2.3.0 <3", + "snappy": "^7.2.2" + }, + "peerDependenciesMeta": { + "@aws-sdk/credential-providers": { + "optional": true + }, + "@mongodb-js/zstd": { + "optional": true + }, + "kerberos": { + "optional": true + }, + "mongodb-client-encryption": { + "optional": true + }, + "snappy": { + "optional": true + } + } + }, + "node_modules/mongodb-connection-string-url": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-2.6.0.tgz", + "integrity": "sha512-WvTZlI9ab0QYtTYnuMLgobULWhokRjtC7db9LtcVfJ+Hsnyr5eo6ZtNAt3Ly24XZScGMelOcGtm7lSn0332tPQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/whatwg-url": "^8.2.1", + "whatwg-url": "^11.0.0" + } + }, + "node_modules/mongodb-memory-server": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/mongodb-memory-server/-/mongodb-memory-server-9.5.0.tgz", + "integrity": "sha512-In3zRT40cLlVtpy7FK6b96Lby6JBAdXj8Kf9YrH4p1Aa2X4ptojq7SmiRR3x47Lo0/UCXXIwhJpkdbYY8kRZAw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "mongodb-memory-server-core": "9.5.0", + "tslib": "^2.6.3" + }, + "engines": { + "node": ">=14.20.1" + } + }, + "node_modules/mongodb-memory-server-core": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/mongodb-memory-server-core/-/mongodb-memory-server-core-9.5.0.tgz", + "integrity": "sha512-Jb/V80JeYAKWaF4bPFme7SmTR6ew1PWgkpPUepLDfRraeN49i1cruxICeA4zz4T33W/o31N+zazP8wI8ebf7yw==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-mutex": "^0.4.1", + "camelcase": "^6.3.0", + "debug": "^4.3.7", + "find-cache-dir": "^3.3.2", + "follow-redirects": "^1.15.9", + "https-proxy-agent": "^7.0.5", + "mongodb": "^5.9.2", + "new-find-package-json": "^2.0.0", + "semver": "^7.6.3", + "tar-stream": "^3.1.7", + "tslib": "^2.6.3", + "yauzl": "^3.1.3" + }, + "engines": { + "node": ">=14.20.1" + } + }, + "node_modules/mongoose": { + "version": "8.24.1", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.24.1.tgz", + "integrity": "sha512-UpHBA0l5kHyKJQFjmBaFYQFo5sgz1DK0TRqDkOyBLYbqiIbKKhIvBpHWBXqeo0rgW4kGI1UhhAw+kTQZoj1BdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bson": "^6.10.4", + "kareem": "2.6.3", + "mongodb": "~6.20.0", + "mpath": "0.9.0", + "mquery": "5.0.0", + "ms": "2.1.3", + "sift": "17.1.3" + }, + "engines": { + "node": ">=16.20.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mongoose" + } + }, + "node_modules/mongoose/node_modules/@types/whatwg-url": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.5.tgz", + "integrity": "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/webidl-conversions": "*" + } + }, + "node_modules/mongoose/node_modules/bson": { + "version": "6.10.4", + "resolved": "https://registry.npmjs.org/bson/-/bson-6.10.4.tgz", + "integrity": "sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=16.20.1" + } + }, + "node_modules/mongoose/node_modules/mongodb": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.20.0.tgz", + "integrity": "sha512-Tl6MEIU3K4Rq3TSHd+sZQqRBoGlFsOgNrH5ltAcFBV62Re3Fd+FcaVf8uSEQFOJ51SDowDVttBTONMfoYWrWlQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@mongodb-js/saslprep": "^1.3.0", + "bson": "^6.10.4", + "mongodb-connection-string-url": "^3.0.2" + }, + "engines": { + "node": ">=16.20.1" + }, + "peerDependencies": { + "@aws-sdk/credential-providers": "^3.188.0", + "@mongodb-js/zstd": "^1.1.0 || ^2.0.0", + "gcp-metadata": "^5.2.0", + "kerberos": "^2.0.1", + "mongodb-client-encryption": ">=6.0.0 <7", + "snappy": "^7.3.2", + "socks": "^2.7.1" + }, + "peerDependenciesMeta": { + "@aws-sdk/credential-providers": { + "optional": true + }, + "@mongodb-js/zstd": { + "optional": true + }, + "gcp-metadata": { + "optional": true + }, + "kerberos": { + "optional": true + }, + "mongodb-client-encryption": { + "optional": true + }, + "snappy": { + "optional": true + }, + "socks": { + "optional": true + } + } + }, + "node_modules/mongoose/node_modules/mongodb-connection-string-url": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.2.tgz", + "integrity": "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/whatwg-url": "^11.0.2", + "whatwg-url": "^14.1.0 || ^13.0.0" + } + }, + "node_modules/mongoose/node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/mongoose/node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/mpath": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz", + "integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mquery": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/mquery/-/mquery-5.0.0.tgz", + "integrity": "sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "4.x" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/new-find-package-json": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/new-find-package-json/-/new-find-package-json-2.0.0.tgz", + "integrity": "sha512-lDcBsjBSMlj3LXH2v/FW3txlh2pYTjmbOXPYJD93HI5EwuLzI11tdHSIpUMmfq/IOsldj4Ps8M8flhm+pCK4Ew==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">=12.22.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true, + "license": "MIT" + }, + "node_modules/picomatch": { + "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": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/playwright": { + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.1.tgz", + "integrity": "sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.61.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.1.tgz", + "integrity": "sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.9.5", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.5.tgz", + "integrity": "sha512-/FVl766LpUfB5vXgCYOYa0MeV/441Ia99AeICQIQFTY/Nw0roZwULcXpku5i1/m5kt/baz+s4Zogspd839HSMg==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/sift": { + "version": "17.1.3", + "resolved": "https://registry.npmjs.org/sift/-/sift-17.1.3.tgz", + "integrity": "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.9.tgz", + "integrity": "sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "ip-address": "^10.1.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/sparse-bitfield": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", + "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "memory-pager": "^1.0.2" + } + }, + "node_modules/streamx": { + "version": "2.28.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.28.0.tgz", + "integrity": "sha512-1Yowhzjf0ivGMrTIkY9hav5TxobO9qIVqUE41fiCGMGgc3CLlf4MY+9AHmZqBWgDTue0fY9zWjYFVyf6Diuobw==", + "dev": true, + "license": "MIT", + "dependencies": { + "events-universal": "^1.0.0", + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar-stream": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.2.0.tgz", + "integrity": "sha512-ojzvCvVaNp6aOTFmG7jaRD0meowIAuPc3cMMhSgKiVWws1GyHbGd/xvnyuRKcKlMpt3qvxx6r0hreCNITP9hIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "b4a": "^1.6.4", + "bare-fs": "^4.5.5", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "node_modules/teex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", + "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "streamx": "^2.12.5" + } + }, + "node_modules/text-decoder": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz", + "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, + "license": "MIT" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tr46": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", + "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/ts-api-utils": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", + "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "devOptional": true, + "license": "0BSD" + }, + "node_modules/tsx": { + "version": "4.23.1", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.1.tgz", + "integrity": "sha512-GQHnkIfxyx1wYCOS/wonik5MVRZU9hi1TEZmzGZSCJB1y9YgoZ8H6itNE/u4suE+yLmOzuE4E5S4TZ/ZX2wcWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/tsx/node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/turbo": { + "version": "2.10.5", + "resolved": "https://registry.npmjs.org/turbo/-/turbo-2.10.5.tgz", + "integrity": "sha512-07Y/C7OUp23l4P92PJoYtFNbHjLhftrZH5Ce7dbczS4kX2Re+wtbXvZLoxn/pUtzgsQaRCBaRuZPJp4zmAn0WQ==", + "dev": true, + "license": "MIT", + "bin": { + "turbo": "bin/turbo" + }, + "optionalDependencies": { + "@turbo/darwin-64": "2.10.5", + "@turbo/darwin-arm64": "2.10.5", + "@turbo/linux-64": "2.10.5", + "@turbo/linux-arm64": "2.10.5", + "@turbo/windows-64": "2.10.5", + "@turbo/windows-arm64": "2.10.5" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-url": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", + "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^3.0.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/yauzl": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-3.4.0.tgz", + "integrity": "sha512-jIH9yLR9wqr0wOS0TpBvo/g/2UgZH5qePVbjgRliiF0BYvOZyaBknKsF+x9Iht0O6sqgnB93rCICdOZFecJuDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9647665c..31f0de09 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -111,6 +111,9 @@ importers: multer: specifier: ^2.1.1 version: 2.2.0 + nodemailer: + specifier: ^9.0.3 + version: 9.0.3 obscenity: specifier: ^0.4.6 version: 0.4.6 @@ -160,6 +163,9 @@ importers: '@types/node': specifier: ^25.9.1 version: 25.9.4 + '@types/nodemailer': + specifier: ^8.0.1 + version: 8.0.1 '@types/range-parser': specifier: ^1.2.7 version: 1.2.7 @@ -1606,6 +1612,9 @@ packages: '@types/node@25.9.4': resolution: {integrity: sha512-dszCsrKb5U7ZsVZBWiHFklTloVl0mSEnWH/iZXfZUlI4rzCUnsvGmgqfuVRHL54ugE7/wRuxEIXRa2iMZ+BG6g==} + '@types/nodemailer@8.0.1': + resolution: {integrity: sha512-PxpaInm8V1JQDd4j0ds5HfvWQk8JupS1C0Picb96QJsrrRDjBH+DlK7L4ZdNSqNULhiZRQHc40nLVShaGxXAMw==} + '@types/prop-types@15.7.15': resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} @@ -3245,6 +3254,10 @@ packages: resolution: {integrity: sha512-1uz8041X6LoI6ZSdZacM9lVY28vuzDlSKitnpbSNK0RfKoIJkX29NBPVEFXhnuSuEOA9Ww0xnPJ+ILWbGAv8DA==} engines: {node: '>=18'} + nodemailer@9.0.3: + resolution: {integrity: sha512-n+YP+NKwR5zRWa60k3GiQ6Q3B4KXCoAw40dAKeCtYn020iNN74aWK2liXIC3ZEATeGql7we3tE3t8QwhY0eskw==} + engines: {node: '>=6.0.0'} + nodemon@3.1.14: resolution: {integrity: sha512-jakjZi93UtB3jHMWsXL68FXSAosbLfY0In5gtKq3niLSkrWznrVBzXFNOEMJUfc9+Ke7SHWoAZsiMkNP3vq6Jw==} engines: {node: '>=10'} @@ -5377,6 +5390,10 @@ snapshots: dependencies: undici-types: 7.24.6 + '@types/nodemailer@8.0.1': + dependencies: + '@types/node': 25.9.4 + '@types/prop-types@15.7.15': {} '@types/qs@6.15.1': {} @@ -7154,6 +7171,8 @@ snapshots: node-releases@2.0.48: {} + nodemailer@9.0.3: {} + nodemon@3.1.14: dependencies: chokidar: 3.6.0