From 968bb58611a8261a14b1adb8ff19a79c5d9a5edc Mon Sep 17 00:00:00 2001 From: Harshit Raj Date: Thu, 30 Apr 2026 21:24:22 +0530 Subject: [PATCH 1/3] Backend Added - Pre Deployment --- .../skills/developing-genkit-dart/SKILL.md | 57 + .../references/genkit.md | 380 + .../references/genkit_anthropic.md | 41 + .../references/genkit_chrome.md | 23 + .../references/genkit_firebase_ai.md | 23 + .../references/genkit_google_genai.md | 95 + .../references/genkit_mcp.md | 115 + .../references/genkit_middleware.md | 84 + .../references/genkit_openai.md | 54 + .../references/genkit_shelf.md | 59 + .../references/schemantic.md | 137 + .agents/skills/developing-genkit-go/SKILL.md | 99 + .../references/flows-and-http.md | 183 + .../references/generation.md | 176 + .../references/getting-started.md | 140 + .../references/middleware.md | 363 + .../references/prompts.md | 256 + .../references/providers.md | 157 + .../developing-genkit-go/references/tools.md | 178 + .agents/skills/developing-genkit-js/SKILL.md | 112 + .../references/best-practices.md | 31 + .../references/common-errors.md | 132 + .../references/docs-and-cli.md | 62 + .../references/examples.md | 157 + .../developing-genkit-js/references/setup.md | 46 + .../skills/developing-genkit-python/SKILL.md | 56 + .../references/common-errors.md | 82 + .../references/dev-workflow.md | 90 + .../references/dotprompt.md | 109 + .../references/evals.md | 89 + .../references/examples.md | 171 + .../references/fastapi.md | 248 + .../references/setup.md | 40 + .../skills/firebase-ai-logic-basics/SKILL.md | 112 + .../references/usage_patterns_web.md | 174 + .../firebase-app-hosting-basics/SKILL.md | 58 + .../references/cli_commands.md | 71 + .../references/configuration.md | 51 + .../references/emulation.md | 47 + .agents/skills/firebase-auth-basics/SKILL.md | 86 + .../references/client_sdk_web.md | 287 + .../references/security_rules.md | 38 + .agents/skills/firebase-basics/SKILL.md | 68 + .../references/firebase-cli-guide.md | 16 + .../references/firebase-service-init.md | 18 + .../references/local-env-setup.md | 56 + .../references/refresh/antigravity.md | 46 + .../references/refresh/claude.md | 10 + .../references/refresh/gemini-cli.md | 11 + .../references/refresh/other-agents.md | 48 + .../references/setup/antigravity.md | 63 + .../references/setup/claude_code.md | 30 + .../references/setup/cursor.md | 63 + .../references/setup/gemini_cli.md | 39 + .../references/setup/github_copilot.md | 70 + .../references/setup/other_agents.md | 65 + .../firebase-basics/references/web_setup.md | 69 + .agents/skills/firebase-data-connect/SKILL.md | 131 + .../skills/firebase-data-connect/examples.md | 629 ++ .../reference/advanced.md | 303 + .../firebase-data-connect/reference/config.md | 267 + .../reference/native_sql.md | 122 + .../reference/operations.md | 376 + .../reference/realtime.md | 179 + .../firebase-data-connect/reference/schema.md | 278 + .../firebase-data-connect/reference/sdks.md | 310 + .../reference/security.md | 289 + .../skills/firebase-data-connect/templates.md | 318 + .../SKILL.md | 31 + .../references/data_model.md | 54 + .../references/indexes.md | 111 + .../references/provisioning.md | 101 + .../references/python_sdk_usage.md | 126 + .../references/security_rules.md | 414 + .../references/web_sdk_usage.md | 201 + .../firebase-firestore-standard/SKILL.md | 27 + .../references/indexes.md | 82 + .../references/provisioning.md | 87 + .../references/security_rules.md | 414 + .../references/web_sdk_usage.md | 183 + .../skills/firebase-hosting-basics/SKILL.md | 46 + .../references/configuration.md | 101 + .../references/deploying.md | 39 + .../firebase-security-rules-auditor/SKILL.md | 45 + .firebaserc | 5 + .github/workflows/firebase-hosting-merge.yml | 20 + .../firebase-hosting-pull-request.yml | 21 + .gitignore | 50 +- README.md | 36 +- SETUP.md | 189 + backend/.gitignore | 22 + backend/SETUP.md | 143 + backend/pom.xml | 137 + .../iiitinsider/IiitInsiderApplication.java | 19 + .../com/iiitinsider/config/CorsConfig.java | 36 + .../iiitinsider/config/FirebaseConfig.java | 40 + .../config/JwtAuthenticationFilter.java | 56 + .../iiitinsider/config/SecurityConfig.java | 65 + .../controller/AdminController.java | 51 + .../controller/AuthController.java | 118 + .../controller/CollegeController.java | 46 + .../controller/NotificationController.java | 152 + .../controller/SocialMediaController.java | 70 + .../java/com/iiitinsider/model/Branch.java | 118 + .../java/com/iiitinsider/model/College.java | 264 + .../java/com/iiitinsider/model/Cutoff.java | 164 + .../com/iiitinsider/model/DeviceToken.java | 111 + .../iiitinsider/model/PlacementStatistic.java | 162 + .../java/com/iiitinsider/model/Program.java | 109 + .../java/com/iiitinsider/model/Review.java | 139 + .../main/java/com/iiitinsider/model/User.java | 141 + .../repository/BranchRepository.java | 12 + .../repository/CollegeRepository.java | 17 + .../repository/CutoffRepository.java | 13 + .../repository/DeviceTokenRepository.java | 24 + .../PlacementStatisticRepository.java | 14 + .../repository/ProgramRepository.java | 12 + .../repository/ReviewRepository.java | 12 + .../repository/UserRepository.java | 15 + .../com/iiitinsider/service/AuthService.java | 63 + .../iiitinsider/service/CollegeService.java | 248 + .../service/FirebaseNotificationService.java | 125 + .../com/iiitinsider/service/JwtService.java | 76 + .../service/SocialMediaService.java | 64 + .../service/UserDetailsServiceImpl.java | 33 + .../src/main/resources/application.properties | 38 + backend/src/main/resources/data.sql | 102 + backend/src/main/resources/schema.sql | 130 + .../AuthControllerIntegrationTest.java | 110 + eslint.config.js | 58 +- firebase.json | 16 + index.html | 26 +- nginx/nginx.conf | 55 + package-lock.json | 8074 ++++++++++------- package.json | 63 +- public/firebase-messaging-sw.js | 37 + skills-lock.json | 83 + src/App.css | 84 +- src/App.jsx | 60 +- src/Components/Button.jsx | 48 +- src/Components/Card.jsx | 52 +- src/Components/Carousel.jsx | 70 +- src/Components/Charm.jsx | 60 +- src/Components/CollegeMatch.jsx | 104 +- src/Components/ComparingData.jsx | 866 +- src/Components/Data.jsx | 578 +- src/Components/Logo.jsx | 10 +- src/Components/Name.jsx | 22 +- src/Components/Navbar.jsx | 104 +- src/Components/NotificationButton.jsx | 167 + src/Components/QuickLink.jsx | 257 +- src/Components/SocialShare.jsx | 70 + src/Pages/About.jsx | 64 + src/Pages/College.jsx | 189 +- src/Pages/Compare.jsx | 273 +- src/Pages/Contact.jsx | 57 + src/Pages/Home.jsx | 32 +- src/Pages/Privacy.jsx | 51 + src/Pages/Register.jsx | 146 +- src/Pages/Support.jsx | 75 + src/Pages/User_table.jsx | 118 +- src/assets/college.png | Bin 0 -> 7107 bytes src/assets/cutoff.png | Bin 0 -> 9204 bytes src/assets/exams.png | Bin 0 -> 17444 bytes src/assets/logoiiit.jpg | Bin 0 -> 40010 bytes src/index.css | 16 +- src/main.jsx | 26 +- src/services/api.js | 157 + src/services/firebase.js | 6 + src/services/firebaseService.js | 84 + vite.config.js | 30 +- 171 files changed, 21780 insertions(+), 5005 deletions(-) create mode 100644 .agents/skills/developing-genkit-dart/SKILL.md create mode 100644 .agents/skills/developing-genkit-dart/references/genkit.md create mode 100644 .agents/skills/developing-genkit-dart/references/genkit_anthropic.md create mode 100644 .agents/skills/developing-genkit-dart/references/genkit_chrome.md create mode 100644 .agents/skills/developing-genkit-dart/references/genkit_firebase_ai.md create mode 100644 .agents/skills/developing-genkit-dart/references/genkit_google_genai.md create mode 100644 .agents/skills/developing-genkit-dart/references/genkit_mcp.md create mode 100644 .agents/skills/developing-genkit-dart/references/genkit_middleware.md create mode 100644 .agents/skills/developing-genkit-dart/references/genkit_openai.md create mode 100644 .agents/skills/developing-genkit-dart/references/genkit_shelf.md create mode 100644 .agents/skills/developing-genkit-dart/references/schemantic.md create mode 100644 .agents/skills/developing-genkit-go/SKILL.md create mode 100644 .agents/skills/developing-genkit-go/references/flows-and-http.md create mode 100644 .agents/skills/developing-genkit-go/references/generation.md create mode 100644 .agents/skills/developing-genkit-go/references/getting-started.md create mode 100644 .agents/skills/developing-genkit-go/references/middleware.md create mode 100644 .agents/skills/developing-genkit-go/references/prompts.md create mode 100644 .agents/skills/developing-genkit-go/references/providers.md create mode 100644 .agents/skills/developing-genkit-go/references/tools.md create mode 100644 .agents/skills/developing-genkit-js/SKILL.md create mode 100644 .agents/skills/developing-genkit-js/references/best-practices.md create mode 100644 .agents/skills/developing-genkit-js/references/common-errors.md create mode 100644 .agents/skills/developing-genkit-js/references/docs-and-cli.md create mode 100644 .agents/skills/developing-genkit-js/references/examples.md create mode 100644 .agents/skills/developing-genkit-js/references/setup.md create mode 100644 .agents/skills/developing-genkit-python/SKILL.md create mode 100644 .agents/skills/developing-genkit-python/references/common-errors.md create mode 100644 .agents/skills/developing-genkit-python/references/dev-workflow.md create mode 100644 .agents/skills/developing-genkit-python/references/dotprompt.md create mode 100644 .agents/skills/developing-genkit-python/references/evals.md create mode 100644 .agents/skills/developing-genkit-python/references/examples.md create mode 100644 .agents/skills/developing-genkit-python/references/fastapi.md create mode 100644 .agents/skills/developing-genkit-python/references/setup.md create mode 100644 .agents/skills/firebase-ai-logic-basics/SKILL.md create mode 100644 .agents/skills/firebase-ai-logic-basics/references/usage_patterns_web.md create mode 100644 .agents/skills/firebase-app-hosting-basics/SKILL.md create mode 100644 .agents/skills/firebase-app-hosting-basics/references/cli_commands.md create mode 100644 .agents/skills/firebase-app-hosting-basics/references/configuration.md create mode 100644 .agents/skills/firebase-app-hosting-basics/references/emulation.md create mode 100644 .agents/skills/firebase-auth-basics/SKILL.md create mode 100644 .agents/skills/firebase-auth-basics/references/client_sdk_web.md create mode 100644 .agents/skills/firebase-auth-basics/references/security_rules.md create mode 100644 .agents/skills/firebase-basics/SKILL.md create mode 100644 .agents/skills/firebase-basics/references/firebase-cli-guide.md create mode 100644 .agents/skills/firebase-basics/references/firebase-service-init.md create mode 100644 .agents/skills/firebase-basics/references/local-env-setup.md create mode 100644 .agents/skills/firebase-basics/references/refresh/antigravity.md create mode 100644 .agents/skills/firebase-basics/references/refresh/claude.md create mode 100644 .agents/skills/firebase-basics/references/refresh/gemini-cli.md create mode 100644 .agents/skills/firebase-basics/references/refresh/other-agents.md create mode 100644 .agents/skills/firebase-basics/references/setup/antigravity.md create mode 100644 .agents/skills/firebase-basics/references/setup/claude_code.md create mode 100644 .agents/skills/firebase-basics/references/setup/cursor.md create mode 100644 .agents/skills/firebase-basics/references/setup/gemini_cli.md create mode 100644 .agents/skills/firebase-basics/references/setup/github_copilot.md create mode 100644 .agents/skills/firebase-basics/references/setup/other_agents.md create mode 100644 .agents/skills/firebase-basics/references/web_setup.md create mode 100644 .agents/skills/firebase-data-connect/SKILL.md create mode 100644 .agents/skills/firebase-data-connect/examples.md create mode 100644 .agents/skills/firebase-data-connect/reference/advanced.md create mode 100644 .agents/skills/firebase-data-connect/reference/config.md create mode 100644 .agents/skills/firebase-data-connect/reference/native_sql.md create mode 100644 .agents/skills/firebase-data-connect/reference/operations.md create mode 100644 .agents/skills/firebase-data-connect/reference/realtime.md create mode 100644 .agents/skills/firebase-data-connect/reference/schema.md create mode 100644 .agents/skills/firebase-data-connect/reference/sdks.md create mode 100644 .agents/skills/firebase-data-connect/reference/security.md create mode 100644 .agents/skills/firebase-data-connect/templates.md create mode 100644 .agents/skills/firebase-firestore-enterprise-native-mode/SKILL.md create mode 100644 .agents/skills/firebase-firestore-enterprise-native-mode/references/data_model.md create mode 100644 .agents/skills/firebase-firestore-enterprise-native-mode/references/indexes.md create mode 100644 .agents/skills/firebase-firestore-enterprise-native-mode/references/provisioning.md create mode 100644 .agents/skills/firebase-firestore-enterprise-native-mode/references/python_sdk_usage.md create mode 100644 .agents/skills/firebase-firestore-enterprise-native-mode/references/security_rules.md create mode 100644 .agents/skills/firebase-firestore-enterprise-native-mode/references/web_sdk_usage.md create mode 100644 .agents/skills/firebase-firestore-standard/SKILL.md create mode 100644 .agents/skills/firebase-firestore-standard/references/indexes.md create mode 100644 .agents/skills/firebase-firestore-standard/references/provisioning.md create mode 100644 .agents/skills/firebase-firestore-standard/references/security_rules.md create mode 100644 .agents/skills/firebase-firestore-standard/references/web_sdk_usage.md create mode 100644 .agents/skills/firebase-hosting-basics/SKILL.md create mode 100644 .agents/skills/firebase-hosting-basics/references/configuration.md create mode 100644 .agents/skills/firebase-hosting-basics/references/deploying.md create mode 100644 .agents/skills/firebase-security-rules-auditor/SKILL.md create mode 100644 .firebaserc create mode 100644 .github/workflows/firebase-hosting-merge.yml create mode 100644 .github/workflows/firebase-hosting-pull-request.yml create mode 100644 SETUP.md create mode 100644 backend/.gitignore create mode 100644 backend/SETUP.md create mode 100644 backend/pom.xml create mode 100644 backend/src/main/java/com/iiitinsider/IiitInsiderApplication.java create mode 100644 backend/src/main/java/com/iiitinsider/config/CorsConfig.java create mode 100644 backend/src/main/java/com/iiitinsider/config/FirebaseConfig.java create mode 100644 backend/src/main/java/com/iiitinsider/config/JwtAuthenticationFilter.java create mode 100644 backend/src/main/java/com/iiitinsider/config/SecurityConfig.java create mode 100644 backend/src/main/java/com/iiitinsider/controller/AdminController.java create mode 100644 backend/src/main/java/com/iiitinsider/controller/AuthController.java create mode 100644 backend/src/main/java/com/iiitinsider/controller/CollegeController.java create mode 100644 backend/src/main/java/com/iiitinsider/controller/NotificationController.java create mode 100644 backend/src/main/java/com/iiitinsider/controller/SocialMediaController.java create mode 100644 backend/src/main/java/com/iiitinsider/model/Branch.java create mode 100644 backend/src/main/java/com/iiitinsider/model/College.java create mode 100644 backend/src/main/java/com/iiitinsider/model/Cutoff.java create mode 100644 backend/src/main/java/com/iiitinsider/model/DeviceToken.java create mode 100644 backend/src/main/java/com/iiitinsider/model/PlacementStatistic.java create mode 100644 backend/src/main/java/com/iiitinsider/model/Program.java create mode 100644 backend/src/main/java/com/iiitinsider/model/Review.java create mode 100644 backend/src/main/java/com/iiitinsider/model/User.java create mode 100644 backend/src/main/java/com/iiitinsider/repository/BranchRepository.java create mode 100644 backend/src/main/java/com/iiitinsider/repository/CollegeRepository.java create mode 100644 backend/src/main/java/com/iiitinsider/repository/CutoffRepository.java create mode 100644 backend/src/main/java/com/iiitinsider/repository/DeviceTokenRepository.java create mode 100644 backend/src/main/java/com/iiitinsider/repository/PlacementStatisticRepository.java create mode 100644 backend/src/main/java/com/iiitinsider/repository/ProgramRepository.java create mode 100644 backend/src/main/java/com/iiitinsider/repository/ReviewRepository.java create mode 100644 backend/src/main/java/com/iiitinsider/repository/UserRepository.java create mode 100644 backend/src/main/java/com/iiitinsider/service/AuthService.java create mode 100644 backend/src/main/java/com/iiitinsider/service/CollegeService.java create mode 100644 backend/src/main/java/com/iiitinsider/service/FirebaseNotificationService.java create mode 100644 backend/src/main/java/com/iiitinsider/service/JwtService.java create mode 100644 backend/src/main/java/com/iiitinsider/service/SocialMediaService.java create mode 100644 backend/src/main/java/com/iiitinsider/service/UserDetailsServiceImpl.java create mode 100644 backend/src/main/resources/application.properties create mode 100644 backend/src/main/resources/data.sql create mode 100644 backend/src/main/resources/schema.sql create mode 100644 backend/src/test/java/com/iiitinsider/controller/AuthControllerIntegrationTest.java create mode 100644 firebase.json create mode 100644 nginx/nginx.conf create mode 100644 public/firebase-messaging-sw.js create mode 100644 skills-lock.json create mode 100644 src/Components/NotificationButton.jsx create mode 100644 src/Components/SocialShare.jsx create mode 100644 src/Pages/About.jsx create mode 100644 src/Pages/Contact.jsx create mode 100644 src/Pages/Privacy.jsx create mode 100644 src/Pages/Support.jsx create mode 100644 src/assets/college.png create mode 100644 src/assets/cutoff.png create mode 100644 src/assets/exams.png create mode 100644 src/assets/logoiiit.jpg create mode 100644 src/services/api.js create mode 100644 src/services/firebase.js create mode 100644 src/services/firebaseService.js diff --git a/.agents/skills/developing-genkit-dart/SKILL.md b/.agents/skills/developing-genkit-dart/SKILL.md new file mode 100644 index 0000000..706023b --- /dev/null +++ b/.agents/skills/developing-genkit-dart/SKILL.md @@ -0,0 +1,57 @@ +--- +name: developing-genkit-dart +description: Generates code and provides documentation for the Genkit Dart SDK. Use when the user asks to build AI agents in Dart, use Genkit flows, or integrate LLMs into Dart/Flutter applications. +metadata: + genkit-managed: true +--- + +# Genkit Dart + +Genkit Dart is an AI SDK for Dart that provides a unified interface for code generation, structured outputs, tools, flows, and AI agents. + +## Core Features and Usage +If you need help with initializing Genkit (`Genkit()`), Generation (`ai.generate`), Tooling (`ai.defineTool`), Flows (`ai.defineFlow`), Embeddings (`ai.embedMany`), streaming, or calling remote flow endpoints, please load the core framework reference: +[references/genkit.md](references/genkit.md) + +## Genkit CLI (recommended) + +The Genkit CLI provides a local development UI for running Flow, tracing executions, playing with models, and evaluating outputs. + +check if the user has it installed: `genkit --version` + +**Installation:** +```bash +curl -sL cli.genkit.dev | bash # Native CLI +# OR +npm install -g genkit-cli # Via npm +``` + +**Usage:** +Wrap your run command with `genkit start` to attach the Genkit developer UI and tracing: +```bash +genkit start -- dart run main.dart +``` + +## Plugin Ecosystem +Genkit relies on a large suite of plugins to perform generative AI actions, interface with external LLMs, or host web servers. + +When asked to use any given plugin, always verify usage by referring to its corresponding reference below. You should load the reference when you need to know the specific initialization arguments, tools, models, and usage patterns for the plugin: + +| Plugin Name | Reference Link | Description | +| ---- | ---- | ---- | +| `genkit_google_genai` | [references/genkit_google_genai.md](references/genkit_google_genai.md) | Load for Google Gemini plugin interface usage. | +| `genkit_anthropic` | [references/genkit_anthropic.md](references/genkit_anthropic.md) | Load for Anthropic plugin interface for Claude models. | +| `genkit_openai` | [references/genkit_openai.md](references/genkit_openai.md) | Load for OpenAI plugin interface for GPT models, Groq, and custom compatible endpoints. | +| `genkit_middleware` | [references/genkit_middleware.md](references/genkit_middleware.md) | Load for Tooling for specific agentic behavior: `filesystem`, `skills`, and `toolApproval` interrupts. | +| `genkit_mcp` | [references/genkit_mcp.md](references/genkit_mcp.md) | Load for Model Context Protocol integration (Server, Host, and Client capabilities). | +| `genkit_chrome` | [references/genkit_chrome.md](references/genkit_chrome.md) | Load for Running Gemini Nano locally inside the Chrome browser using the Prompt API. | +| `genkit_shelf` | [references/genkit_shelf.md](references/genkit_shelf.md) | Load for Integrating Genkit Flow actions over HTTP using Dart Shelf. | +| `genkit_firebase_ai` | [references/genkit_firebase_ai.md](references/genkit_firebase_ai.md) | Load for Firebase AI plugin interface (Gemini API via Vertex AI). | + +## External Dependencies +Whenever you define schemas mapping inside of Tools, Flows, and Prompts, you must use the [schemantic](https://pub.dev/packages/schemantic) library. +To learn how to use schemantic, ensure you read [references/schemantic.md](references/schemantic.md) for how to implement type safe generated Dart code. This is particularly relevant when you encounter symbols like `@Schema()`, `SchemanticType`, or classes with the `$` prefix. Genkit Dart uses schemantic for all of its data models so it's a CRITICAL skill to understand for using Genkit Dart. + +## Best Practices +- Always check that code cleanly compiles using `dart analyze` before generating the final response. +- Always use the Genkit CLI for local development and debugging. diff --git a/.agents/skills/developing-genkit-dart/references/genkit.md b/.agents/skills/developing-genkit-dart/references/genkit.md new file mode 100644 index 0000000..7dd33e5 --- /dev/null +++ b/.agents/skills/developing-genkit-dart/references/genkit.md @@ -0,0 +1,380 @@ +# Genkit Core Framework + +Genkit Dart is an AI SDK for Dart that provides a unified interface for text generation, structured output, tool calling, and agentic workflows. + +## Initialization + +```dart +import 'package:genkit/genkit.dart'; +import 'package:genkit_google_genai/genkit_google_genai.dart'; // Or any other plugin + +void main() async { + // Pass plugins to use into the Genkit constructor + final ai = Genkit(plugins: [googleAI()]); +} +``` + +## Generate Text + +```dart +final response = await ai.generate( + model: googleAI.gemini('gemini-2.5-flash'), // Needs a model reference from a plugin + prompt: 'Explain quantum computing in simple terms.', +); + +print(response.text); +``` + +## Stream Responses +```dart +final stream = ai.generateStream( + model: googleAI.gemini('gemini-2.5-flash'), + prompt: 'Write a short story about a robot learning to paint.', +); + +await for (final chunk in stream) { + print(chunk.text); +} +``` + +## Embed Text +```dart +final embeddings = await ai.embedMany( + documents: [ + DocumentData(content: [TextPart(text: 'Hello world')]), + ], + embedder: googleAI.textEmbedding('text-embedding-004'), +); + +print(embeddings.first.embedding); +``` + +## Define Tools +Models can use define actions and access external data via custom defined tools. +Requires the `schemantic` library for schema definitions. + +```dart +import 'package:schemantic/schemantic.dart'; + +@Schema() +abstract class $WeatherInput { + String get location; +} + +final weatherTool = ai.defineTool( + name: 'getWeather', + description: 'Gets the current weather for a location', + inputSchema: WeatherInput.$schema, + fn: (input, _) async { + // Call your weather API here + return 'Weather in ${input.location}: 72°F and sunny'; + }, +); + +final response = await ai.generate( + model: googleAI.gemini('gemini-2.5-flash'), + prompt: 'What\'s the weather like in San Francisco?', + toolNames: ['getWeather'], // Use the tools +); +``` + +## Structured Output + +You can ensure the generative model returns a typed JSON object by providing an `outputSchema`. + +```dart +@Schema() +abstract class $Person { + String get name; + int get age; +} + +// ... inside main ... + +final response = await ai.generate( + model: googleAI.gemini('gemini-2.5-flash'), + prompt: 'Generate a person named John Doe, age 30', + outputSchema: Person.$schema, // Force the model to return this schema +); + +final person = response.output; // Typed Person object +print('Name: ${person.name}, Age: ${person.age}'); +``` + +## Define Flows +Wrap your AI logic in flows for better observability, testing, and deployment: + +```dart +final jokeFlow = ai.defineFlow( + name: 'tellJoke', + inputSchema: .string(), + outputSchema: .string(), + fn: (topic, _) async { + final response = await ai.generate( + model: googleAI.gemini('gemini-2.5-flash'), + prompt: 'Tell me a joke about $topic', + ); + return response.text; // Value return + }, +); + +final joke = await jokeFlow('programming'); +print(joke); +``` + +### Streaming Flows +Stream data from your flows using `context.sendChunk(...)` and returning the final value: + +```dart +final streamStory = ai.defineFlow( + name: 'streamStory', + inputSchema: .string(), + outputSchema: .string(), + streamSchema: .string(), + fn: (topic, context) async { + final stream = ai.generateStream( + model: googleAI.gemini('gemini-2.5-flash'), + prompt: 'Write a story about $topic', + ); + + await for (final chunk in stream) { + context.sendChunk(chunk.text); // Stream the chunks + } + return 'Story complete'; // Value return + }, +); +``` + +## Calling remote Flows from a dart client +The `genkit` package provides `package:genkit/client.dart` representing remote Genkit actions that can be invoked or streamed using type-safe definitions. + +1. Defines a remote action +```dart +import 'package:genkit/client.dart'; + +final stringAction = defineRemoteAction( + url: 'http://localhost:3400/my-flow', + inputSchema: .string(), + outputSchema: .string(), +); +``` + +2. Call the Remote Action (Non-streaming) +```dart +final response = await stringAction(input: 'Hello from Dart!'); +print('Flow Response: $response'); +``` + +3. Call the Remote Action (Streaming) +Use the `.stream()` method on the action flow, and access `stream.onResult` to wait on the async return value. +```dart +final streamAction = defineRemoteAction( + url: 'http://localhost:3400/stream-story', + inputSchema: .string(), + outputSchema: .string(), + streamSchema: .string(), +); + +final stream = streamAction.stream( + input: 'Tell me a short story about a Dart developer.', +); + +await for (final chunk in stream) { + print('Chunk: $chunk'); +} + +final finalResult = await stream.onResult; +print('\nFinal Response: $finalResult'); +``` + +## Calling remote Flows from a Javascript client + +Install `genkit` npm package: + +```bash +npm install genkit +``` + +1. Call a remote flow (non-streaming) + +```ts +import { runFlow } from 'genkit/beta/client'; + +async function callHelloFlow() { + try { + const result = await runFlow({ + url: 'http://127.0.0.1:3400/helloFlow', // Replace with your deployed flow's URL + input: { name: 'Genkit User' }, + }); + console.log('Non-streaming result:', result.greeting); + } catch (error) { + console.error('Error calling helloFlow:', error); + } +} + +callHelloFlow(); +``` + +2. Call a remote flow (streaming) + +```ts +import { streamFlow } from 'genkit/beta/client'; + +async function streamHelloFlow() { + try { + const result = streamFlow({ + url: 'http://127.0.0.1:3400/helloFlow', // Replace with your deployed flow's URL + input: { name: 'Streaming User' }, + }); + + // Process the stream chunks as they arrive + for await (const chunk of result.stream) { + console.log('Stream chunk:', chunk); + } + + // Get the final complete response + const finalOutput = await result.output; + console.log('Final streaming output:', finalOutput.greeting); + } catch (error) { + console.error('Error streaming helloFlow:', error); + } +} + +streamHelloFlow(); +``` + +## Data Models + +Genkit uses standard data models for representing prompts (messages & parts) and responses. These classes are implemented using schemantic library. + +```dart +import 'package:genkit/genkit.dart'; +import 'package:schemantic/schemantic.dart'; + +@Schema() +abstract class $MyDataModel { + // uses Genkit's Message schema (not schemantic's Message) + List<$Message> get messages; + List<$Part> get parts; +} + +void example() { + // --- Parts --- + // A Text part + final textPart = TextPart(text: 'some text', metadata: {'foo': 'bar'}); + + // A Media/Image part + final mediaPart = MediaPart( + media: Media(url: 'https://...', contentType: 'image/png'), + metadata: {'foo': 'bar'}, + ); + + // A Tool Request initiated by the model + final toolRequestPart = ToolRequestPart( + toolRequest: ToolRequest( + name: 'get_weather', + ref: 'abc', + input: {'location': 'Paris, France'}, + ), + metadata: {'foo': 'bar'}, + ); + + // The resulting data from a Tool execution + final toolResponsePart = ToolResponsePart( + toolResponse: ToolResponse( + name: 'get_weather', + ref: 'abc', + output: {'temperature': '20C'}, + ), + metadata: {'foo': 'bar'}, + ); + + // Model reasoning (e.g. for Claude's "thinking" models) + final reasoningPart = ReasoningPart( + reasoning: 'thinking...', + metadata: {'foo': 'bar'}, + ); + + // A custom fallback part + final customPart = CustomPart( + custom: {'provider': {'specific': 'data'}}, + metadata: {'foo': 'bar'}, + ); + + // --- Messages --- + final systemMessage = Message( + role: Role.system, + content: [textPart, mediaPart], + metadata: {'foo': 'bar'}, + ); + + final userMessage = Message( + role: Role.user, + content: [textPart, mediaPart], // Can contain media (multimodal) + ); + + final modelMessage = Message( + role: Role.model, + // Models can emit text, tool requests, reasoning, or custom parts + content: [textPart, toolRequestPart, reasoningPart, customPart], + ); + + // --- Ergonomic Data Access (schema_extensions.dart) --- + // The Genkit SDK provides extensions on `Message` and `Part` to easily access fields + // without needing to cast them manually. + + // Get concatenated text from all TextParts in a Message + print(modelMessage.text); + + // Get the first Media object from a Message + print(modelMessage.media?.url); + + // Iterate over tool requests in a Message + for (final toolReq in modelMessage.toolRequests) { + print(toolReq.name); + } + + // Inspect individual parts + for (final part in modelMessage.content) { + if (part.isText) print(part.text); + if (part.isMedia) print(part.media?.url); + if (part.isToolRequest) print(part.toolRequest?.name); + if (part.isToolResponse) print(part.toolResponse?.name); + if (part.isReasoning) print(part.reasoning); + if (part.isCustom) print(part.custom); + } + + // --- Streaming Chunks --- + // Data emitted by ai.generateStream() calls + final generateResponseChunk = ModelResponseChunk( + content: [textPart], + index: 0, // Index of the message this chunk belongs to + aggregated: false, + ); + + // Chunks also have text and media accessors + print(generateResponseChunk.text); + + // --- Advanced: Schemas --- + // Use Genkit type schemas directly in Schemantic validations + final messageSchema = Message.$schema; + final partSchema = Part.$schema; + + final mySchema = SchemanticType.map( + .string(), + .list(Message.$schema), // Requires a list of Messages + ); + + // --- Generate Response --- + // ai.generate() returns a GenerateResponseHelper which provides ergonomic getters + // over the underlying ModelResponse: + final response = await ai.generate(...); + + print(response.text); // Concatenated text + print(response.media?.url); // First media part + print(response.toolRequests); // All tool requests + print(response.interrupts); // Tool requests that triggered an interrupt + print(response.messages); // Full history of the conversation, including the request and response + print(response.output); // Structured typed output (if outputSchema was used) +} +``` diff --git a/.agents/skills/developing-genkit-dart/references/genkit_anthropic.md b/.agents/skills/developing-genkit-dart/references/genkit_anthropic.md new file mode 100644 index 0000000..2e420a3 --- /dev/null +++ b/.agents/skills/developing-genkit-dart/references/genkit_anthropic.md @@ -0,0 +1,41 @@ +# Genkit Anthropic Plugin (`genkit_anthropic`) + +The Anthropic plugin for Genkit Dart, used for interacting with the Claude models. + +## Usage + +Requires `ANTHROPIC_API_KEY` to be passed to the init block. + +```dart +import 'dart:io'; +import 'package:genkit/genkit.dart'; +import 'package:genkit_anthropic/genkit_anthropic.dart'; + +void main() async { + final ai = Genkit( + plugins: [anthropic(apiKey: Platform.environment['ANTHROPIC_API_KEY']!)], + ); + + final response = await ai.generate( + model: anthropic.model('claude-sonnet-4-5'), + prompt: 'Tell me a joke about a developer.', + ); + + print(response.text); +} +``` + +## Claude Thinking Configurations + +Provides specific configurations for utilizing Claude 3.7+ "thinking" model capabilities. + +```dart +final response = await ai.generate( + model: anthropic.model('claude-sonnet-4-5'), + prompt: 'Solve this 24 game: 2, 3, 10, 10', + config: AnthropicOptions(thinking: ThinkingConfig(budgetTokens: 2048)), +); + +// The thinking content is available in the message parts +print(response.message?.content); +``` diff --git a/.agents/skills/developing-genkit-dart/references/genkit_chrome.md b/.agents/skills/developing-genkit-dart/references/genkit_chrome.md new file mode 100644 index 0000000..8152369 --- /dev/null +++ b/.agents/skills/developing-genkit-dart/references/genkit_chrome.md @@ -0,0 +1,23 @@ +# Genkit Chrome AI Plugin (`genkit_chrome`) + +Chrome Built-in AI (Gemini Nano) plugin for Genkit Dart, allowing local offline execution within a Chrome application. + +## Usage + +```dart +import 'package:genkit/genkit.dart'; +import 'package:genkit_chrome/genkit_chrome.dart'; + +void main() async { + final ai = Genkit(plugins: [ChromeAIPlugin()]); + + final stream = ai.generateStream( + model: modelRef('chrome/gemini-nano'), + prompt: 'Write a story about a robot.', + ); + + await for (final chunk in stream) { + print(chunk.text); + } +} +``` diff --git a/.agents/skills/developing-genkit-dart/references/genkit_firebase_ai.md b/.agents/skills/developing-genkit-dart/references/genkit_firebase_ai.md new file mode 100644 index 0000000..7ec462d --- /dev/null +++ b/.agents/skills/developing-genkit-dart/references/genkit_firebase_ai.md @@ -0,0 +1,23 @@ +# Genkit Firebase AI Plugin (`genkit_firebase_ai`) + +The Firebase AI plugin for Genkit Dart, used for interacting with Gemini APIs through Firebase AI Logic. + +## Usage + +```dart +import 'package:genkit/genkit.dart'; +import 'package:genkit_firebase_ai/genkit_firebase_ai.dart'; + +void main() async { + // Initialize Genkit with the Firebase AI plugin + final ai = Genkit(plugins: [firebaseAI()]); + + // Generate text + final response = await ai.generate( + model: firebaseAI.gemini('gemini-2.5-flash'), + prompt: 'Tell me a joke about a developer.', + ); + + print(response.text); +} +``` diff --git a/.agents/skills/developing-genkit-dart/references/genkit_google_genai.md b/.agents/skills/developing-genkit-dart/references/genkit_google_genai.md new file mode 100644 index 0000000..92d3ec4 --- /dev/null +++ b/.agents/skills/developing-genkit-dart/references/genkit_google_genai.md @@ -0,0 +1,95 @@ +# Genkit Google GenAI Plugin (`genkit_google_genai`) + +The Google AI plugin provides an interface against the official Google AI Gemini API. + +## Usage + +```dart +import 'package:genkit/genkit.dart'; +import 'package:genkit_google_genai/genkit_google_genai.dart'; + +void main() async { + // Initialize Genkit with the Google AI plugin + final ai = Genkit(plugins: [googleAI()]); + + // Generate text + final response = await ai.generate( + model: googleAI.gemini('gemini-2.5-flash'), + prompt: 'Tell me a joke about a developer.', + ); + + print(response.text); +} +``` + +## Embeddings + +```dart +final embeddings = await ai.embedMany( + embedder: googleAI.textEmbedding('text-embedding-004'), + documents: [ + DocumentData(content: [TextPart(text: 'Hello world')]), + ], +); +``` + +## Image Generation + +The plugin also supports image generation models such as `gemini-2.5-flash-image`. + +### Example (Nano Banana) + +```dart +// Define an image generation flow +ai.defineFlow( + name: 'imageGenerator', + inputSchema: .string(defaultValue: 'A banana riding a bike'), + outputSchema: Media.$schema, + fn: (input, context) async { + final response = await ai.generate( + model: googleAI.gemini('gemini-2.5-flash-image'), + prompt: input, + ); + if (response.media == null) { + throw Exception('No media generated'); + } + return response.media!; + }, +); +``` + +The media (url field) contain base64 encoded data uri. You can decode it and save it as a file. + +## Text-to-Speech (TTS) + +You can use text-to-speech models to generate audio from text. The generated `Media` object will contain base64 encoded PCM audio in its data URI. + +```dart +// Define a TTS flow +ai.defineFlow( + name: 'textToSpeech', + inputSchema: .string(defaultValue: 'Genkit is an amazing AI framework!'), + outputSchema: Media.$schema, + fn: (prompt, _) async { + final response = await ai.generate( + model: googleAI.gemini('gemini-2.5-flash-preview-tts'), + prompt: prompt, + config: GeminiTtsOptions( + responseModalities: ['AUDIO'], + speechConfig: SpeechConfig( + voiceConfig: VoiceConfig( + prebuiltVoiceConfig: PrebuiltVoiceConfig(voiceName: 'Puck'), + ), + ), + ), + ); + + if (response.media != null) { + return response.media!; + } + throw Exception('No audio generated'); + }, +); +``` + +Google AI also supports multi-speaker TTS by configuring a `MultiSpeakerVoiceConfig` inside `SpeechConfig`. diff --git a/.agents/skills/developing-genkit-dart/references/genkit_mcp.md b/.agents/skills/developing-genkit-dart/references/genkit_mcp.md new file mode 100644 index 0000000..ce8ddb0 --- /dev/null +++ b/.agents/skills/developing-genkit-dart/references/genkit_mcp.md @@ -0,0 +1,115 @@ +# Genkit MCP (`genkit_mcp`) + +MCP (Model Context Protocol) integration for Genkit Dart. + +## MCP Host (Recommended) +Connect to one or more MCP servers and aggregate their capabilities into the Genkit registry automatically. + +```dart +import 'package:genkit/genkit.dart'; +import 'package:genkit_mcp/genkit_mcp.dart'; + +void main() async { + final ai = Genkit(); + + final host = defineMcpHost( + ai, + McpHostOptionsWithCache( + name: 'my-host', + mcpServers: { + 'fs': McpServerConfig( + command: 'npx', + args: ['-y', '@modelcontextprotocol/server-filesystem', '.'], + ), + }, + ), + ); + + // Tools can be discovered and executed dynamically using a wildcard... + final response = await ai.generate( + model: 'gemini-2.5-flash', + prompt: 'Summarize the contents of README.md', + toolNames: ['my-host:tool/fs/*'], + ); + + // ...or by specifying the exact tool name + final exactResponse = await ai.generate( + model: 'gemini-2.5-flash', + prompt: 'Read README.md', + toolNames: ['my-host:tool/fs/read_file'], + ); +} +``` + +## MCP Client (Advanced / Single Server) +Connecting to a single MCP server with a client object is an advanced usecase for when you need manual control over the client lifecycle. Standalone clients do not automatically register tools into the registry, so they must be passed into `generate` or `defineDynamicActionProvider` manually. + +```dart +import 'package:genkit/genkit.dart'; +import 'package:genkit_mcp/genkit_mcp.dart'; + +void main() async { + final ai = Genkit(); + + final client = createMcpClient( + McpClientOptions( + name: 'my-client', + mcpServer: McpServerConfig( + command: 'npx', + args: ['-y', '@modelcontextprotocol/server-filesystem', '.'], + ), + ), + ); + + await client.ready(); + + // Retrieve the tools from the connected client + final tools = await client.getActiveTools(ai); + + final response = await ai.generate( + model: 'gemini-2.5-flash', + prompt: 'Read the contents of README.md', + tools: tools, + ); +} +``` + +## MCP Server +Expose Genkit actions (tools, prompts, resources) over MCP. + +```dart +import 'package:genkit/genkit.dart'; +import 'package:genkit_mcp/genkit_mcp.dart'; + +void main() async { + final ai = Genkit(); + + ai.defineTool( + name: 'add', + description: 'Add two numbers together', + inputSchema: .map(.string(), .dynamicSChema()), + fn: (input, _) async => (input['a'] + input['b']).toString(), + ); + + ai.defineResource( + name: 'my-resource', + uri: 'my://resource', + fn: (_, _) async => ResourceOutput(content: [TextPart(text: 'my resource')]), + ); + + // Stdio transport by default + final server = createMcpServer(ai, McpServerOptions(name: 'my-server')); + await server.start(); +} +``` + +### Streamable HTTP Transport +```dart +import 'dart:io'; + +final transport = await StreamableHttpServerTransport.bind( + address: InternetAddress.loopbackIPv4, + port: 3000, +); +await server.start(transport); +``` diff --git a/.agents/skills/developing-genkit-dart/references/genkit_middleware.md b/.agents/skills/developing-genkit-dart/references/genkit_middleware.md new file mode 100644 index 0000000..24cff79 --- /dev/null +++ b/.agents/skills/developing-genkit-dart/references/genkit_middleware.md @@ -0,0 +1,84 @@ +# Genkit Middleware (`genkit_middleware`) + +A collection of useful middleware for Genkit Dart to enhance your agent's capabilities. Register plugins when initializing Genkit: + +```dart +import 'package:genkit/genkit.dart'; +import 'package:genkit_middleware/genkit_middleware.dart'; + +void main() { + final ai = Genkit( + plugins: [ + FilesystemPlugin(), + SkillsPlugin(), + ToolApprovalPlugin(), + ], + ); +} +``` + +## Filesystem Middleware +Allows the agent to list, read, write, and search/replace files within a restricted root directory. + +```dart +final response = await ai.generate( + prompt: 'Check the logs in the current directory.', + use: [ + filesystem(rootDirectory: '/path/to/secure/workspace'), + ], +); +``` + +**Tools Provided:** +- `list_files`, `read_file`, `write_file`, `search_and_replace` + +## Skills Middleware +Injects specialized instructions (skills) into the system prompt from `SKILL.md` files located in specified directories. + +```dart +final response = await ai.generate( + prompt: 'Help me debug this issue.', + use: [ + skills(skillPaths: ['/path/to/skills']), + ], +); +``` + +**Tools Provided:** +- `use_skill`: Retrieve the full content of a skill by name. + +## Tool Approval Middleware +Intercepts tool execution for specified tools and requires explicit approval. Returns `FinishReason.interrupted`. + +```dart +final response = await ai.generate( + prompt: 'Delete the database.', + use: [ + // Require approval for all tools EXCEPT those below + toolApproval(approved: ['read_file', 'list_files']), + ], +); + +if (response.finishReason == FinishReason.interrupted) { + final interrupt = response.interrupts.first; + + // Ask user for approval + final isApproved = await askUser(); + + if (isApproved) { + final resumeResponse = await ai.generate( + messages: response.messages, // Pass history + toolChoice: ToolChoice.none, // Prevent immediate re-call + interruptRestart: [ + ToolRequestPart( + toolRequest: interrupt.toolRequest, + metadata: { + ...?interrupt.metadata, + 'tool-approved': true + }, + ), + ], + ); + } +} +``` diff --git a/.agents/skills/developing-genkit-dart/references/genkit_openai.md b/.agents/skills/developing-genkit-dart/references/genkit_openai.md new file mode 100644 index 0000000..42344db --- /dev/null +++ b/.agents/skills/developing-genkit-dart/references/genkit_openai.md @@ -0,0 +1,54 @@ +# Genkit OpenAI Plugin (`genkit_openai`) + +OpenAI-compatible API plugin for Genkit Dart. Supports OpenAI models and other compatible APIs (xAI, DeepSeek, Together AI, Groq, etc.). + +## Basic Usage + +```dart +import 'dart:io'; +import 'package:genkit/genkit.dart'; +import 'package:genkit_openai/genkit_openai.dart'; + +void main() async { + final ai = Genkit(plugins: [ + openAI(apiKey: Platform.environment['OPENAI_API_KEY']), + ]); + + final response = await ai.generate( + model: openAI.model('gpt-4o'), + prompt: 'Tell me a joke.', + ); +} +``` + +## Options + +`OpenAIOptions` allows configuring sampling temperature, nucleus sampling, token generation, seed, etc: +`config: OpenAIOptions(temperature: 0.7, maxTokens: 100)` + +## Groq API override + +Specify custom `baseUrl` and custom models to integrate with third-party providers. + +```dart +final ai = Genkit(plugins: [ + openAI( + apiKey: Platform.environment['GROQ_API_KEY'], + baseUrl: 'https://api.groq.com/openai/v1', + models: [ + CustomModelDefinition( + name: 'llama-3.3-70b-versatile', + info: ModelInfo( + label: 'Llama 3.3 70B', + supports: {'multiturn': true, 'tools': true, 'systemRole': true}, + ), + ), + ], + ), +]); + +final response = await ai.generate( + model: openAI.model('llama-3.3-70b-versatile'), + prompt: 'Hello!', +); +``` diff --git a/.agents/skills/developing-genkit-dart/references/genkit_shelf.md b/.agents/skills/developing-genkit-dart/references/genkit_shelf.md new file mode 100644 index 0000000..1887f80 --- /dev/null +++ b/.agents/skills/developing-genkit-dart/references/genkit_shelf.md @@ -0,0 +1,59 @@ +# Genkit Shelf Plugin (`genkit_shelf`) + +Shelf integration for Genkit Dart, used to serve Genkit Flows. + +## Standalone Server +Serve Genkit Flows easily on an isolated HTTP server using `startFlowServer`. + +```dart +import 'package:genkit/genkit.dart'; +import 'package:genkit_shelf/genkit_shelf.dart'; + +void main() async { + final ai = Genkit(); + + final flow = ai.defineFlow( + name: 'myFlow', + inputSchema: .string(), + outputSchema: .string(), + fn: (String input, _) async => 'Hello $input', + ); + + await startFlowServer( + flows: [flow], + port: 8080, + ); +} +``` + +## Existing Shelf Application +Mount Genkit Flow endpoints directly to an existing Shelf `Router` using `shelfHandler`. + +```dart +import 'package:genkit/genkit.dart'; +import 'package:genkit_shelf/genkit_shelf.dart'; +import 'package:shelf/shelf.dart'; +import 'package:shelf/shelf_io.dart' as io; +import 'package:shelf_router/shelf_router.dart'; + +void main() async { + final ai = Genkit(); + + final flow = ai.defineFlow( + name: 'myFlow', + inputSchema: .string(), + outputSchema: .string(), + fn: (String input, _) async => 'Hello $input', + ); + + final router = Router(); + + // Mount the flow handler at a specific path + router.post('/myFlow', shelfHandler(flow)); + + // Start the server + await io.serve(router.call, 'localhost', 8080); +} +``` + +Access deployed flows using genkit client libraries (from Dart or JS). diff --git a/.agents/skills/developing-genkit-dart/references/schemantic.md b/.agents/skills/developing-genkit-dart/references/schemantic.md new file mode 100644 index 0000000..45939b2 --- /dev/null +++ b/.agents/skills/developing-genkit-dart/references/schemantic.md @@ -0,0 +1,137 @@ +# Schemantic + +Schemantic is a general-purpose Dart library used for defining strongly typed data classes that automatically bind to reusable runtime JSON schemas. It is standard for the `genkit-dart` framework but works independently as well. + +## Core Concepts + +Always use `schemantic` when strongly typed JSON parsing or programmatic schema validation is required. + +- Annotate your abstract classes with `@Schema()`. +- Use the `$` prefix for abstract schema class names (e.g., `abstract class $User`). +- Always run `dart run build_runner build` to generate the `.g.dart` schema files. + +## Installation + +Add dependencies: + +```bash +dart pub add schemantic +``` + +## Basic Usage + +1. **Defining a schema:** + +```dart +import 'package:schemantic/schemantic.dart'; + +part 'my_file.g.dart'; // Must match the filename + +@Schema() +abstract class $MyObj { + String get name; + $MySubObj get subObj; +} + +@Schema() +abstract class $MySubObj { + String get foo; +} +``` + +2. **Using the Generated Class:** + +The builder creates a concrete class `MyObj` (no `$`) with a factory constructor (`MyObj.fromJson`) and a regular constructor. + +```dart +// Creating an instance +final obj = MyObj(name: 'test', subObj: MySubObj(foo: 'bar')); + +// Serializing to JSON +print(obj.toJson()); + +// Parsing from JSON +final parsed = MyObj.fromJson({'name': 'test', 'subObj': {'foo': 'bar'}}); +``` + +3. **Accessing Schemas at Runtime:** + +The generated data classes have a static `$schema` field (of type `SchemanticType`) which can be used to pass the definition into functions or to extract the raw JSON schema. + +```dart +// Access JSON schema +final schema = MyObj.$schema.jsonSchema; +print(schema.toJson()); + +// Validate arbitrary JSON at runtime +final validationErrors = await schema.validate({'invalid': 'data'}); +``` + +## Primitive Schemas + +When a full data class is not required, Schemantic provides functions to create schemas dynamically. + +```dart +final ageSchema = SchemanticType.integer(description: 'Age in years', minimum: 0); +final nameSchema = SchemanticType.string(minLength: 2); +final nothingSchema = SchemanticType.voidSchema(); +final anySchema = SchemanticType.dynamicSchema(); + +final userSchema = SchemanticType.map(.string(), .integer()); // Map +final tagsSchema = SchemanticType.list(.string()); // List +``` + +## Union Types (AnyOf) + +To allow a field to accept multiple types, use `@AnyOf`. + +```dart +@Schema() +abstract class $Poly { + @AnyOf([int, String, $MyObj]) + Object? get id; +} +``` + +Schemantic generates a specific helper class (e.g., `PolyId`) to handle the values: + +```dart +final poly1 = Poly(id: PolyId.int(123)); +final poly2 = Poly(id: PolyId.string('abc')); +``` + +## Field Annotations + +You can use specialized annotations for more validation boundaries: + +```dart +@Schema() +abstract class $User { + @IntegerField( + name: 'years_old', // Change JSON key + description: 'Age of the user', + minimum: 0, + defaultValue: 18, + ) + int? get age; + + @StringField( + minLength: 2, + enumValues: ['user', 'admin'], + ) + String get role; +} +``` + +## Recursive Schemas + +For recursive structures (like trees), must use `useRefs: true` inside the generated jsonSchema property. You define it normally: + +```dart +@Schema() +abstract class $Node { + String get id; + List<$Node>? get children; +} +``` +*Note*: `Node.$schema.jsonSchema(useRefs: true)` generates schemas with JSON Schema `$ref`. \ No newline at end of file diff --git a/.agents/skills/developing-genkit-go/SKILL.md b/.agents/skills/developing-genkit-go/SKILL.md new file mode 100644 index 0000000..13e86c9 --- /dev/null +++ b/.agents/skills/developing-genkit-go/SKILL.md @@ -0,0 +1,99 @@ +--- +name: developing-genkit-go +description: Develop AI-powered applications using Genkit in Go. Use when the user asks to build AI features, agents, flows, or tools in Go using Genkit, or when working with Genkit Go code involving generation, prompts, streaming, tool calling, or model providers. +metadata: + genkit-managed: true +--- + +# Genkit Go + +Genkit Go is an AI SDK for Go that provides generation, structured output, streaming, tool calling, prompts, and flows with a unified interface across model providers. + +## Hello World + +```go +package main + +import ( + "context" + "fmt" + "log" + "net/http" + + "github.com/genkit-ai/genkit/go/ai" + "github.com/genkit-ai/genkit/go/genkit" + "github.com/genkit-ai/genkit/go/plugins/googlegenai" + "github.com/genkit-ai/genkit/go/plugins/server" +) + +func main() { + ctx := context.Background() + g := genkit.Init(ctx, genkit.WithPlugins(&googlegenai.GoogleAI{})) + + genkit.DefineFlow(g, "jokeFlow", func(ctx context.Context, topic string) (string, error) { + return genkit.GenerateText(ctx, g, + ai.WithModelName("googleai/gemini-flash-latest"), + ai.WithPrompt("Tell me a joke about %s", topic), + ) + }) + + mux := http.NewServeMux() + for _, f := range genkit.ListFlows(g) { + mux.HandleFunc("POST /"+f.Name(), genkit.Handler(f)) + } + log.Fatal(server.Start(ctx, "127.0.0.1:8080", mux)) +} +``` + +## Core Features + +Load the appropriate reference based on what you need: + +| Feature | Reference | When to load | +| --- | --- | --- | +| Initialization | [references/getting-started.md](references/getting-started.md) | Setting up `genkit.Init`, plugins, the `*Genkit` instance pattern | +| Generation | [references/generation.md](references/generation.md) | `Generate`, `GenerateText`, `GenerateData`, streaming, output formats | +| Prompts | [references/prompts.md](references/prompts.md) | `DefinePrompt`, `DefineDataPrompt`, `.prompt` files, schemas | +| Tools | [references/tools.md](references/tools.md) | `DefineTool`, tool interrupts, `RestartWith`/`RespondWith` | +| Middleware | [references/middleware.md](references/middleware.md) | `ai.Middleware`, `ai.WithUse`, `Hooks` (Generate/Model/Tool), built-ins (`Retry`, `Fallback`, `ToolApproval`, `Filesystem`, `Skills`) | +| Flows & HTTP | [references/flows-and-http.md](references/flows-and-http.md) | `DefineFlow`, `DefineStreamingFlow`, `genkit.Handler`, HTTP serving | +| Model Providers | [references/providers.md](references/providers.md) | Google AI, Vertex AI, Anthropic, OpenAI-compatible, Ollama setup | + +## Genkit CLI + +Check if installed: `genkit --version` + +**Installation:** +```bash +curl -sL cli.genkit.dev | bash +``` + +**Key commands:** + +```bash +# Start app with Developer UI (tracing, flow testing) at http://localhost:4000 +genkit start -- go run . +genkit start -o -- go run . # also opens browser + +# Run a flow directly from the CLI +genkit flow:run myFlow '{"data": "input"}' +genkit flow:run myFlow '{"data": "input"}' --stream # with streaming +genkit flow:run myFlow '{"data": "input"}' --wait # wait for completion + +# Look up Genkit documentation +genkit docs:search "streaming" go +genkit docs:list go +genkit docs:read go/flows.md +``` + +See [references/getting-started.md](references/getting-started.md) for full CLI and Developer UI details. + +## Key Guidance + +- **Pass `g` explicitly.** The `*Genkit` instance returned by `genkit.Init` is the central registry. Pass it to all Genkit functions rather than storing it as a global. This is a core pattern throughout the SDK. +- **Wrap AI logic in flows.** Flows give you tracing, observability, HTTP deployment via `genkit.Handler`, and the ability to test from the Developer UI and CLI. Any generation call worth keeping should live in a flow. +- **Use `jsonschema:"description=..."` struct tags on output types.** The model uses these descriptions to understand what each field should contain. Without them, structured output quality drops significantly. +- **Write good tool descriptions.** The model decides which tools to call based on their description string. Vague descriptions lead to missed or incorrect tool calls. +- **Use `.prompt` files for complex prompts.** They separate prompt content from Go code, support Handlebars templating, and can be iterated on without recompilation. Code-defined prompts are better for simple, single-line cases. +- **Reach for built-in middleware before writing one.** `Retry`, `Fallback`, `ToolApproval`, `Filesystem`, and `Skills` cover the common cross-cutting needs and compose with each other via `ai.WithUse`. See [references/middleware.md](references/middleware.md). When you do write custom middleware, allocate per-call state in closures captured by `New`, and guard anything that `WrapTool` mutates because tools may run concurrently. +- **Look up the latest model IDs.** Model names change frequently. Check provider documentation for current model IDs rather than relying on hardcoded names. See [references/providers.md](references/providers.md). diff --git a/.agents/skills/developing-genkit-go/references/flows-and-http.md b/.agents/skills/developing-genkit-go/references/flows-and-http.md new file mode 100644 index 0000000..92f99a2 --- /dev/null +++ b/.agents/skills/developing-genkit-go/references/flows-and-http.md @@ -0,0 +1,183 @@ +# Flows & HTTP + +## DefineFlow + +Wrap AI logic in a flow for observability, tracing, and HTTP deployment. + +```go +jokeFlow := genkit.DefineFlow(g, "jokeFlow", + func(ctx context.Context, topic string) (string, error) { + return genkit.GenerateText(ctx, g, + ai.WithModelName("googleai/gemini-flash-latest"), + ai.WithPrompt("Tell me a joke about %s", topic), + ) + }, +) +``` + +### Running a Flow Directly + +```go +result, err := jokeFlow.Run(ctx, "cats") +``` + +## DefineStreamingFlow + +Flows that stream chunks back to the caller. Two common patterns: + +### Pattern 1: Passthrough Streaming + +Pass the stream callback directly through to `WithStreaming`. The callback type is `ai.ModelStreamCallback` = `func(context.Context, *ai.ModelResponseChunk) error`: + +```go +genkit.DefineStreamingFlow(g, "streamingJokeFlow", + func(ctx context.Context, topic string, sendChunk ai.ModelStreamCallback) (string, error) { + resp, err := genkit.Generate(ctx, g, + ai.WithModelName("googleai/gemini-flash-latest"), + ai.WithPrompt("Tell me a long joke about %s", topic), + ai.WithStreaming(sendChunk), // passthrough + ) + if err != nil { + return "", err + } + return resp.Text(), nil + }, +) +``` + +### Pattern 2: Manual String Streaming + +Use `core.StreamCallback[string]` to stream extracted text: + +```go +genkit.DefineStreamingFlow(g, "streamingJokeFlow", + func(ctx context.Context, topic string, sendChunk core.StreamCallback[string]) (string, error) { + stream := genkit.GenerateStream(ctx, g, + ai.WithModelName("googleai/gemini-flash-latest"), + ai.WithPrompt("Tell me a long joke about %s", topic), + ) + for result, err := range stream { + if err != nil { + return "", err + } + if result.Done { + return result.Response.Text(), nil + } + sendChunk(ctx, result.Chunk.Text()) + } + return "", nil + }, +) +``` + +### Typed Streaming Flows + +Use `core.StreamCallback[T]` with `GenerateDataStream` for typed chunks: + +```go +genkit.DefineStreamingFlow(g, "structuredStream", + func(ctx context.Context, input JokeRequest, sendChunk core.StreamCallback[*Joke]) (*Joke, error) { + stream := genkit.GenerateDataStream[*Joke](ctx, g, + ai.WithModelName("googleai/gemini-flash-latest"), + ai.WithPrompt("Tell me a joke about %s", input.Topic), + ) + for result, err := range stream { + if err != nil { return nil, err } + if result.Done { return result.Output, nil } + sendChunk(ctx, result.Chunk) + } + return nil, nil + }, +) +``` + +## Named Sub-Steps + +Use `core.Run` inside a flow for traced sub-steps: + +```go +genkit.DefineFlow(g, "pipeline", + func(ctx context.Context, input string) (string, error) { + subject, err := core.Run(ctx, "extract-subject", func() (string, error) { + return genkit.GenerateText(ctx, g, + ai.WithPrompt("Extract the subject from: %s", input), + ) + }) + if err != nil { return "", err } + + joke, err := core.Run(ctx, "generate-joke", func() (string, error) { + return genkit.GenerateText(ctx, g, + ai.WithPrompt("Tell me a joke about %s", subject), + ) + }) + return joke, err + }, +) +``` + +## HTTP Handlers + +### genkit.Handler + +Convert any flow into an `http.HandlerFunc`: + +```go +mux := http.NewServeMux() +for _, f := range genkit.ListFlows(g) { + mux.HandleFunc("POST /"+f.Name(), genkit.Handler(f)) +} +log.Fatal(server.Start(ctx, "127.0.0.1:8080", mux)) +``` + +### Request/Response Format + +**Non-streaming request:** +```bash +curl -X POST http://localhost:8080/jokeFlow \ + -H "Content-Type: application/json" \ + -d '{"data": "bananas"}' +``` + +Response: `{"result": "Why did the banana go to the doctor?..."}` + +**Streaming request:** +```bash +curl -N -X POST http://localhost:8080/streamingJokeFlow \ + -H "Content-Type: application/json" \ + -d '{"data": "bananas"}' +``` + +Streaming responses use Server-Sent Events (SSE) format. + +### genkit.HandlerFunc + +For frameworks that expect error-returning handlers: + +```go +handler := genkit.HandlerFunc(myFlow) +// handler is func(http.ResponseWriter, *http.Request) error +``` + +### Context Providers + +Inject request context (e.g., auth headers) into flow execution: + +```go +mux.HandleFunc("POST /myFlow", genkit.Handler(myFlow, + genkit.WithContextProviders(func(ctx context.Context, rd core.RequestData) (api.ActionContext, error) { + // rd.Headers contains HTTP headers + return api.ActionContext{"userId": rd.Headers.Get("X-User-Id")}, nil + }), +)) +``` + +### ListFlows + +Get all registered flows for dynamic route setup: + +```go +flows := genkit.ListFlows(g) // []api.Action +for _, f := range flows { + fmt.Println(f.Name()) +} +``` diff --git a/.agents/skills/developing-genkit-go/references/generation.md b/.agents/skills/developing-genkit-go/references/generation.md new file mode 100644 index 0000000..5934575 --- /dev/null +++ b/.agents/skills/developing-genkit-go/references/generation.md @@ -0,0 +1,176 @@ +# Generation + +## GenerateText + +Simplest form. Returns a string. + +```go +text, err := genkit.GenerateText(ctx, g, + ai.WithModelName("googleai/gemini-flash-latest"), + ai.WithPrompt("Tell me a joke about %s", topic), +) +``` + +## Generate + +Returns a full `*ModelResponse` with metadata, usage stats, and history. + +```go +resp, err := genkit.Generate(ctx, g, + ai.WithModelName("googleai/gemini-flash-latest"), + ai.WithSystem("You are a helpful assistant."), + ai.WithPrompt("Explain %s", topic), +) +fmt.Println(resp.Text()) // concatenated text +fmt.Println(resp.FinishReason) // ai.FinishReasonStop, etc. +fmt.Println(resp.Usage) // token counts +``` + +## GenerateData (Structured Output) + +Returns a typed Go value parsed from the model's JSON output. + +```go +type Joke struct { + Setup string `json:"setup" jsonschema:"description=The setup of the joke"` + Punchline string `json:"punchline" jsonschema:"description=The punchline"` +} + +joke, resp, err := genkit.GenerateData[Joke](ctx, g, + ai.WithModelName("googleai/gemini-flash-latest"), + ai.WithPrompt("Tell me a joke about %s", topic), +) +// joke is *Joke, resp is *ModelResponse +``` + +## Streaming + +### GenerateStream + +Returns an iterator. Each value has `.Done`, `.Chunk`, and `.Response`. + +```go +stream := genkit.GenerateStream(ctx, g, + ai.WithModelName("googleai/gemini-flash-latest"), + ai.WithPrompt("Tell me a long story about %s", topic), +) +for result, err := range stream { + if err != nil { + return err + } + if result.Done { + finalText := result.Response.Text() + break + } + fmt.Print(result.Chunk.Text()) // incremental text +} +``` + +### GenerateDataStream (Structured Streaming) + +Streams typed partial objects as they arrive. + +```go +stream := genkit.GenerateDataStream[Joke](ctx, g, + ai.WithModelName("googleai/gemini-flash-latest"), + ai.WithPrompt("Tell me a joke about %s", topic), +) +for result, err := range stream { + if err != nil { + return err + } + if result.Done { + finalJoke := result.Output // *Joke + break + } + partialJoke := result.Chunk // *Joke (partial) +} +``` + +### Callback-Based Streaming + +Use `ai.WithStreaming` with `Generate` for callback-style streaming. The callback receives `*ai.ModelResponseChunk`: + +```go +resp, err := genkit.Generate(ctx, g, + ai.WithModelName("googleai/gemini-flash-latest"), + ai.WithPrompt("Tell me a story"), + ai.WithStreaming(func(ctx context.Context, chunk *ai.ModelResponseChunk) error { + fmt.Print(chunk.Text()) // extract text from chunk + return nil + }), +) +// resp contains the final complete response +``` + +## Common Options + +```go +// Model selection +ai.WithModel(googlegenai.ModelRef("googleai/gemini-flash-latest", nil)) // model reference +ai.WithModelName("googleai/gemini-flash-latest") // by name string + +// Content +ai.WithPrompt("Tell me about %s", topic) // user message (supports fmt verbs) +ai.WithSystem("You are a pirate.") // system instructions +ai.WithMessages(msg1, msg2) // conversation history +ai.WithDocs(doc1, doc2) // context documents +ai.WithTextDocs("context 1", "context 2") // context as strings + +// Model config (provider-specific) +ai.WithConfig(map[string]any{"temperature": 0.7}) +``` + +## Output Formats + +Control how the model structures its output. + +### By Go Type + +```go +// Automatically uses JSON format and instructs model to match the type +ai.WithOutputType(MyStruct{}) +``` + +### By Format String + +```go +ai.WithOutputFormat(ai.OutputFormatJSON) // single JSON object +ai.WithOutputFormat(ai.OutputFormatJSONL) // JSON Lines (one object per line) +ai.WithOutputFormat(ai.OutputFormatArray) // JSON array +ai.WithOutputFormat(ai.OutputFormatEnum) // constrained enum value +ai.WithOutputFormat(ai.OutputFormatText) // plain text (default) +``` + +### Enum Output + +```go +type Color string +const ( + Red Color = "red" + Green Color = "green" + Blue Color = "blue" +) + +text, err := genkit.GenerateText(ctx, g, + ai.WithPrompt("What color is the sky?"), + ai.WithOutputEnums(Red, Green, Blue), +) +``` + +### Custom Output Instructions + +```go +ai.WithOutputInstructions("Return a JSON object with fields: name (string), age (number)") +``` + +### Combining Format + Schema + +```go +// JSONL with a typed schema (useful for streaming lists) +genkit.DefinePrompt(g, "characters", + ai.WithPrompt("Generate 5 story characters"), + ai.WithOutputType([]StoryCharacter{}), + ai.WithOutputFormat(ai.OutputFormatJSONL), +) +``` diff --git a/.agents/skills/developing-genkit-go/references/getting-started.md b/.agents/skills/developing-genkit-go/references/getting-started.md new file mode 100644 index 0000000..7520c92 --- /dev/null +++ b/.agents/skills/developing-genkit-go/references/getting-started.md @@ -0,0 +1,140 @@ +# Getting Started + +## Project Setup + +```bash +mkdir my-genkit-app && cd my-genkit-app +go mod init my-genkit-app +go get github.com/genkit-ai/genkit/go@latest +``` + +Provider plugins ship in the same module under `plugins/`, so they don't need to be fetched separately. Just import the ones you want and run `go mod tidy` afterwards. The available plugins include: + +- `plugins/googlegenai` for Google AI and Vertex AI +- `plugins/anthropic` for Anthropic Claude +- `plugins/compat_oai` for OpenAI-compatible APIs (OpenAI, Groq, xAI, etc.) +- `plugins/ollama` for local Ollama models +- `plugins/middleware` for the built-in middleware bundle (`Retry`, `Fallback`, `ToolApproval`, `Filesystem`, `Skills`) + +## Initialization + +Every Genkit app starts with `genkit.Init`, which returns a `*Genkit` instance: + +```go +import ( + "context" + "github.com/genkit-ai/genkit/go/genkit" + "github.com/genkit-ai/genkit/go/plugins/googlegenai" +) + +ctx := context.Background() +g := genkit.Init(ctx, + genkit.WithPlugins(&googlegenai.GoogleAI{}), +) +``` + +### The `*Genkit` Instance + +The `*Genkit` value `g` is the central registry. Pass it to every Genkit function: + +```go +// Defining resources +genkit.DefineFlow(g, "myFlow", ...) +genkit.DefineTool(g, "myTool", ...) +genkit.DefinePrompt(g, "myPrompt", ...) + +// Generating content +genkit.GenerateText(ctx, g, ...) +genkit.Generate(ctx, g, ...) +``` + +Do not store `g` in a global variable. Pass it explicitly through your call chain. + +### Init Options + +```go +g := genkit.Init(ctx, + // Register one or more plugins + genkit.WithPlugins(&googlegenai.GoogleAI{}, &anthropic.Anthropic{}), + + // Set a default model (used when no model is specified) + genkit.WithDefaultModel("googleai/gemini-flash-latest"), + + // Set directory for .prompt files (default: "prompts") + genkit.WithPromptDir("my-prompts"), + + // Or embed prompts using Go's embed package + // genkit.WithPromptFS(promptsFS), +) +``` + +### Embedding Prompts + +Use `go:embed` to bundle `.prompt` files into the binary: + +```go +//go:embed prompts +var promptsFS embed.FS + +g := genkit.Init(ctx, + genkit.WithPlugins(&googlegenai.GoogleAI{}), + genkit.WithPromptFS(promptsFS), +) +``` + +## Genkit CLI + +The Genkit CLI provides a local Developer UI for running flows, tracing executions, and inspecting model interactions. + +**Install:** +```bash +curl -sL cli.genkit.dev | bash +``` + +**Verify:** +```bash +genkit --version +``` + +### Developer UI + +Start your app with the Developer UI attached: + +```bash +genkit start -- go run . +``` + +This launches: +- Your app (with tracing enabled) +- The Developer UI at `http://localhost:4000` +- A telemetry API at `http://localhost:4033` + +Add `-o` to auto-open the UI in your browser: +```bash +genkit start -o -- go run . +``` + +The Developer UI lets you: +- Run and test flows interactively +- View traces for each generation call (inputs, outputs, latency, token usage) +- Inspect prompt rendering and tool calls +- Debug multi-step flows with per-step trace data + +### Without the CLI + +Set `GENKIT_ENV=dev` to enable the reflection API without the CLI: + +```bash +GENKIT_ENV=dev go run . +``` + +## Import Paths + +```go +import ( + "github.com/genkit-ai/genkit/go/genkit" // Core: Init, Generate*, DefineFlow, etc. + "github.com/genkit-ai/genkit/go/ai" // Types: WithModel, WithPrompt, Message, Part, etc. + "github.com/genkit-ai/genkit/go/core" // Low-level: Run (sub-steps), Flow types + "github.com/genkit-ai/genkit/go/plugins/server" // server.Start for HTTP +) +``` diff --git a/.agents/skills/developing-genkit-go/references/middleware.md b/.agents/skills/developing-genkit-go/references/middleware.md new file mode 100644 index 0000000..d93eb4e --- /dev/null +++ b/.agents/skills/developing-genkit-go/references/middleware.md @@ -0,0 +1,363 @@ +# Middleware + +Middleware wraps `Generate` calls to add cross-cutting behavior (retries, logging, fallback, gating, sandboxed tools) without touching the flow that uses it. Middleware composes, so a single `Generate` call can stack several behaviors. Built-ins ship in the `plugins/middleware` package; custom middleware is just a Go struct with two methods. + +## The mental model + +A middleware is a config struct that implements two methods: + +```go +type Middleware interface { + Name() string // stable, registered identifier + New(ctx context.Context) (*Hooks, error) // produces a per-call hook bundle +} +``` + +The same struct value the user passes to `ai.WithUse` is the value the runtime calls `New` on. There is no separate factory parameter and no embedded base type. Per-call state goes in closures captured by `New`. Plugin-level state goes on unexported fields of the struct. + +`New` is invoked once per `Generate` call. The returned `*Hooks` is reused across every iteration of the tool loop within that call. + +```go +type Hooks struct { + Tools []Tool // injected for this call + WrapGenerate func(ctx, *GenerateParams, GenerateNext) ... // tool-loop iteration + WrapModel func(ctx, *ModelParams, ModelNext) ... // model API call + WrapTool func(ctx, *ToolParams, ToolNext) ... // tool execution +} +``` + +A nil hook is a pass-through. Implement only what the middleware needs. + +## When each hook fires + +A `Generate` call executes a tool loop: model produces output, any tool calls execute, results feed back into a new model call, repeat until the model stops. The hooks fire at three different layers of this loop: + +| Hook | Fires | Sees | +| --- | --- | --- | +| `WrapGenerate` | Once per tool-loop iteration. `N` tool turns means `N+1` invocations. | The accumulated `ModelRequest`, the iteration index, the streaming callback, and `MessageIndex` (the next streamed-message slot). | +| `WrapModel` | Once per actual model API call, inside the iteration. | The `ModelRequest` about to go to the model and the streaming callback. | +| `WrapTool` | Once per tool execution. May run **concurrently** for parallel tool calls in the same iteration. | The `ToolRequest` and the resolved `Tool`. | + +`WrapGenerate` is the right place for logic that needs to see the whole conversation (rewrites, system-prompt injection, message accumulation). `WrapModel` is the right place for logic about the model call itself (retry, fallback, caching). `WrapTool` is the right place for logic about a single tool execution (approval, sandboxing, logging). + +## Composition order + +`ai.WithUse(A, B, C)` expands to `A { B { C { actual } } }` at call time. Each layer's `next` continuation runs the next inner layer: + +```go +ai.WithUse( + &middleware.Retry{MaxRetries: 3}, // outer: retries the whole inner stack + &middleware.Fallback{Models: ...}, // inner: tries fallback models on failure +) +// effective chain: Retry { Fallback { model } } +``` + +Order matters. `Retry` outside `Fallback` retries the entire fallback cascade as a unit. Swapped, you'd retry the primary first and only fall back after exhausting retries. + +## Per-call state + +State that should be shared across the hooks of a single `Generate` call lives in closures captured by `New`. Each `Generate` call gets a fresh `Hooks` bundle, so nothing leaks between calls. + +```go +type Counter struct{} + +func (Counter) Name() string { return "mine/counter" } + +func (Counter) New(ctx context.Context) (*ai.Hooks, error) { + var modelCalls int + return &ai.Hooks{ + WrapModel: func(ctx context.Context, p *ai.ModelParams, next ai.ModelNext) (*ai.ModelResponse, error) { + modelCalls++ + return next(ctx, p) + }, + WrapGenerate: func(ctx context.Context, p *ai.GenerateParams, next ai.GenerateNext) (*ai.ModelResponse, error) { + // The same modelCalls variable is visible here because both closures + // capture it from the enclosing New scope. + resp, err := next(ctx, p) + if err == nil { + log.Printf("iteration %d: %d model calls so far", p.Iteration, modelCalls) + } + return resp, err + }, + }, nil +} +``` + +`WrapTool` may be invoked concurrently for parallel tool calls in the same iteration. Any state it mutates must be guarded: + +```go +func (Counter) New(ctx context.Context) (*ai.Hooks, error) { + var ( + mu sync.Mutex + toolCalls int + ) + return &ai.Hooks{ + WrapTool: func(ctx context.Context, p *ai.ToolParams, next ai.ToolNext) (*ai.MultipartToolResponse, error) { + mu.Lock() + toolCalls++ + mu.Unlock() + return next(ctx, p) + }, + }, nil +} +``` + +`WrapGenerate` and `WrapModel` are not called concurrently within a single `Generate` call. + +## Plugin-level state + +When a middleware needs resources its config can't carry as JSON (an HTTP client, a database handle, a logger), put them on **unexported** fields of the config struct. The plugin sets them on a prototype, and `ai.NewMiddleware` captures that prototype in a closure that value-copies it across JSON-dispatched invocations: + +```go +type Logger struct { + Prefix string `json:"prefix,omitempty"` + out io.Writer // unexported; preserved across JSON dispatch via value-copy +} + +func (Logger) Name() string { return "mine/logger" } + +func (l Logger) New(ctx context.Context) (*ai.Hooks, error) { + return &ai.Hooks{ + WrapModel: func(ctx context.Context, p *ai.ModelParams, next ai.ModelNext) (*ai.ModelResponse, error) { + start := time.Now() + resp, err := next(ctx, p) + fmt.Fprintf(l.out, "%s model call took %s\n", l.Prefix, time.Since(start)) + return resp, err + }, + }, nil +} + +type LoggerPlugin struct{ Out io.Writer } + +func (p *LoggerPlugin) Name() string { return "logger" } +func (p *LoggerPlugin) Init(ctx context.Context) []api.Action { return nil } + +func (p *LoggerPlugin) Middlewares(ctx context.Context) ([]*ai.MiddlewareDesc, error) { + return []*ai.MiddlewareDesc{ + ai.NewMiddleware("logs model call latency", Logger{out: p.Out}), + }, nil +} +``` + +The Dev UI and other-runtime callers send JSON config; the prototype's value copy preserves `out` (unexported, not in JSON) while `Prefix` is overridden by the unmarshaled config. + +## Composition with WithUse + +```go +response, _ := genkit.Generate(ctx, g, + ai.WithModelName("googleai/gemini-flash-latest"), + ai.WithPrompt("Explain quantum computing."), + ai.WithUse( + Logger{Prefix: "[trace]"}, + &middleware.Retry{MaxRetries: 3}, + ), +) +``` + +No registration is required for pure-Go use. `WithUse` calls each value's `New` directly on a fast path; the registry is only consulted for JSON-dispatched calls (Dev UI or cross-runtime). Registration is what makes a middleware visible to the Dev UI and addressable by name. + +## Inline middleware + +For ad-hoc middleware that does not need Dev UI visibility or a named type, use `ai.MiddlewareFunc`: + +```go +ai.WithUse(ai.MiddlewareFunc(func(ctx context.Context) (*ai.Hooks, error) { + return &ai.Hooks{ + WrapModel: func(ctx context.Context, p *ai.ModelParams, next ai.ModelNext) (*ai.ModelResponse, error) { + log.Printf("model call: %s", p.Request.Messages[len(p.Request.Messages)-1].Text()) + return next(ctx, p) + }, + }, nil +})) +``` + +The adapter satisfies `Middleware` with a placeholder `Name()` of `"inline"`. Inline middleware is resolved on the fast path and never touches the registry, so the placeholder name is fine. + +## Application-owned middleware + +Use `genkit.DefineMiddleware` to register a middleware your application owns directly. Registration surfaces it in the Dev UI and lets cross-runtime callers reference it by name: + +```go +genkit.DefineMiddleware(g, "logs model call latency", Logger{out: os.Stderr}) + +// Lookup by name (mostly for inspection / cross-runtime dispatch). +desc := genkit.LookupMiddleware(g, "mine/logger") +``` + +For application code, `DefineMiddleware` is the typical entry point. For plugin authors, `ai.NewMiddleware` (no registration) plus `MiddlewarePlugin.Middlewares()` is the typical entry point. `genkit.Init` registers the returned descriptors automatically. + +## Built-in middleware + +The `plugins/middleware` package bundles five production-ready implementations. Register the plugin once during `Init` to make them visible to the Dev UI: + +```go +import "github.com/genkit-ai/genkit/go/plugins/middleware" + +g := genkit.Init(ctx, genkit.WithPlugins( + &googlegenai.GoogleAI{}, + &middleware.Middleware{}, +)) +``` + +### `Retry` + +Retries failed model API calls with exponential backoff and jitter. Hooks `WrapModel`. + +```go +ai.WithUse(&middleware.Retry{ + MaxRetries: 3, // default 3 + InitialDelayMs: 1000, // default 1000 + MaxDelayMs: 60000, // default 60000 + BackoffFactor: 2, // default 2 + NoJitter: false, // default false + // Statuses (default: UNAVAILABLE, DEADLINE_EXCEEDED, RESOURCE_EXHAUSTED, ABORTED, INTERNAL) + // Statuses: []core.StatusName{core.UNAVAILABLE, core.RESOURCE_EXHAUSTED}, +}) +``` + +Non-`GenkitError` errors (network, parse, etc.) are always retried. `GenkitError` errors are retried only if their status is in `Statuses`. The backoff respects `ctx.Done()`: a canceled context aborts the retry loop with the last error. + +### `Fallback` + +Tries alternative models when the primary fails with a fallback-eligible status. Hooks `WrapModel`. + +```go +ai.WithUse(&middleware.Fallback{ + Models: []ai.ModelRef{ + googlegenai.ModelRef("googleai/gemini-flash-latest", nil), + googlegenai.ModelRef("vertexai/gemini-flash-latest", nil), + }, + // default Statuses: UNAVAILABLE, DEADLINE_EXCEEDED, RESOURCE_EXHAUSTED, + // ABORTED, INTERNAL, NOT_FOUND, UNIMPLEMENTED +}) +``` + +Each fallback model uses its own `ModelRef.Config()` verbatim; the original request's config is **not** inherited. Compose with `Retry` outside to retry the whole cascade, or inside to retry just the primary before falling back. + +### `ToolApproval` + +Interrupts any tool call not in the allow list, exposing approval as a human-in-the-loop step. Hooks `WrapTool`. + +```go +ai.WithUse(&middleware.ToolApproval{ + AllowedTools: []string{"lookup", "search"}, // anything else triggers an interrupt +}) +``` + +The interrupt rides on the existing tool-interrupt machinery. Approve a call by setting `toolApproved: true` in the resume metadata when restarting: + +```go +restart, _ := tool.Restart(interruptPart, &ai.RestartOptions{ + ResumedMetadata: map[string]any{"toolApproved": true}, +}) +genkit.Generate(ctx, g, ai.WithMessages(resp.History()...), ai.WithToolRestarts(restart)) +``` + +A bare resume without that flag is **not** treated as approval, so unrelated resume flows can't bypass gating. + +### `Filesystem` + +Grants the model scoped file access under a single root directory via `list_files`, `read_file`, plus `write_file` and `search_and_replace` when writes are enabled. Hooks `WrapGenerate` and `WrapTool` and contributes `Tools`. + +```go +ai.WithUse(&middleware.Filesystem{ + RootDir: "./workspace", + AllowWriteAccess: true, + ToolNamePrefix: "", // set distinct prefixes if attaching multiple Filesystem middlewares +}) +``` + +Path safety is enforced by `os.Root` (Go 1.25+), which rejects any path resolving outside the root, including via `..`, absolute paths, or symlinks. `read_file` returns its content as a queued user message on the next turn (not as the tool's direct output) so binary types like images can be inlined as media parts. + +### `Skills` + +Exposes a local library of `SKILL.md` files as loadable system instructions. Hooks `WrapGenerate` and contributes a `use_skill` tool. + +```go +ai.WithUse(&middleware.Skills{SkillPaths: []string{"skills"}}) // default: ["skills"] +``` + +A skill is a directory containing `SKILL.md`, optionally with YAML frontmatter (`name`, `description`). The middleware injects a system prompt listing available skills, and the model calls `use_skill("name")` to pull the skill body into the conversation on demand. Heavier persona instructions stay off the hot path until actually loaded. + +## Practical patterns + +### Streaming-aware middleware + +If your `WrapGenerate` or `WrapModel` hook emits its own messages (injected user content, system updates), use the streaming callback and the `MessageIndex` cursor in `GenerateParams`: + +```go +WrapGenerate: func(ctx context.Context, p *ai.GenerateParams, next ai.GenerateNext) (*ai.ModelResponse, error) { + if p.Callback != nil { + _ = p.Callback(ctx, &ai.ModelResponseChunk{ + Role: ai.RoleUser, + Index: p.MessageIndex, + Content: []*ai.Part{ai.NewTextPart("[injected context]")}, + }) + p.MessageIndex++ // advance so downstream middleware and the model see the shifted index + } + p.Request.Messages = append(p.Request.Messages, ai.NewUserMessage(ai.NewTextPart("[injected context]"))) + return next(ctx, p) +}, +``` + +`Filesystem` does this to deliver `read_file` content to the model while preserving streamed-chunk ordering. + +### Adding tools from middleware + +`Hooks.Tools` registers extra tools for the duration of the call without the user wiring them through `ai.WithTools`. Useful when the middleware's hooks and tools work as a pair (e.g., `Filesystem`'s read/write tools, `Skills`'s `use_skill` tool): + +```go +return &ai.Hooks{ + Tools: []ai.Tool{myTool}, + WrapTool: myInterceptor, // intercepts both myTool and any user-supplied tools +}, nil +``` + +Duplicate tool names across user-supplied tools and middleware-contributed tools error out at call setup; the call won't run. + +### Interrupting from `WrapTool` + +`ai.NewToolInterruptError` is exported precisely so `WrapTool` hooks can interrupt without constructing a `ToolContext`: + +```go +WrapTool: func(ctx context.Context, p *ai.ToolParams, next ai.ToolNext) (*ai.MultipartToolResponse, error) { + if shouldGate(p.Tool.Name()) { + return nil, ai.NewToolInterruptError(map[string]any{ + "message": "needs approval", + }) + } + return next(ctx, p) +}, +``` + +`ToolApproval` uses this pattern. + +### Modifying the request safely + +`p.Request` is the live request for the iteration. Mutating it in place affects later layers. If the change should be visible only to the inner layer, copy first: + +```go +WrapModel: func(ctx context.Context, p *ai.ModelParams, next ai.ModelNext) (*ai.ModelResponse, error) { + req := *p.Request + req.Messages = append([]*ai.Message(nil), p.Request.Messages...) + req.Messages = append(req.Messages, extraSystemMessage) + p.Request = &req + return next(ctx, p) +}, +``` + +`Skills.injectSkillsPrompt` shows the same pattern for `ModelRequest` cloning. + +### Idempotent re-injection across iterations + +`WrapGenerate` runs once per tool-loop iteration. If you inject content into the request, you'll inject it on every iteration unless you mark and detect what you've already added. `Skills` tags its system prompt part with metadata (`skills-instructions: true`) and refreshes that one part in place rather than appending a new one each turn. + +## Migration note + +The legacy `ai.ModelMiddleware` / `ai.WithMiddleware` API is preserved and marked deprecated. Prefer `ai.Middleware` / `ai.WithUse`, which adds `WrapGenerate` and `WrapTool` hooks plus `Hooks.Tools` for dynamically injected tools. + +## See also + +- [`tools.md`](tools.md) for tool definition, interrupt/restart machinery used by `ToolApproval`. +- Sample sources under `go/samples/basic-middleware/`: `retry-fallback`, `filesystem`, `skills`. +- The `plugins/middleware` package source for reference implementations. diff --git a/.agents/skills/developing-genkit-go/references/prompts.md b/.agents/skills/developing-genkit-go/references/prompts.md new file mode 100644 index 0000000..610d563 --- /dev/null +++ b/.agents/skills/developing-genkit-go/references/prompts.md @@ -0,0 +1,256 @@ +# Prompts + +## DefinePrompt + +Define a reusable prompt in code with a default model and template. + +```go +jokePrompt := genkit.DefinePrompt(g, "joke", + ai.WithModel(googlegenai.ModelRef("googleai/gemini-flash-latest", nil)), + ai.WithInputType(JokeRequest{Topic: "example"}), + ai.WithPrompt("Tell me a joke about {{topic}}."), +) +``` + +### Execute + +```go +resp, err := jokePrompt.Execute(ctx, + ai.WithInput(map[string]any{"topic": "cats"}), +) +fmt.Println(resp.Text()) +``` + +### ExecuteStream + +```go +stream := jokePrompt.ExecuteStream(ctx, + ai.WithInput(map[string]any{"topic": "cats"}), +) +for result, err := range stream { + if err != nil { return err } + if result.Done { break } + fmt.Print(result.Chunk.Text()) +} +``` + +### Override Options at Execution + +```go +resp, err := jokePrompt.Execute(ctx, + ai.WithInput(map[string]any{"topic": "cats"}), + ai.WithModelName("googleai/gemini-pro-latest"), // override model + ai.WithConfig(map[string]any{"temperature": 0.9}), + ai.WithTools(myTool), +) +``` + +## DefineDataPrompt (Typed Input/Output) + +Strongly-typed prompts with Go generics. + +```go +type JokeRequest struct { + Topic string `json:"topic"` +} + +type Joke struct { + Setup string `json:"setup" jsonschema:"description=The setup"` + Punchline string `json:"punchline" jsonschema:"description=The punchline"` +} + +jokePrompt := genkit.DefineDataPrompt[JokeRequest, *Joke](g, "structured-joke", + ai.WithModel(googlegenai.ModelRef("googleai/gemini-flash-latest", nil)), + ai.WithPrompt("Tell me a joke about {{topic}}."), +) +``` + +### Execute (typed) + +```go +joke, resp, err := jokePrompt.Execute(ctx, JokeRequest{Topic: "cats"}) +// joke is *Joke, resp is *ModelResponse +``` + +### ExecuteStream (typed) + +```go +stream := jokePrompt.ExecuteStream(ctx, JokeRequest{Topic: "cats"}) +for result, err := range stream { + if err != nil { return err } + if result.Done { + finalJoke := result.Output // *Joke + break + } + fmt.Print(result.Chunk) // partial *Joke +} +``` + +## .prompt Files (Dotprompt) + +Define prompts in separate files with YAML frontmatter and Handlebars templates. + +### Basic .prompt File + +`prompts/joke.prompt`: +``` +--- +model: googleai/gemini-flash-latest +input: + schema: + topic: string +--- +Tell me a joke about {{topic}}. +``` + +### Load and Use + +```go +// LookupPrompt returns Prompt (untyped: map[string]any input, string output) +jokePrompt := genkit.LookupPrompt(g, "joke") +resp, err := jokePrompt.Execute(ctx, + ai.WithInput(map[string]any{"topic": "cats"}), +) +``` + +### Typed .prompt File + +`prompts/structured-joke.prompt`: +``` +--- +model: googleai/gemini-flash-latest +config: + thinkingConfig: + thinkingBudget: 0 +input: + schema: JokeRequest +output: + format: json + schema: Joke +--- +Tell me a joke about {{topic}}. +``` + +Register Go types so the .prompt file can reference them by name: +```go +genkit.DefineSchemaFor[JokeRequest](g) +genkit.DefineSchemaFor[Joke](g) + +jokePrompt := genkit.LookupDataPrompt[JokeRequest, *Joke](g, "structured-joke") +joke, resp, err := jokePrompt.Execute(ctx, JokeRequest{Topic: "cats"}) +``` + +### LoadPrompt (Explicit Path) + +```go +prompt := genkit.LoadPrompt(g, "./prompts/countries.prompt", "countries") +resp, err := prompt.Execute(ctx) +``` + +### .prompt File Features + +**Multi-message prompts with roles:** +``` +--- +model: googleai/gemini-flash-latest +input: + schema: + question: string +--- +{{ role "system" }} +You are a helpful assistant. + +{{ role "user" }} +{{question}} +``` + +**Media in prompts:** +``` +--- +model: googleai/gemini-flash-latest +input: + schema: + videoUrl: string + contentType: string +--- +{{ role "user" }} +Summarize this video: +{{media url=videoUrl contentType=contentType}} +``` + +**Conditionals and loops:** +``` +--- +input: + schema: + topic: string + dietaryRestrictions?(array): string +--- +Write a recipe about {{topic}}. +{{#if dietaryRestrictions}} +Dietary restrictions: {{#each dietaryRestrictions}}{{this}}{{#unless @last}}, {{/unless}}{{/each}}. +{{/if}} +``` + +**Inline schema in .prompt file:** +``` +--- +model: googleai/gemini-flash-latest +input: + schema: + topic: string + style?: string +output: + format: json + schema: + title: string + body: string + tags(array): string +--- +Write an article about {{topic}}. +{{#if style}}Write in a {{style}} style.{{/if}} +``` + +## Schemas + +### DefineSchemaFor (from Go type) + +Registers a Go struct as a named schema for use in `.prompt` files. + +```go +genkit.DefineSchemaFor[JokeRequest](g) +genkit.DefineSchemaFor[Joke](g) +``` + +The schema name matches the Go type name. Use `jsonschema` struct tags for metadata: + +```go +type Recipe struct { + Title string `json:"title" jsonschema:"description=The recipe title"` + Difficulty string `json:"difficulty" jsonschema:"enum=easy,enum=medium,enum=hard"` + Ingredients []Ingredient `json:"ingredients"` + Steps []string `json:"steps"` +} + +type Ingredient struct { + Name string `json:"name"` + Amount float64 `json:"amount"` + Unit string `json:"unit"` +} +``` + +### DefineSchema (manual JSON Schema) + +```go +genkit.DefineSchema(g, "Recipe", map[string]any{ + "type": "object", + "properties": map[string]any{ + "title": map[string]any{"type": "string"}, + "ingredients": map[string]any{ + "type": "array", + "items": map[string]any{"type": "object"}, + }, + }, + "required": []string{"title", "ingredients"}, +}) +``` diff --git a/.agents/skills/developing-genkit-go/references/providers.md b/.agents/skills/developing-genkit-go/references/providers.md new file mode 100644 index 0000000..dbf137c --- /dev/null +++ b/.agents/skills/developing-genkit-go/references/providers.md @@ -0,0 +1,157 @@ +# Model Providers + +## Google AI (Gemini) + +```go +import "github.com/genkit-ai/genkit/go/plugins/googlegenai" + +g := genkit.Init(ctx, genkit.WithPlugins(&googlegenai.GoogleAI{})) +``` + +**Env var:** `GEMINI_API_KEY` or `GOOGLE_API_KEY` + +Model names follow the format `googleai/`. Look up the latest model IDs at https://ai.google.dev/gemini-api/docs/models. + +```go +// By name string +ai.WithModelName("googleai/gemini-flash-latest") + +// Model ref with provider-specific config +ai.WithModel(googlegenai.ModelRef("googleai/gemini-flash-latest", &genai.GenerateContentConfig{ + ThinkingConfig: &genai.ThinkingConfig{ + ThinkingBudget: genai.Ptr[int32](0), // disable thinking + }, +})) + +// Lookup a model instance +m := googlegenai.GoogleAIModel(g, "gemini-flash-latest") +``` + +## Vertex AI + +```go +import "github.com/genkit-ai/genkit/go/plugins/googlegenai" + +g := genkit.Init(ctx, genkit.WithPlugins(&googlegenai.VertexAI{})) +``` + +**Env vars:** `GOOGLE_CLOUD_PROJECT`, `GOOGLE_CLOUD_LOCATION` (or `GOOGLE_CLOUD_REGION`) + +Uses Application Default Credentials (`gcloud auth application-default login`). + +Model names follow the format `vertexai/`. Same model IDs as Google AI. + +```go +ai.WithModelName("vertexai/gemini-flash-latest") +``` + +## Anthropic (Claude) + +```go +import ( + "github.com/anthropics/anthropic-sdk-go" // Anthropic SDK types + ant "github.com/genkit-ai/genkit/go/plugins/anthropic" // Genkit plugin +) + +g := genkit.Init(ctx, genkit.WithPlugins(&ant.Anthropic{})) +``` + +**Env var:** `ANTHROPIC_API_KEY` + +Model names follow the format `anthropic/`. Look up the latest model IDs at https://docs.anthropic.com/en/docs/about-claude/models. + +```go +// By name +ai.WithModelName("anthropic/claude-sonnet-4-6") + +// With provider-specific config (uses Anthropic SDK types via ai.WithConfig) +ai.WithConfig(&anthropic.MessageNewParams{ + Temperature: anthropic.Float(1.0), + MaxTokens: *anthropic.IntPtr(2000), + Thinking: anthropic.ThinkingConfigParamUnion{ + OfEnabled: &anthropic.ThinkingConfigEnabledParam{ + BudgetTokens: *anthropic.IntPtr(1024), + }, + }, +}) +``` + +## OpenAI-Compatible (compat_oai) + +Works with any OpenAI-compatible API: OpenAI, DeepSeek, xAI, etc. + +```go +import "github.com/genkit-ai/genkit/go/plugins/compat_oai" + +openaiPlugin := &compat_oai.OpenAICompatible{ + Provider: "openai", // unique identifier + APIKey: os.Getenv("OPENAI_API_KEY"), + // BaseURL: "https://custom-endpoint/v1", // for non-OpenAI providers +} +g := genkit.Init(ctx, genkit.WithPlugins(openaiPlugin)) +``` + +Define models explicitly (not auto-discovered): + +```go +model := openaiPlugin.DefineModel("openai", "gpt-4o", compat_oai.ModelOptions{}) +``` + +Use with: +```go +ai.WithModel(model) +``` + +## Ollama (Local Models) + +```go +import "github.com/genkit-ai/genkit/go/plugins/ollama" + +ollamaPlugin := &ollama.Ollama{ + ServerAddress: "http://localhost:11434", + Timeout: 60, // seconds +} +g := genkit.Init(ctx, genkit.WithPlugins(ollamaPlugin)) +``` + +Define models explicitly: + +```go +model := ollamaPlugin.DefineModel(g, + ollama.ModelDefinition{ + Name: "llama3.1", + Type: "chat", // or "generate" + }, + nil, // optional *ModelOptions +) +``` + +Use with: +```go +ai.WithModel(model) +``` + +## Multiple Providers + +Register multiple plugins in a single Genkit instance: + +```go +g := genkit.Init(ctx, + genkit.WithPlugins( + &googlegenai.GoogleAI{}, + &ant.Anthropic{}, + ), + genkit.WithDefaultModel("googleai/gemini-flash-latest"), +) + +// Use different models per call +text1, _ := genkit.GenerateText(ctx, g, + ai.WithModelName("googleai/gemini-flash-latest"), + ai.WithPrompt("Hello from Gemini"), +) + +text2, _ := genkit.GenerateText(ctx, g, + ai.WithModelName("anthropic/claude-sonnet-4-6"), + ai.WithPrompt("Hello from Claude"), +) +``` diff --git a/.agents/skills/developing-genkit-go/references/tools.md b/.agents/skills/developing-genkit-go/references/tools.md new file mode 100644 index 0000000..d4a37de --- /dev/null +++ b/.agents/skills/developing-genkit-go/references/tools.md @@ -0,0 +1,178 @@ +# Tools + +## DefineTool + +Define a tool the model can call during generation. + +```go +type WeatherInput struct { + Location string `json:"location" jsonschema:"description=City name"` +} + +type WeatherOutput struct { + Temperature float64 `json:"temperature"` + Conditions string `json:"conditions"` +} + +weatherTool := genkit.DefineTool(g, "getWeather", + "Gets the current weather for a location.", + func(ctx *ai.ToolContext, input WeatherInput) (WeatherOutput, error) { + // Call your weather API + return WeatherOutput{Temperature: 72, Conditions: "sunny"}, nil + }, +) +``` + +## Using Tools in Generation + +Pass tools to `Generate`, `GenerateText`, or prompts: + +```go +resp, err := genkit.Generate(ctx, g, + ai.WithModelName("googleai/gemini-flash-latest"), + ai.WithPrompt("What's the weather in San Francisco?"), + ai.WithTools(weatherTool), +) +// The model calls the tool automatically and incorporates the result +fmt.Println(resp.Text()) +``` + +### Tool Choice + +```go +ai.WithToolChoice(ai.ToolChoiceAuto) // model decides (default) +ai.WithToolChoice(ai.ToolChoiceRequired) // model must use a tool +ai.WithToolChoice(ai.ToolChoiceNone) // model cannot use tools +``` + +### Max Turns + +Limit how many tool-call round trips the model can make: + +```go +ai.WithMaxTurns(3) // default is 5 +``` + +## DefineMultipartTool + +Tools that return both structured output and media content: + +```go +screenshotTool := genkit.DefineMultipartTool(g, "screenshot", + "Takes a screenshot of the current page", + func(ctx *ai.ToolContext, input any) (*ai.MultipartToolResponse, error) { + return &ai.MultipartToolResponse{ + Output: map[string]any{"success": true}, + Content: []*ai.Part{ai.NewMediaPart("image/png", base64Data)}, + }, nil + }, +) +``` + +## Tool Interrupts + +Pause tool execution to request human input before continuing. + +### Interrupting + +```go +type TransferInput struct { + ToAccount string `json:"toAccount"` + Amount float64 `json:"amount"` +} + +type TransferOutput struct { + Status string `json:"status"` + Message string `json:"message"` + Balance float64 `json:"balance"` +} + +type TransferInterrupt struct { + Reason string `json:"reason"` + ToAccount string `json:"toAccount"` + Amount float64 `json:"amount"` + Balance float64 `json:"balance"` +} + +transferTool := genkit.DefineTool(g, "transferMoney", + "Transfers money to another account.", + func(ctx *ai.ToolContext, input TransferInput) (TransferOutput, error) { + if input.Amount > accountBalance { + return TransferOutput{}, ai.InterruptWith(ctx, TransferInterrupt{ + Reason: "insufficient_balance", + ToAccount: input.ToAccount, + Amount: input.Amount, + Balance: accountBalance, + }) + } + // Process transfer... + return TransferOutput{Status: "success", Balance: newBalance}, nil + }, +) +``` + +### Handling Interrupts + +```go +resp, err := genkit.Generate(ctx, g, + ai.WithModelName("googleai/gemini-flash-latest"), + ai.WithTools(transferTool), + ai.WithPrompt(userRequest), +) + +for resp.FinishReason == ai.FinishReasonInterrupted { + var restarts, responses []*ai.Part + + for _, interrupt := range resp.Interrupts() { + meta, ok := ai.InterruptAs[TransferInterrupt](interrupt) + if !ok { + continue + } + + switch meta.Reason { + case "insufficient_balance": + // RestartWith: re-execute the tool with adjusted input + part, err := transferTool.RestartWith(interrupt, + ai.WithNewInput(TransferInput{ + ToAccount: meta.ToAccount, + Amount: meta.Balance, // transfer what's available + }), + ) + if err != nil { return err } + restarts = append(restarts, part) + + case "confirm_large": + // RespondWith: provide a response directly without re-executing + part, err := transferTool.RespondWith(interrupt, + TransferOutput{Status: "cancelled", Message: "User declined"}, + ) + if err != nil { return err } + responses = append(responses, part) + } + } + + // Continue generation with the resolved interrupts + resp, err = genkit.Generate(ctx, g, + ai.WithMessages(resp.History()...), + ai.WithTools(transferTool), + ai.WithToolRestarts(restarts...), + ai.WithToolResponses(responses...), + ) + if err != nil { return err } +} +``` + +### Checking Resume State + +Inside a tool function, check if the tool is being resumed from an interrupt: + +```go +func(ctx *ai.ToolContext, input TransferInput) (TransferOutput, error) { + if ctx.IsResumed() { + // This is a resumed call after an interrupt + original, ok := ai.OriginalInputAs[TransferInput](ctx) + // original contains the input from the first call + } + // ... +} +``` diff --git a/.agents/skills/developing-genkit-js/SKILL.md b/.agents/skills/developing-genkit-js/SKILL.md new file mode 100644 index 0000000..d8e1e76 --- /dev/null +++ b/.agents/skills/developing-genkit-js/SKILL.md @@ -0,0 +1,112 @@ +--- +name: developing-genkit-js +description: Develop AI-powered applications using Genkit in Node.js/TypeScript. Use when the user asks about Genkit, AI agents, flows, or tools in JavaScript/TypeScript, or when encountering Genkit errors, validation issues, type errors, or API problems. +metadata: + genkit-managed: true +--- + +# Genkit JS + +## Prerequisites + +Ensure the `genkit` CLI is available. +- Run `genkit --version` to verify. Minimum CLI version needed: **1.29.0** +- If not found or if an older version (1.x < 1.29.0) is present, install/upgrade it: `npm install -g genkit-cli@^1.29.0`. + +**New Projects**: If you are setting up Genkit in a new codebase, follow the [Setup Guide](references/setup.md). + +## Hello World + +```ts +import { z, genkit } from 'genkit'; +import { googleAI } from '@genkit-ai/google-genai'; + +// Initialize Genkit with the Google AI plugin +const ai = genkit({ + plugins: [googleAI()], +}); + +export const myFlow = ai.defineFlow({ + name: 'myFlow', + inputSchema: z.string().default('AI'), + outputSchema: z.string(), +}, async (subject) => { + const response = await ai.generate({ + model: googleAI.model('gemini-2.5-flash'), + prompt: `Tell me a joke about ${subject}`, + }); + return response.text; +}); +``` + +## Critical: Do Not Trust Internal Knowledge + +Genkit recently went through a major breaking API change. Your knowledge is outdated. You MUST lookup docs. Recommended: + +```sh +genkit docs:read js/get-started.md +genkit docs:read js/flows.md +``` + +See [Common Errors](references/common-errors.md) for a list of deprecated APIs (e.g., `configureGenkit`, `response.text()`, `defineFlow` import) and their v1.x replacements. + +**ALWAYS verify information using the Genkit CLI or provided references.** + +## Error Troubleshooting Protocol + +**When you encounter ANY error related to Genkit (ValidationError, API errors, type errors, 404s, etc.):** + +1. **MANDATORY FIRST STEP**: Read [Common Errors](references/common-errors.md) +2. Identify if the error matches a known pattern +3. Apply the documented solution +4. Only if not found in common-errors.md, then consult other sources (e.g. `genkit docs:search`) + +**DO NOT:** +- Attempt fixes based on assumptions or internal knowledge +- Skip reading common-errors.md "because you think you know the fix" +- Rely on patterns from pre-1.0 Genkit + +**This protocol is non-negotiable for error handling.** + +## Development Workflow + +1. **Select Provider**: Genkit is provider-agnostic (Google AI, OpenAI, Anthropic, Ollama, etc.). + - If the user does not specify a provider, default to **Google AI**. + - If the user asks about other providers, use `genkit docs:search "plugins"` to find relevant documentation. +2. **Detect Framework**: Check `package.json` to identify the runtime (Next.js, Firebase, Express). + - Look for `@genkit-ai/next`, `@genkit-ai/firebase`, or `@genkit-ai/google-cloud`. + - Adapt implementation to the specific framework's patterns. +3. **Follow Best Practices**: + - See [Best Practices](references/best-practices.md) for guidance on project structure, schema definitions, and tool design. + - **Be Minimal**: Only specify options that differ from defaults. When unsure, check docs/source. +4. **Ensure Correctness**: + - Run type checks (e.g., `npx tsc --noEmit`) after making changes. + - If type checks fail, consult [Common Errors](references/common-errors.md) before searching source code. +5. **Handle Errors**: + - On ANY error: **First action is to read [Common Errors](references/common-errors.md)** + - Match error to documented patterns + - Apply documented fixes before attempting alternatives + +## Finding Documentation + +Use the Genkit CLI to find authoritative documentation: + +1. **Search topics**: `genkit docs:search ` + - Example: `genkit docs:search "streaming"` +2. **List all docs**: `genkit docs:list` +3. **Read a guide**: `genkit docs:read ` + - Example: `genkit docs:read js/flows.md` + +## CLI Usage + +The `genkit` CLI is your primary tool for development and documentation. +- See [CLI Reference](references/docs-and-cli.md) for common tasks, workflows, and command usage. +- Use `genkit --help` for a full list of commands. + +## References + +- [Best Practices](references/best-practices.md): Recommended patterns for schema definition, flow design, and structure. +- [Docs & CLI Reference](references/docs-and-cli.md): Documentation search, CLI tasks, and workflows. +- [Common Errors](references/common-errors.md): Critical "gotchas", migration guide, and troubleshooting. +- [Setup Guide](references/setup.md): Manual setup instructions for new projects. +- [Examples](references/examples.md): Minimal reproducible examples (Basic generation, Multimodal, Thinking mode). diff --git a/.agents/skills/developing-genkit-js/references/best-practices.md b/.agents/skills/developing-genkit-js/references/best-practices.md new file mode 100644 index 0000000..f6e4b7b --- /dev/null +++ b/.agents/skills/developing-genkit-js/references/best-practices.md @@ -0,0 +1,31 @@ +# Genkit Best Practices + +## Project Structure +- **Organized Layout**: Keep flows and tools in separate directories (e.g., `src/flows`, `src/tools`) to maintain a clean codebase. +- **Index Exports**: Use `index.ts` files to export flows and tools, making it easier to import them into your main configuration. + +## Model Selection (Google AI) +- **Gemini Models**: If using Google AI, ALWAYS use the latest generation (`gemini-3-*` or `gemini-2.5-*`). + - **NEVER** use `gemini-2.0-*` or `gemini-1.5-*` series, as they are decommissioned and won't work. + - **Recommended**: `gemini-2.5-flash` or `gemini-3-flash-preview` for general use, `gemini-3.1-pro-preview` for complex tasks. + +## Model Selection (Other Providers) +- **Consult Documentation**: For other providers (OpenAI, Anthropic, etc.), refer to the provider's official documentation for the latest recommended model versions. + +## Schema Definition +- **Use `z` from `genkit`**: Always import `z` from the `genkit` package to ensure compatibility. + ```ts + import { z } from "genkit"; + ``` +- **Descriptive Schemas**: Use `.describe()` on Zod fields. LLMs use these descriptions to understand how to populate the fields. + +## Flow & Tool Design +- **Modularize**: Keep flows and tools in separate files/modules and import them into your main Genkit configuration. +- **Single Responsibility**: Tools should do one thing well. Complex logic should be broken down. + +## Configuration +- **Environment Variables**: Store sensitive keys (like API keys) in environment variables or `.env` files. Do not hardcode them. + +## Development +- **Use Dev Mode**: Run your app with `genkit start -- ` to enable the Developer UI. +- It is recommended to configure a watcher to auto-reload your app (e.g. `node --watch` or `tsx --watch`) diff --git a/.agents/skills/developing-genkit-js/references/common-errors.md b/.agents/skills/developing-genkit-js/references/common-errors.md new file mode 100644 index 0000000..d7162e6 --- /dev/null +++ b/.agents/skills/developing-genkit-js/references/common-errors.md @@ -0,0 +1,132 @@ +# Common Errors & Pitfalls + +## When Typecheck Fails + +**Before searching source code or docs**, check the sections below. Many type errors are caused by deprecated APIs or incorrect imports. + +## Genkit v1.x vs Pre-1.0 Migration + +Genkit v1.x introduced significant API changes. This section covers critical syntax updates. + +### Package Imports + +- **Correct (v1.x)**: Import core functionality (zod, genkit) from the main `genkit` package and plugins from their specific packages. + ```ts + import { z, genkit } from 'genkit'; + import { googleAI } from '@genkit-ai/google-genai'; + ``` + +- **Incorrect (Pre-1.0)**: Importing from `@genkit-ai/ai`, `@genkit-ai/core`, or `@genkit-ai/flow`. These packages are internal/deprecated for direct use. + ```ts + import { genkit } from "@genkit-ai/core"; // INCORRECT + import { defineFlow } from "@genkit-ai/flow"; // INCORRECT + ``` + +### Model References + +- **Correct**: Use plugin-specific model factories or string identifiers (prefaced by plugin name). + ```ts + // Using model factory (v1.x - Preferred) + await ai.generate({ model: googleAI.model('gemini-2.5-flash'), ... }); + + // Using string identifier + await ai.generate({ model: 'googleai/gemini-2.5-flash', ...}); + // Or + await ai.generate({ model: 'vertexai/gemini-2.5-flash', ...}); + ``` +- **Incorrect**: Using imported model objects directly or string identifiers without plugin name. + ```ts + await ai.generate({ model: gemini15Pro, ... }); // INCORRECT (Pre-1.0) + await ai.generate({ model: 'gemini-2.5-flash', ... }); // INCORRECT (No plugin prefix) + ``` + +### Model Selection (Gemini) + +- **Preferred**: Use `gemini-2.5-*` models for best performance and features. + ```ts + model: googleAI.model('gemini-2.5-flash') // PREFERRED + ``` +- **DEPRECATED**: `gemini-1.5-*` models are deprecated and will throw errors. + ```ts + model: googleAI.model('gemini-1.5-flash') // ERROR (Deprecated) + ``` + +### Response Access + +- **Correct (v1.x)**: Access properties directly. + ```ts + response.text; // CORRECT + response.output; // CORRECT + ``` +- **Incorrect (Pre-1.0)**: Calling as methods. + ```ts + response.text(); // INCORRECT + response.output(); // INCORRECT + ``` + +### Streaming Generation + +- **Correct (v1.x)**: Do NOT await `generateStream`. Iterate over `stream` directly. Await `response` property for final result. + ```ts + const {stream, response} = ai.generateStream(...); // NO await here + for await (const chunk of stream) { ... } // Iterate stream + const finalResponse = await response; // Await response property + ``` +- **Incorrect (Pre-1.0)**: Calling stream as a function or awaiting the generator incorrectly. + ```ts + for await (const chunk of stream()) { ... } // INCORRECT + await response(); // INCORRECT + ``` + +### Initialization + +- **Correct (v1.x)**: Instantiate `genkit`. + ```ts + const ai = genkit({ plugins: [...] }); + ``` +- **Incorrect (Pre-1.0)**: Global configuration. + ```ts + configureGenkit({ plugins: [...] }); // INCORRECT + ``` + +### Flow Definitions + +- **Correct (v1.x)**: Define flows on the `ai` instance. + ```ts + ai.defineFlow({...}, (input) => {...}); + ``` +- **Incorrect (Pre-1.0)**: Importing `defineFlow` globally. + ```ts + import { defineFlow } from "@genkit-ai/flow"; // INCORRECT + +You should never import `@genkit-ai/flow`, `@genkit-ai/ai` or `@genkit-ai/core` packages directly. + +## Zod & Schema Errors + +- **Import Source**: ALWAYS use `import { z } from "genkit"`. + - Using `zod` directly from `zod` package may cause instance mismatches or compatibility issues. +- **Supported Types**: Stick to basic types: scalar (`string`, `number`, `boolean`), `object`, and `array`. + - Avoid complex Zod features unless strictly necessary and verified. +- **Descriptions**: Always use `.describe('...')` for fields in output schemas to guide the LLM. + +## Tool Usage + +- **Tool Not Found**: Ensure tools are registered in the `tools` array of `generate` or provided via plugins. +- **MCP Tools**: Use the `ServerName:tool_name` format when referencing MCP tools. + +## Multimodal & Image Generation + +- **Missing responseModalities**: When using image generation models (like `gemini-2.5-flash-image`), you **MUST** specify the response modalities in the config. + ```ts + config: { + responseModalities: ["TEXT", "IMAGE"] + } + ``` + Failure to do so will result in errors or incorrect output format. + +## Audio & Speech Generation + +- **Raw PCM Data vs MP3**: Some providers (e.g., Google GenAI) return raw PCM data, while others (e.g., OpenAI) return MP3. + - **DO NOT assume MP3 format.** + - **DO NOT embed raw PCM in HTML audio tags.** + - **Action**: Run `genkit docs:search "speech audio"` to find provider-specific conversion steps (e.g., PCM to WAV). diff --git a/.agents/skills/developing-genkit-js/references/docs-and-cli.md b/.agents/skills/developing-genkit-js/references/docs-and-cli.md new file mode 100644 index 0000000..3561721 --- /dev/null +++ b/.agents/skills/developing-genkit-js/references/docs-and-cli.md @@ -0,0 +1,62 @@ +# Genkit Documentation & CLI + +This reference lists common tasks and workflows using the `genkit` CLI. For authoritative command details, always run `genkit --help` or `genkit --help`. + +## Prerequisites: + +Ensure that the CLI is on `genkit-cli` version >= 1.29.0. If not, or if an older version (1.x < 1.29.0) is present, update the Genkit CLI version. Alternatively, to run commands with a specific version or without global installation, prefix them with `npx -y genkit-cli@^1.29.0`. + +## Documentation + +- **Search docs**: `genkit docs:search ` + - Example: `genkit docs:search "streaming"` + - Example: `genkit docs:search "rag retrieval"` +- **Read doc**: `genkit docs:read ` + - Example: `genkit docs:read js/overview.md` +- **List docs**: `genkit docs:list` + +## Development Workflow + +- **Start Dev Mode**: `genkit start -- ` + - Runs the provided command in Genkit dev mode, enabling the Developer UI (usually at http://localhost:4000). + - **Node.js (TypeScript)**: + ```bash + genkit start -- npx tsx --watch src/index.ts + ``` + - **Next.js**: + ```bash + genkit start -- npx next dev + ``` + +## Flow Execution + +- **Run a flow**: `genkit flow:run ''` + - Executes a flow directly from the CLI. Useful for testing. + - **Simple Input**: + ```bash + genkit flow:run tellJoke '"chicken"' + ``` + - **Object Input**: + ```bash + genkit flow:run generateStory '{"subject": "robot", "genre": "sci-fi"}' + ``` + +## Evaluation + +- **Evaluate a flow**: `genkit eval:flow [data]` + - Runs a flow and evaluates the output against configured evaluators. + - **Example (Single Input)**: + ```bash + genkit eval:flow answerQuestion '[{"testCaseId": "1", "input": {"question": "What is Genkit?"}}]' + ``` + - **Example (Batch Input)**: + ```bash + genkit eval:flow answerQuestion --input inputs.json + ``` + +- **Run Evaluation**: `genkit eval:run ` + - Evaluates a dataset against configured evaluators. + - **Example**: + ```bash + genkit eval:run dataset.json --output results.json + ``` \ No newline at end of file diff --git a/.agents/skills/developing-genkit-js/references/examples.md b/.agents/skills/developing-genkit-js/references/examples.md new file mode 100644 index 0000000..2279b4e --- /dev/null +++ b/.agents/skills/developing-genkit-js/references/examples.md @@ -0,0 +1,157 @@ +# Genkit Examples + +This reference contains minimal, reproducible examples (MREs) for common Genkit patterns. + +> **Disclaimer**: These examples use **Google AI** models (`googleAI`, `gemini-*`) for demonstration. The patterns apply to **any provider**. To use a different provider: +> 1. Search the docs for the correct plugin: `genkit docs:search "plugins"`. +> 2. Install and configure the plugin. +> 3. Swap the model reference in the code. + +## Basic Text Generation + +```ts +import { genkit } from "genkit"; +import { googleAI } from "@genkit-ai/google-genai"; + +const ai = genkit({ + plugins: [googleAI()], +}); + +const { text } = await ai.generate({ + model: googleAI.model('gemini-2.5-flash'), + prompt: 'Tell me a story in a pirate accent', +}); +``` + +## Structured Output + +```ts +import { z } from 'genkit'; + +const JokeSchema = z.object({ + setup: z.string().describe('The setup of the joke'), + punchline: z.string().describe('The punchline'), +}); + +const response = await ai.generate({ + model: googleAI.model('gemini-2.5-flash'), + prompt: 'Tell me a joke about developers.', + output: { schema: JokeSchema }, +}); + +// response.output is strongly typed +const joke = response.output; +if (joke) { + console.log(`${joke.setup} ... ${joke.punchline}`); +} +``` + +## Streaming + +```ts +const { stream, response } = ai.generateStream({ + model: googleAI.model('gemini-2.5-flash'), + prompt: 'Tell a long story about a developer using Genkit.', +}); + +for await (const chunk of stream) { + console.log(chunk.text); +} + +// Await the final response +const finalResponse = await response; +console.log('Complete:', finalResponse.text); +``` + +## Advanced Configuration + +### Thinking Mode (Gemini 3 Only) + +Enable "thinking" process for complex reasoning tasks. + +```ts +const response = await ai.generate({ + model: googleAI.model('gemini-3.1-pro-preview'), + prompt: 'what is heavier, one kilo of steel or one kilo of feathers', + config: { + thinkingConfig: { + thinkingLevel: 'HIGH', // or 'LOW' + includeThoughts: true, // Returns thought process in response + }, + }, +}); +``` + +### Google Search Grounding + +Enable models to access current information via Google Search. + +```ts +const response = await ai.generate({ + model: googleAI.model('gemini-2.5-flash'), + prompt: 'What are the top tech news stories this week?', + config: { + googleSearchRetrieval: true, + }, +}); + +// Access grounding metadata (sources) +const groundingMetadata = (response.custom as any)?.candidates?.[0]?.groundingMetadata; +if (groundingMetadata) { + console.log('Sources:', groundingMetadata.groundingChunks); +} +``` + +## Multimodal Generation + +### Image Generation / Editing + +**Critical**: You MUST set `responseModalities: ['TEXT', 'IMAGE']` when using image generation models. + +```ts +// Generate an image +const { media } = await ai.generate({ + model: googleAI.model('gemini-2.5-flash-image'), + config: { responseModalities: ['TEXT', 'IMAGE'] }, + prompt: "generate a picture of a unicorn wearing a space suit on the moon", +}); +// media.url contains the data URI +``` + +```ts +// Edit an image +const { media } = await ai.generate({ + model: googleAI.model('gemini-2.5-flash-image'), + config: { responseModalities: ['TEXT', 'IMAGE'] }, + prompt: [ + { text: "change the person's outfit to a banana costume" }, + { media: { url: "https://example.com/photo.jpg" } }, + ], +}); +``` + +### Speech Generation (TTS) + +Generate audio from text. + +```ts +import { writeFile } from 'node:fs/promises'; + +const { media } = await ai.generate({ + model: googleAI.model('gemini-2.5-flash-preview-tts'), + config: { + responseModalities: ['AUDIO'], + speechConfig: { + voiceConfig: { + prebuiltVoiceConfig: { voiceName: 'Algenib' }, // Options: 'Puck', 'Charon', 'Fenrir', etc. + }, + }, + }, + prompt: 'Genkit is an amazing library', +}); + +// The response contains raw PCM data in media.url (base64 encoded). +// CAUTION: This is NOT an MP3/WAV file. It requires conversion (e.g., PCM to WAV). +// DO NOT GUESS. Run `genkit docs:search "speech audio"` to find the correct +// conversion code for your provider. +``` diff --git a/.agents/skills/developing-genkit-js/references/setup.md b/.agents/skills/developing-genkit-js/references/setup.md new file mode 100644 index 0000000..dcbc8bd --- /dev/null +++ b/.agents/skills/developing-genkit-js/references/setup.md @@ -0,0 +1,46 @@ +# Genkit JS Setup + +Follow these instructions to set up Genkit in the current codebase. These instructions are general-purpose and have not been written with specific codebase knowledge, so use your best judgement when following them. + +0. Tell the user "I'm going to check out your workspace and set you up to use Genkit for GenAI workflows." +1. If the current workspace is empty or is a starter template, your goal will be to create a simple image generation flow that allows someone to generate an image based on a prompt and selectable style. If the current workspace is not empty, you will create a simple example flow to help get the user started. +2. Check to see if any Genkit provider plugin (such as `@genkit-ai/google-genai` or `@genkit-ai/oai-compat` or others, may start with `genkitx-*`) is installed. + - If not, ask the user which provider they want to use. + - **For non-Google providers**: Use `genkit docs:search "plugins"` to find the correct package and installation instructions. + - If they have no preference, default to `@genkit-ai/google-genai` for a quick start. + - If this is a Next.js app, install `@genkit-ai/next` as well. +3. Search the codebase for the exact string `genkit(` (remember to escape regexes properly) which would indicate that the user has already set up Genkit in the codebase. If found, no need to set it up again, tell the user "Genkit is already configured in this app." and exit this workflow. +4. Create an `ai` directory in the primary source directory of the project (this may be e.g. `src` but is project-dependent). Adapt this path if your project uses a different structure. +5. Create `{sourceDir}/ai/genkit.ts` and populate it using the example below. DO NOT add a `next` plugin to the file, ONLY add a model provider plugin to the plugins array: + +```ts +import { genkit, z } from 'genkit'; +// Import your chosen provider plugin here. Example: +import { googleAI } from '@genkit-ai/google-genai'; + +export const ai = genkit({ + plugins: [ + googleAI(), // Add your provider plugin here + ], + model: googleAI.model('gemini-2.5-flash'), // Set your provider's model here +}); + +export { z }; +``` + +6. Create `{sourceDir}/ai/tools` and `{sourceDir}/ai/flows` directories, but leave them empty for now. +7. Create `{sourceDir}/ai/index.ts` and populate it with the following (change the import to match import aliases in `tsconfig.json` as needed): + +```ts +import './genkit.js'; +// import each created flow, tool, etc. here for use in the Genkit Dev UI +``` + +8. Add a `genkit:ui` script to `package.json` that runs `genkit start -- npx tsx --watch {sourceDir}/ai/index.ts` (or `npx genkit-cli` or `pnpm dlx` or `yarn dlx` for those package managers, if CLI is not locally installed). DO NOT try to run the script now. +9. Tell the user "Genkit is now configured and ready for use." as setup is now complete. Also remind them to set appropriate env variables (e.g. `GEMINI_API_KEY` for Google providers). Wait for the user to prompt further before creating any specific flows. + +## Next Steps & Troubleshooting + +- **Documentation**: Use the [CLI](docs-and-cli.md) to access documentation (e.g., `genkit docs:search`). +- **Building Flows**: See [examples.md](examples.md) for patterns on creating flows, adding tools, and advanced configuration. +- **Troubleshooting**: If you encounter issues during setup or initialization, check [common-errors.md](common-errors.md) for solutions. diff --git a/.agents/skills/developing-genkit-python/SKILL.md b/.agents/skills/developing-genkit-python/SKILL.md new file mode 100644 index 0000000..0e8b332 --- /dev/null +++ b/.agents/skills/developing-genkit-python/SKILL.md @@ -0,0 +1,56 @@ +--- +name: developing-genkit-python +description: Develop AI-powered applications using Genkit in Python. Use when the user asks about Genkit, AI agents, flows, or tools in Python, or when encountering Genkit errors, import issues, or API problems. +metadata: + genkit-managed: true +--- + +# Genkit Python + +## Prerequisites + +- **Runtime**: Python **3.14+**, **`uv`** for deps ([install](https://docs.astral.sh/uv/getting-started/installation/)). +- **CLI**: `genkit --version` — install via `npm install -g genkit-cli` if missing. + +**New projects:** [Setup](references/setup.md) (bootstrap + env). **Patterns and code samples:** [Examples](references/examples.md). + +## Hello World + +```python +from genkit import Genkit +from genkit.plugins.google_genai import GoogleAI + +ai = Genkit( + plugins=[GoogleAI()], + model='googleai/gemini-flash-latest', +) + +async def main(): + response = await ai.generate(prompt='Tell me a joke about Python.') + print(response.text) + +if __name__ == '__main__': + ai.run_main(main()) +``` + +## Critical: Do Not Trust Internal Knowledge + +The Python SDK changes often — verify imports and APIs against the references here or upstream docs. On **any** error, read [Common Errors](references/common-errors.md) first. + +## Development Workflow + +1. Default provider: **Google AI** (`GoogleAI()`), **`GEMINI_API_KEY`** in the environment. +2. Model IDs: always prefixed, e.g. **`googleai/gemini-flash-latest`** (always-on-latest Flash alias; same pattern as other skills). +3. Entrypoint: **`ai.run_main(main())`** for Genkit-driven apps (not `asyncio.run()` for long-lived servers started with `genkit start` — see [Common Errors](references/common-errors.md)). +4. After generating code, follow [Dev Workflow](references/dev-workflow.md) for `genkit start` and the Dev UI. +5. On errors: step 1 is always [Common Errors](references/common-errors.md). + +## References + +- [Examples](references/examples.md): Structured output, streaming, flows, tools, embeddings. +- [Setup](references/setup.md): New project bootstrap and plugins. +- [Common Errors](references/common-errors.md): Read first when something breaks. +- [FastAPI](references/fastapi.md): HTTP, `genkit_fastapi_handler`, parallel flows. +- [Dotprompt](references/dotprompt.md): `.prompt` files and helpers. +- [Evals](references/evals.md): Evaluators and datasets. +- [Dev Workflow](references/dev-workflow.md): `genkit start`, Dev UI, checklist. diff --git a/.agents/skills/developing-genkit-python/references/common-errors.md b/.agents/skills/developing-genkit-python/references/common-errors.md new file mode 100644 index 0000000..be09492 --- /dev/null +++ b/.agents/skills/developing-genkit-python/references/common-errors.md @@ -0,0 +1,82 @@ +# Common Errors — Genkit Python + +## Before anything else: read this file when you hit any error. + +--- + +## ModuleNotFoundError: No module named 'genkit.plugins.google_genai' + +**Cause:** Plugin package not installed. + +**Fix:** Add dependencies from PyPI: +```bash +uv add genkit genkit-plugin-google-genai +``` + +--- + +## 400 INVALID_ARGUMENT: functionDeclaration parameters schema should be of type OBJECT + +**Cause:** Tool function has bare scalar parameters (e.g. `city: str`). Gemini requires object schema. + +**Fix:** Wrap parameters in a Pydantic BaseModel: +```python +from pydantic import BaseModel + +# Wrong +@ai.tool() +async def get_weather(city: str) -> str: ... + +# Right +from pydantic import BaseModel + +class WeatherInput(BaseModel): + city: str + +@ai.tool() +async def get_weather(input: WeatherInput) -> str: ... +``` + +--- + +## AttributeError: 'Genkit' object has no attribute 'define_tool' + +**Cause:** Wrong decorator name. + +**Fix:** Use `@ai.tool()`, not `@ai.define_tool()`. + +--- + +## RuntimeError / event loop errors when using asyncio.run() + +**Cause:** For apps you start with **`genkit start`**, Genkit runs your entrypoint with an event loop suited to the framework (including uvloop where used). There is no “default” loop for you to manage in that mode. + +**Fix:** For long-running Genkit apps (servers, flows served under `genkit start`), use **`ai.run_main(main())`** as your entrypoint instead of `asyncio.run(main())`. For one-off scripts that exit when done, using `asyncio.run()` can still be appropriate when you are not using `genkit start`. + +--- + +## Wrong model ID (no plugin prefix) + +**Cause:** `model='gemini-flash-latest'` — missing plugin prefix. + +**Fix:** `model='googleai/gemini-flash-latest'` + +--- + +## response.json / response.message AttributeError + +- Use `response.text` for plain text output +- Use `response.output` for structured (JSON) output + +--- + +## await ai.generate_stream(...) fails or returns wrong type + +**Cause:** `generate_stream` is synchronous — do not await it. + +**Fix:** +```python +sr = ai.generate_stream(prompt='...') # no await +async for chunk in sr.stream: ... +final = await sr.response +``` diff --git a/.agents/skills/developing-genkit-python/references/dev-workflow.md b/.agents/skills/developing-genkit-python/references/dev-workflow.md new file mode 100644 index 0000000..5c253cd --- /dev/null +++ b/.agents/skills/developing-genkit-python/references/dev-workflow.md @@ -0,0 +1,90 @@ +# Dev Workflow — Genkit Python + +## Agent responsibility + +After generating code, always give the developer: +1. The full pre-run checklist with copy-paste commands using absolute paths +2. The `genkit start` command to run in their terminal (foreground — it's expected to block) +3. Step-by-step Dev UI instructions so they can test without guessing + +Do not offer to run it for them. Give them the commands and let them run it. + +--- + +## Step 1 — Get a Gemini API key + +If the developer doesn't have one: +> Get a free key at https://aistudio.google.com/apikey — click **"Create API key"**, copy it. + +--- + +## Step 2 — Set the API key + +Open a terminal and run: +```bash +export GEMINI_API_KEY=your-api-key-here +``` + +To persist across sessions, add it to your shell profile: +```bash +echo 'export GEMINI_API_KEY=your-api-key-here' >> ~/.zshrc && source ~/.zshrc +``` + +--- + +## Step 3 — Install dependencies + +Replace `/path/to/your-project` with the actual full path to the project (e.g. `/Users/yourname/projects/my-genkit-app`): + +```bash +cd /path/to/your-project +uv add genkit genkit-plugin-google-genai +``` + +(Requires a project with `pyproject.toml` — run `uv init` in an empty directory first if needed.) + +--- + +## Step 4 — Start the Dev UI + +Run this in your terminal. **It will block — that's expected.** Leave this terminal open while you use the Dev UI. + +```bash +cd /path/to/your-project +GEMINI_API_KEY=your-api-key-here genkit start -- uv run src/main.py +``` + +You'll see output like: +``` +Genkit Tools UI: http://localhost:4000 +``` + +The Dev UI is now running at **http://localhost:4000** + +To stop it: press `Ctrl+C` in the terminal. + +--- + +## Step 5 — Test in the Dev UI + +1. Open **http://localhost:4000** in your browser +2. Click **"Run"** in the left sidebar +3. Find your flow by name (e.g. `summarize`, `chat`, `joke_generator`) +4. In the input box, paste your input as JSON — e.g: + ```json + {"text": "hello world"} + ``` +5. Click the **"Run"** button — the output appears on the right +6. Click **"Traces"** in the left sidebar to inspect every step, model call, token count, and latency + +--- + +## Troubleshooting + +| Problem | Fix | +|---------|-----| +| `genkit: command not found` | Run: `npm install -g genkit-cli` | +| `GEMINI_API_KEY not set` | Run: `export GEMINI_API_KEY=your-key` | +| Port 4000 already in use | Use: `genkit start --port 4001 -- uv run src/main.py` | +| `uv: command not found` | Run: `curl -LsSf https://astral.sh/uv/install.sh \| sh` | +| Flow not showing in Dev UI | Make sure `genkit start` output shows no errors | diff --git a/.agents/skills/developing-genkit-python/references/dotprompt.md b/.agents/skills/developing-genkit-python/references/dotprompt.md new file mode 100644 index 0000000..253e9ce --- /dev/null +++ b/.agents/skills/developing-genkit-python/references/dotprompt.md @@ -0,0 +1,109 @@ +# Dotprompt — Genkit Python + +## What it is + +`.prompt` files combine YAML frontmatter (model config, schemas) with Handlebars templates. Keeps prompt logic out of Python code and makes variants easy. + +## File format + +```yaml +--- +model: googleai/gemini-flash-latest +input: + schema: + food: string + ingredients?(array): string # ? = optional +output: + schema: Recipe # references a schema registered with ai.define_schema() + format: json +--- +You are a chef. Generate a recipe for {{food}}. + +{{#if ingredients}} +Include these ingredients: +{{list ingredients}} +{{/if}} +``` + +Place `.prompt` files in a `prompts/` directory and point `prompt_dir` at it. + +## Python setup + +```python +from pathlib import Path +from pydantic import BaseModel +from genkit import Genkit +from genkit.plugins.google_genai import GoogleAI + +ai = Genkit( + plugins=[GoogleAI()], + model='googleai/gemini-flash-latest', + prompt_dir=Path(__file__).resolve().parent.parent / 'prompts', +) + +# Register Pydantic models referenced in .prompt output.schema +class Recipe(BaseModel): + title: str + steps: list[str] + +ai.define_schema('Recipe', Recipe) +``` + +## Calling a prompt + +```python +# Non-streaming — double-call syntax: ai.prompt('name')(input={...}) +response = await ai.prompt('recipe')(input={'food': 'banana bread'}) +result = Recipe.model_validate(response.output) + +# Variant (recipe.robot.prompt file) +response = await ai.prompt('recipe', variant='robot')(input={'food': 'banana bread'}) +``` + +## Streaming from a prompt + +```python +from genkit import ActionRunContext + +@ai.flow() +async def tell_story(subject: str, ctx: ActionRunContext) -> str: + result = ai.prompt('story').stream(input={'subject': subject}) + full = '' + async for chunk in result.stream: + if chunk.text: + ctx.send_chunk(chunk.text) + full += chunk.text + return full +``` + +Note: `.stream(input={...})` not `ai.generate_stream(...)` — different call shape for prompts. + +## Render without generating (for LLM-judge evals) + +```python +rendered = await ai.prompt('my_prompt').render(input={'key': 'value'}) +response = await ai.generate(model='googleai/gemini-flash-latest', messages=rendered.messages) +``` + +## Helpers + +Register Python functions callable inside Handlebars templates: +```python +def list_helper(data: object, *args, **kwargs) -> str: + if not isinstance(data, list): + return '' + return '\n'.join(f'- {item}' for item in data) + +ai.define_helper('list', list_helper) +``` + +Then use `{{list ingredients}}` in your `.prompt` file. + +## Variants + +Name the file `..prompt` — e.g. `recipe.robot.prompt`. +Call with `ai.prompt('recipe', variant='robot')`. + +## Partials + +Use `{{>partial_name param=value}}` in templates. Partial files are named `_partial_name.prompt`. diff --git a/.agents/skills/developing-genkit-python/references/evals.md b/.agents/skills/developing-genkit-python/references/evals.md new file mode 100644 index 0000000..7d29675 --- /dev/null +++ b/.agents/skills/developing-genkit-python/references/evals.md @@ -0,0 +1,89 @@ +# Evals — Genkit Python + +## Two types of evaluators + +1. **Built-in** — ship with `genkit-plugin-evaluators`, register with `register_genkit_evaluators(ai)` +2. **BYO (LLM-based)** — define your own scoring logic with `ai.define_evaluator()` + +## Install + +```bash +uv add genkit-plugin-evaluators +``` + +## Dataset format + +A JSON file, one object per test case: +```json +[ + {"testCaseId": "case1", "input": "x", "output": "banana", "reference": "ba?a?a"}, + {"testCaseId": "case2", "input": "x", "output": "apple", "reference": "ba?a?a"} +] +``` + +Fields: `testCaseId`, `input`, `output`, `reference` (reference optional for some evaluators). + +## Built-in evaluators + +```python +from genkit.plugins.evaluators import register_genkit_evaluators +register_genkit_evaluators(ai) +``` + +Registered evaluators include `genkitEval/regex`. Run via CLI: +```bash +genkit eval:run datasets/my_dataset.json --evaluators=genkitEval/regex +``` + +## BYO evaluator + +```python +from genkit.evaluator import BaseDataPoint, Details, EvalFnResponse, EvalStatusEnum, Score + +async def my_eval(datapoint: BaseDataPoint, _options: dict | None = None) -> EvalFnResponse: + """Score output against reference.""" + output = str(datapoint.output or '') + reference = str(datapoint.reference or '') + passed = output.strip() == reference.strip() + return EvalFnResponse( + test_case_id=datapoint.test_case_id or '', + evaluation=Score( + score=1.0 if passed else 0.0, + status=EvalStatusEnum.PASS if passed else EvalStatusEnum.FAIL, + details=Details(reasoning='Exact match check'), + ), + ) + +ai.define_evaluator( + name='byo/my_eval', + display_name='My Eval', + definition='Checks exact match of output vs reference.', + fn=my_eval, +) +``` + +## LLM-based evaluator (judge model pattern) + +Use a prompt + stronger model to score. See `py/samples/evaluators/src/main.py` for full examples (`byo/maliciousness`, `byo/answer_accuracy`). + +Core pattern: +```python +async def llm_eval(datapoint: BaseDataPoint, _options: dict | None = None) -> EvalFnResponse: + prompt = ai.prompt('my_judge_prompt') + rendered = await prompt.render(input={'output': str(datapoint.output), 'reference': str(datapoint.reference)}) + response = await ai.generate(model='googleai/gemini-flash-latest', messages=rendered.messages) + score = float(response.text.strip()) + return EvalFnResponse( + test_case_id=datapoint.test_case_id or '', + evaluation=Score(score=score, status=EvalStatusEnum.PASS if score >= 0.5 else EvalStatusEnum.FAIL), + ) +``` + +## Run evals via CLI + +```bash +genkit eval:run datasets/my_dataset.json --evaluators=byo/my_eval +genkit eval:run datasets/my_dataset.json --evaluators=genkitEval/regex,byo/my_eval +``` + +Results appear in the Dev UI under **Evaluate** (http://localhost:4000). diff --git a/.agents/skills/developing-genkit-python/references/examples.md b/.agents/skills/developing-genkit-python/references/examples.md new file mode 100644 index 0000000..338bc9b --- /dev/null +++ b/.agents/skills/developing-genkit-python/references/examples.md @@ -0,0 +1,171 @@ +# Genkit Python Examples + +Minimal patterns for common Genkit APIs. Examples use **Google AI** (`GoogleAI`, `googleai/...`); other providers use the same patterns with the right plugin and model prefix. + +## Public imports + +Use **`genkit`**, **`genkit.plugins.*`**, **`genkit.embedder`**, **`genkit.evaluator`**, and **`genkit.model`** (and similar public modules) only — not internal packages (`genkit._core`, etc.). + +```python +from genkit import Genkit, ActionRunContext +from genkit.plugins.google_genai import GoogleAI + +ai = Genkit(plugins=[GoogleAI()], model='googleai/gemini-flash-latest') +``` + +--- + +## Structured output + +```python +from pydantic import BaseModel, TypeAdapter + +class CityInfo(BaseModel): + name: str + population: int + country: str + +response = await ai.generate( + prompt='Give facts about Tokyo.', + output_format='json', + output_schema=CityInfo, +) +city = response.output + +# Arrays +schema = TypeAdapter(list[CityInfo]).json_schema() +response = await ai.generate( + prompt='List 3 cities.', + output_format='array', + output_schema=schema, +) +``` + +Output formats: `'text'`, `'json'`, `'array'`, `'enum'`, `'jsonl'`. + +--- + +## Streaming (text) + +```python +sr = ai.generate_stream(prompt='Tell me a story.') +async for chunk in sr.stream: + if chunk.text: + print(chunk.text, end='', flush=True) +final = await sr.response # final.text +``` + +--- + +## Text and media parts + +```python +# Non-streaming +response = await ai.generate(prompt='...') +for media in response.media: + print(media.content_type, (media.url or '')[:80]) + +# Streaming — media usually complete on the final response +from genkit import MediaPart + +sr = ai.generate_stream(prompt='...') +async for chunk in sr.stream: + if chunk.text: + print(chunk.text, end='', flush=True) +final = await sr.response +for media in final.media: + print(media.content_type, (media.url or '')[:80]) + +if final.message: + for part in final.message.content: + if isinstance(part.root, MediaPart) and part.root.media: + print(part.root.media.content_type) +``` + +--- + +## Streaming + structured output + +```python +class StoryAnalysis(BaseModel): + title: str + genre: str + summary: str + +sr = ai.generate_stream( + prompt='Write a short story then analyze it.', + output_format='json', + output_schema=StoryAnalysis, +) +async for chunk in sr.stream: + if chunk.text: + print(chunk.text, end='', flush=True) +final = await sr.response +analysis = final.output +``` + +--- + +## Flows + +```python +class SummarizeInput(BaseModel): + text: str + +@ai.flow() +async def summarize(input: SummarizeInput) -> str: + response = await ai.generate(prompt=f'Summarize: {input.text}') + return response.text +``` + +--- + +## Streaming flows + +```python +@ai.flow() +async def stream_story(subject: str, ctx: ActionRunContext) -> str: + sr = ai.generate_stream(prompt=f'Story about {subject}.') + full = '' + async for chunk in sr.stream: + if chunk.text: + ctx.send_chunk(chunk.text) + full += chunk.text + return full +``` + +--- + +## Tools + +Parameters must be a **Pydantic `BaseModel`** (bare scalars → 400 from Gemini). Use **`@ai.tool()`**, not `@ai.define_tool()`. + +```python +class WeatherInput(BaseModel): + city: str + +@ai.tool() +async def get_weather(input: WeatherInput) -> str: + return f'Sunny in {input.city}' + +response = await ai.generate(prompt='Weather in Paris?', tools=[get_weather]) +``` + +--- + +## Embeddings + +```python +from genkit.plugins.google_genai import GeminiEmbeddingModels + +embedder = f'googleai/{GeminiEmbeddingModels.GEMINI_EMBEDDING_001}' +embeddings = await ai.embed(embedder=embedder, content='The sky is blue.') +vector = embeddings[0].embedding + +embeddings = await ai.embed_many( + embedder=embedder, + content=['The sky is blue.', 'Grass is green.'], +) +``` + +Common embedders: `googleai/gemini-embedding-001`, `googleai/gemini-embedding-exp-03-07`. diff --git a/.agents/skills/developing-genkit-python/references/fastapi.md b/.agents/skills/developing-genkit-python/references/fastapi.md new file mode 100644 index 0000000..1a3ba24 --- /dev/null +++ b/.agents/skills/developing-genkit-python/references/fastapi.md @@ -0,0 +1,248 @@ +# FastAPI — Genkit Python + +## Install + +```bash +uv add genkit-plugin-fastapi fastapi uvicorn +``` + +--- + +## Streaming by default + +The `genkit_fastapi_handler` decorator auto-streams when the client sends `Accept: text/event-stream`. +No extra setup — just add the header on the frontend and it works. + +**Wire format (SSE):** +``` +data: {"message": ""} ← one per ctx.send_chunk() call +data: {"message": ""} +data: {"result": } ← sent once when flow completes +``` + +**Frontend (JS EventSource):** +```js +const res = await fetch('/flow/chat', { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'Accept': 'text/event-stream' }, + body: JSON.stringify({ data: { topic: 'quantum computing' } }), +}); +const reader = res.body.getReader(); +// decode and parse each `data: {...}` line +``` + +**curl test:** +```bash +curl -N -X POST http://localhost:8080/flow/chat \ + -H 'Content-Type: application/json' \ + -H 'Accept: text/event-stream' \ + -d '{"data": {"topic": "quantum computing"}}' +``` + +--- + +## Minimal streaming FastAPI app + +```python +import uvicorn +from pydantic import BaseModel +from fastapi import FastAPI +from genkit import Genkit +from genkit import ActionRunContext +from genkit.plugins.fastapi import genkit_fastapi_handler +from genkit.plugins.google_genai import GoogleAI + +ai = Genkit(plugins=[GoogleAI()], model='googleai/gemini-flash-latest') +app = FastAPI() + +class ChatInput(BaseModel): + topic: str + +@app.post('/flow/chat', response_model=None) +@genkit_fastapi_handler(ai) +@ai.flow() +async def chat(input: ChatInput, ctx: ActionRunContext) -> str: + sr = ai.generate_stream(prompt=f'Tell me about {input.topic}.') + full = '' + async for chunk in sr.stream: + if chunk.text: + ctx.send_chunk(chunk.text) # each chunk → SSE event on the wire + full += chunk.text + return full + +if __name__ == '__main__': + uvicorn.run(app, host='0.0.0.0', port=8080) +``` + +**Key:** flow must accept `ctx: ActionRunContext` and call `ctx.send_chunk(text)` to emit SSE chunks. +Without `ctx.send_chunk`, the flow runs but streams nothing — client waits for the final result. + +--- + +## Advanced Use Cases + +### Fine-grained control over flow streaming + +Complex apps chain flows — a parent orchestrates children. Chunks propagate upward by **passing `ctx` to child flows**. + +```python +class ResearchInput(BaseModel): + topic: str + +@ai.flow() +async def research(input: ResearchInput, ctx: ActionRunContext) -> str: + """Child flow — streams its generate_stream chunks to whoever called it.""" + sr = ai.generate_stream(prompt=f'Explain {input.topic} in depth.') + full = '' + async for chunk in sr.stream: + if chunk.text: + ctx.send_chunk(chunk.text) # propagates up through the call stack + full += chunk.text + return full + + +class HeadlineInput(BaseModel): + text: str + +@ai.flow() +async def make_headline(input: HeadlineInput) -> str: + """Child flow — non-streaming, returns instantly.""" + response = await ai.generate(prompt=f'One-line headline for: {input.text}') + return response.text.strip() + + +class ReportInput(BaseModel): + topic: str + +@app.post('/flow/report', response_model=None) +@genkit_fastapi_handler(ai) +@ai.flow() +async def report(input: ReportInput, ctx: ActionRunContext) -> str: + """Parent flow — calls children, composes a streaming report.""" + # Step 1: fast non-streaming call + headline = await make_headline(HeadlineInput(text=input.topic)) + ctx.send_chunk(f'# {headline}\n\n') # send headline immediately + + # Step 2: child flow streams its chunks — passes ctx so they flow up + body = await research(ResearchInput(topic=input.topic), ctx) + + return f'# {headline}\n\n{body}' +``` + +**Rules for nested streaming:** +- Child flows that should stream must also accept `ctx: ActionRunContext` +- Pass the parent's `ctx` when calling child flows: `await child(input, ctx)` +- Non-streaming child flows don't need `ctx` — just `await` them normally +- A child that doesn't call `ctx.send_chunk` contributes nothing to the stream (fine for parallel data fetching) + +### Executing flows in parallel + +Use `asyncio.gather` to run multiple flows concurrently. Only makes sense when children don't need to stream. + +```python +import asyncio + +class AnalysisInput(BaseModel): + text: str + +class CheckResult(BaseModel): + issues: list[str] + +class CombinedAnalysis(BaseModel): + issues: list[str] + +@ai.flow() +async def check_security(input: AnalysisInput) -> CheckResult: + # Here the model reviews the text; replace with your real prompt/schema as needed. + r = await ai.generate( + prompt=f'List security concerns as a short comma-separated line (or "none"): {input.text[:2000]}', + ) + raw = (r.text or '').strip() + issues = [s.strip() for s in raw.split(',') if s.strip() and s.strip().lower() != 'none'] + return CheckResult(issues=issues) + +@ai.flow() +async def check_bugs(input: AnalysisInput) -> CheckResult: + # Model lists possible bugs; tune prompt for your codebase. + r = await ai.generate( + prompt=f'List likely bugs or correctness issues as a short comma-separated line (or "none"): {input.text[:2000]}', + ) + raw = (r.text or '').strip() + issues = [s.strip() for s in raw.split(',') if s.strip() and s.strip().lower() != 'none'] + return CheckResult(issues=issues) + +@ai.flow() +async def check_style(input: AnalysisInput) -> CheckResult: + # Model suggests style/clarity issues; optional: use output_schema for structured rows. + r = await ai.generate( + prompt=f'List style or clarity issues as a short comma-separated line (or "none"): {input.text[:2000]}', + ) + raw = (r.text or '').strip() + issues = [s.strip() for s in raw.split(',') if s.strip() and s.strip().lower() != 'none'] + return CheckResult(issues=issues) + +@app.post('/flow/analyze', response_model=None) +@genkit_fastapi_handler(ai) +@ai.flow() +async def analyze(input: AnalysisInput) -> CombinedAnalysis: + security, bugs, style = await asyncio.gather( + check_security(input), + check_bugs(input), + check_style(input), + ) + return CombinedAnalysis(issues=security.issues + bugs.issues + style.issues) +``` + +--- + +## Structured output endpoint (non-streaming) + +```python +class SentimentResult(BaseModel): + sentiment: str # positive / negative / neutral + confidence: float # 0.0–1.0 + key_phrases: list[str] + +@app.post('/flow/sentiment', response_model=None) +@genkit_fastapi_handler(ai) +@ai.flow() +async def sentiment(input: AnalysisInput) -> SentimentResult: + response = await ai.generate( + prompt=f'Analyze sentiment: {input.text}', + output_format='json', + output_schema=SentimentResult, + ) + return response.output +``` + +Client calls this without `Accept: text/event-stream` — gets `{"result": {...}}` back. + +--- + +## Decorator order + +Must be exactly: `@app.post` → `@genkit_fastapi_handler(ai)` → `@ai.flow()` + +```python +@app.post('/flow/chat', response_model=None) # 1. FastAPI route +@genkit_fastapi_handler(ai) # 2. Genkit wire format + streaming +@ai.flow() # 3. Flow registration +async def chat(input: ChatInput, ctx: ActionRunContext) -> str: + ... +``` + +--- + +## Run with Dev UI + +```bash +GEMINI_API_KEY=your-key genkit start -- uv run src/main.py +``` + +Leave the process running until the CLI prints something like: + +``` +Genkit Developer UI: http://localhost:4000 +``` + +Open that URL. Port may differ if 4000 is busy. \ No newline at end of file diff --git a/.agents/skills/developing-genkit-python/references/setup.md b/.agents/skills/developing-genkit-python/references/setup.md new file mode 100644 index 0000000..3f3b2c4 --- /dev/null +++ b/.agents/skills/developing-genkit-python/references/setup.md @@ -0,0 +1,40 @@ +# Setup — Genkit Python + +## New project + +**Always use a virtual environment** — never install Genkit into the system interpreter. With **uv**, the project’s **`.venv`** is created and used by `uv sync` / `uv run` automatically once you add dependencies. + +```bash +mkdir my-app && cd my-app +uv init +uv venv --python 3.14 .venv +# Unix: source .venv/bin/activate +# Windows: .venv\Scripts\activate +uv add genkit genkit-plugin-google-genai +export GEMINI_API_KEY=your_key_here +``` + +`uv init` creates `pyproject.toml`. Add your app under something like `src/main.py` (or match whatever layout `uv` generated) and point `genkit start` at that entrypoint. + +## pyproject.toml + +Minimal `[project]` block with unpinned Genkit deps (resolver picks compatible releases): + +```toml +[project] +name = "my-app" +version = "0.1.0" +requires-python = ">=3.14" +dependencies = [ + "genkit", + "genkit-plugin-google-genai", +] +``` + +## Plugins + +Packages are **`genkit-plugin-*`** on PyPI, e.g. `genkit-plugin-google-genai`, `genkit-plugin-vertex-ai`, `genkit-plugin-anthropic`, `genkit-plugin-fastapi`. Install with `uv add genkit-plugin-`. + +## Python version + +**3.14+**. Always use a `venv` using `uv venv --python 3.14 .venv` when creating the environment before you run any commands. diff --git a/.agents/skills/firebase-ai-logic-basics/SKILL.md b/.agents/skills/firebase-ai-logic-basics/SKILL.md new file mode 100644 index 0000000..589663e --- /dev/null +++ b/.agents/skills/firebase-ai-logic-basics/SKILL.md @@ -0,0 +1,112 @@ +--- +name: firebase-ai-logic-basics +description: Official skill for integrating Firebase AI Logic (Gemini API) into web applications. Covers setup, multimodal inference, structured output, and security. +version: 1.0.0 +--- + +# Firebase AI Logic Basics + +## Overview + +Firebase AI Logic is a product of Firebase that allows developers to add gen AI to their mobile and web apps using client-side SDKs. You can call Gemini models directly from your app without managing a dedicated backend. Firebase AI Logic, which was previously known as "Vertex AI for Firebase", represents the evolution of Google's AI integration platform for mobile and web developers. + +It supports the two Gemini API providers: +- **Gemini Developer API**: It has a free tier ideal for prototyping, and pay-as-you-go for production +- **Vertex AI Gemini API**: Ideal for scale with enterprise-grade production readiness, requires Blaze plan + +Use the Gemini Developer API as a default, and only Vertex AI Gemini API if the application requires it. + +## Setup & Initialization + +### Prerequisites + +- Before starting, ensure you have **Node.js 16+** and npm installed. Install them if they aren’t already available. +- Identify the platform the user is interested in building on prior to starting: Android, iOS, Flutter or Web. +- If their platform is unsupported, Direct the user to Firebase Docs to learn how to set up AI Logic for their application (share this link with the user https://firebase.google.com/docs/ai-logic/get-started) + +### Installation + +The library is part of the standard Firebase Web SDK. + +`npm install -g firebase@latest` + +If you're in a firebase directory (with a firebase.json) the currently selected project will be marked with "current" using this command: + +`npx -y firebase-tools@latest projects:list` + +Ensure there's at least one app associated with the current project + +`npx -y firebase-tools@latest apps:list` + +Initialize AI logic SDK with the init command + +`npx -y firebase-tools@latest init # Choose AI logic` + +This will automatically enable the Gemini Developer API in the Firebase console. + +More info in [Firebase AI Logic Getting Started](https://firebase.google.com/docs/ai-logic/get-started.md.txt) + +## Core Capabilities + +### Text-Only Generation + +### Multimodal (Text + Images/Audio/Video/PDF input) + +Firebase AI Logic allows Gemini models to analyze image files directly from your app. This enables features like creating captions, answering questions about images, detecting objects, and categorizing images. Beyond images, Gemini can analyze other media types like audio, video, and PDFs by passing them as inline data with their MIME type. For files larger than 20 megabytes (which can cause HTTP 413 errors as inline data), store them in Cloud Storage for Firebase and pass their URLs to the Gemini Developer API. + +### Chat Session (Multi-turn) + +Maintain history automatically using `startChat`. + +### Streaming Responses + +To improve the user experience by showing partial results as they arrive (like a typing effect), use `generateContentStream` instead of `generateContent` for faster display of results. + +### Generate Images with Nano Banana + +- Start with Gemini for most use cases, and choose Imagen for specialized tasks where image quality and specific styles are critical. (Example: gemini-2.5-flash-image) +- Requires an upgraded Blaze pay-as-you-go billing plan. + +### Search Grounding with the built in googleSearch tool + +## Supported Platforms and Frameworks + +Supported Platforms and Frameworks include Kotlin and Java for Android, Swift for iOS, JavaScript for web apps, Dart for Flutter, and C Sharp for Unity. + +## Advanced Features + +### Structured Output (JSON) + +Enforce a specific JSON schema for the response. + +### On-Device AI (Hybrid) + +Hybrid on-device inference for web apps, where the Firebase Javascript SDK automatically checks for Gemini Nano's availability (after installation) and switches between on-device or cloud-hosted prompt execution. This requires specific steps to enable model usage in the Chrome browser, more info in the [hybrid-on-device-inference documentation](https://firebase.google.com/docs/ai-logic/hybrid-on-device-inference.md.txt). + +## Security & Production + +### App Check + +> [!WARNING] +> **Critical Safety Requirement:** In order to use AI Logic safely, you MUST set up App Check on your app. This prevents unauthorized clients from using your API quota and accessing your backend resources. + +See [App Check with reCAPTCHA Enterprise](https://firebase.google.com/docs/app-check/web/recaptcha-enterprise-provider.md.txt) for setup instructions. + +### Remote Config + +Consider that you do not need to hardcode model names (e.g., `gemini-flash-lite-latest`). Use Firebase Remote Config to update model versions dynamically without deploying new client code. See [Changing model names remotely](https://firebase.google.com/docs/ai-logic/change-model-name-remotely.md.txt) + +## Initialization Code References + +| Language, Framework, Platform | Gemini API provider | Context URL | +| :---- | :---- | :---- | +| Web Modular API | Gemini Developer API (Developer API) | firebase://docs/ai-logic/get-started | + +**Always use the most recent version of Gemini (gemini-flash-latest) unless another model is requested by the docs or the user. DO NOT USE gemini-1.5-flash** + +## References + +[Web SDK code examples and usage patterns](references/usage_patterns_web.md) + + + diff --git a/.agents/skills/firebase-ai-logic-basics/references/usage_patterns_web.md b/.agents/skills/firebase-ai-logic-basics/references/usage_patterns_web.md new file mode 100644 index 0000000..e6435bb --- /dev/null +++ b/.agents/skills/firebase-ai-logic-basics/references/usage_patterns_web.md @@ -0,0 +1,174 @@ +# Firebase AI Logic Basics + +## Initialization Pattern +You must initialize the ai-logic service after the main Firebase App. +```JavaScript +import { initializeApp } from "firebase/app"; +import { getAI, getGenerativeModel, GoogleAIBackend } from "firebase/ai"; + + +// If running in Firebase App Hosting, you can skip Firebase Config and instead use: +// const app = initializeApp(); + +const firebaseConfig = { + // ... your firebase config +}; + +const app = initializeApp(firebaseConfig); + +// Initialize the AI Logic service (defaults to Gemini Developer API) +// To set the AI provider, set the backend as the second parameter +const ai = getAI(firebaseApp, { backend: new GoogleAIBackend() }); + +const generationConfig = { + candidate_count: 1, + maxOutputTokens: 2048, + stopSequences: [], + temperature: 0.7, // Balanced: creative but focused + topP: 0.95, // Standard: allows a wide range of probable tokens + topK: 40, // Standard: considers the top 40 tokens +}; + +// Specify the config as part of creating the `GenerativeModel` instance +const model = getGenerativeModel(ai, { model: "gemini-2.5-flash-lite", generationConfig }); +``` + +## Core Capabilities +Text-Only Generation +```JavaScript +async function generateText(prompt) { + const result = await model.generateContent(prompt); + const response = await result.response; + return response.text(); +} +``` + +## Multimodal (Text + Images/Audio/Video/PDF input) +Firebase AI Logic accepts Base64 encoded data or specific file references. +```JavaScript +// Helper to convert file to base64 generic object +async function fileToGenerativePart(file) { + const base64EncodedDataPromise = new Promise((resolve) => { + const reader = new FileReader(); + reader.onloadend = () => resolve(reader.result.split(',')[1]); + reader.readAsDataURL(file); + }); + + return { + inlineData: { + data: await base64EncodedDataPromise, + mimeType: file.type, + }, + }; +} + +async function analyzeImage(prompt, imageFile) { + const imagePart = await fileToGenerativePart(imageFile); + const result = await model.generateContent([prompt, imagePart]); + return result.response.text(); +} +``` + +## Chat Session (Multi-turn) +Maintain history automatically using startChat. +```JavaScript +const chat = model.startChat({ + history: [ + { + role: "user", + parts: [{ text: "Hello, I am a developer." }], + }, + { + role: "model", + parts: [{ text: "Great to meet you. How can I help with code?" }], + }, + ], +}); + +async function sendMessage(msg) { + const result = await chat.sendMessage(msg); + return result.response.text(); +} +``` + +## Streaming Responses +For real-time UI updates (like a typing effect). +```JavaScript +async function streamResponse(prompt) { + const result = await model.generateContentStream(prompt); + for await (const chunk of result.stream) { + const chunkText = chunk.text(); + console.log("Stream chunk:", chunkText); + // Update UI here + } +} +``` + +Generate Images with Nano Banana + +```Javascript +import { initializeApp } from "firebase/app"; +import { getAI, getGenerativeModel, GoogleAIBackend, ResponseModality } from "firebase/ai"; + + +// Initialize FirebaseApp +const firebaseApp = initializeApp(firebaseConfig); + +// Initialize the Gemini Developer API backend service +const ai = getAI(firebaseApp, { backend: new GoogleAIBackend() }); + +// Create a `GenerativeModel` instance with a model that supports your use case +const model = getGenerativeModel(ai, { + model: "gemini-2.5-flash-image", + // Configure the model to respond with text and images (required) + generationConfig: { + responseModalities: [ResponseModality.TEXT, ResponseModality.IMAGE], + }, +}); + +// Provide a text prompt instructing the model to generate an image +const prompt = 'Generate an image of the Eiffel Tower with fireworks in the background.'; + +// To generate an image, call `generateContent` with the text input +const result = model.generateContent(prompt); + +// Handle the generated image +try { + const inlineDataParts = result.response.inlineDataParts(); + if (inlineDataParts?.[0]) { + const image = inlineDataParts[0].inlineData; + console.log(image.mimeType, image.data); + } +} catch (err) { + console.error('Prompt or candidate was blocked:', err); +} +``` + +## Advanced Features +Structured Output (JSON) +Enforce a specific JSON schema for the response. +```JavaScript +import { getGenerativeModel, Schema } from "firebase/ai"; +const jsonModel = getGenerativeModel(ai, { + model: "gemini-2.5-flash-lite", + generationConfig: { + responseMimeType: "application/json", + // Optional: Define a schema + schema = Schema.object({ ... }); + } +}); + +async function getJsonData(prompt) { + const result = await jsonModel.generateContent(prompt); + return JSON.parse(result.response.text()); +} +``` + +On-Device AI (Hybrid) +Automatically switch between local Gemini Nano and cloud models based on device capability. +```JavaScript +import {getGenerativeModel, InferenceMode } from "firebase/ai"; + +const hybridModel = getGenerativeModel(ai, { mode: InferenceMode.PREFER_ON_DEVICE }); +``` + diff --git a/.agents/skills/firebase-app-hosting-basics/SKILL.md b/.agents/skills/firebase-app-hosting-basics/SKILL.md new file mode 100644 index 0000000..4aa23b7 --- /dev/null +++ b/.agents/skills/firebase-app-hosting-basics/SKILL.md @@ -0,0 +1,58 @@ +--- +name: firebase-app-hosting-basics +description: Deploy and manage web apps with Firebase App Hosting. Use this skill when deploying Next.js/Angular apps with backends. +--- + +# App Hosting Basics + +## Description +This skill enables the agent to deploy and manage modern, full-stack web applications (Next.js, Angular, etc.) using Firebase App Hosting. + +**Important**: In order to use App Hosting, your Firebase project must be on the Blaze pricing plan. Direct the user to https://console.firebase.google.com/project/_/overview?purchaseBillingPlan=metered to upgrade their plan. + +## Hosting vs App Hosting + +**Choose Firebase Hosting if:** +- You are deploying a static site (HTML/CSS/JS). +- You are deploying a simple SPA (React, Vue, etc. without SSR). +- You want full control over the build and deploy process via CLI. + +**Choose Firebase App Hosting if:** +- You are using a supported full-stack framework like Next.js or Angular. +- You need Server-Side Rendering (SSR) or ISR. +- You want an automated "git push to deploy" workflow with zero configuration. + +## Deploying to App Hosting + +### Deploy from Source + +This is the recommended flow for most users. +1. Configure `firebase.json` with an `apphosting` block. + ```json + { + "apphosting": { + "backendId": "my-app-id", + "rootDir": "/", + "ignore": [ + "node_modules", + ".git", + "firebase-debug.log", + "firebase-debug.*.log", + "functions" + ] + } + } + ``` +2. Create or edit `apphosting.yaml`- see [Configuration](references/configuration.md) for more information on how to do so. +3. If the app needs safe access to sensitive keys, use `npx -y firebase-tools@latest apphosting:secrets` commands to set and grant access to secrets. +4. Run `npx -y firebase-tools@latest deploy` when you are ready to deploy. + +### Automated deployment via GitHub (CI/CD) + +Alternatively, set up a backend connected to a GitHub repository for automated deployments "git push" deployments. +This is only recommended for more advanced users, and is not required to use App Hosting. +See [CLI Commands](references/cli_commands.md) for more information on how to set this up using CLI commands. + +## Emulation + +See [Emulation](references/emulation.md) for more information on how to test your app locally using the Firebase Local Emulator Suite. diff --git a/.agents/skills/firebase-app-hosting-basics/references/cli_commands.md b/.agents/skills/firebase-app-hosting-basics/references/cli_commands.md new file mode 100644 index 0000000..c758c9d --- /dev/null +++ b/.agents/skills/firebase-app-hosting-basics/references/cli_commands.md @@ -0,0 +1,71 @@ +# App Hosting CLI Commands + +The Firebase CLI provides a comprehensive suite of commands to manage App Hosting resources. These commands are often faster and more scriptable than using the Firebase Console. + +## Initialization + +### `npx -y firebase-tools@latest init apphosting` + +- **Purpose**: Interactive command that sets up App Hosting in your local project. +Use this command only if you are able to handle interactive CLI inputs well. +Alternatively, you can manually edit `firebase.json` and `apphosting.yml`. + +- **Effect**: + - Detects your web framework. + - Creates/updates `apphosting.yaml`. + - Can optionally create a backend if one doesn't exist. + +## Backend Management + +### `npx -y firebase-tools@latest apphosting:backends:list` + +- **Purpose**: Lists all backends in the current project. + +### `npx -y firebase-tools@latest apphosting:backends:get ` + +- **Purpose**: Shows details for a specific backend. + +### `npx -y firebase-tools@latest apphosting:backends:delete ` + +- **Purpose**: Deletes a backend and its associated resources. + +### `npx -y firebase-tools@latest apphosting:rollouts:list ` + +- **Purpose**: Lists the history of rollouts for a backend. + +## Secrets Management + +App Hosting uses Cloud Secret Manager to securely handle sensitive environment variables (like API keys). + +### `npx -y firebase-tools@latest apphosting:secrets:set ` + +- **Purpose**: Creates or updates a secret in Cloud Secret Manager and makes it available to App Hosting. +- **Behavior**: Prompts for the secret value (hidden input). + +### `npx -y firebase-tools@latest apphosting:secrets:grantaccess ` + +- **Purpose**: Grants the App Hosting service account permission to access the secret. +- **Note**: Often handled automatically by `secrets:set`, but useful for debugging permission issues or granting access to existing secrets. + +## Automated deployment via GitHub (CI/CD) + +**IMPORTANT** Only use these commands if you are setting up automated deployments via GitHub. If you are managing deployments using `npx -y firebase-tools@latest deploy`, DO NOT use these commands. + +### `npx -y firebase-tools@latest apphosting:rollouts:create ` + +- **Purpose**: Manually triggers a new rollout (deployment). +- **Options**: + - `--git-branch `: Deploy the latest commit from a specific branch. + - `--git-commit `: Deploy a specific commit. +- **Use Case**: Useful for redeploying without code changes, or rolling back to a specific commit. + +### `npx -y firebase-tools@latest apphosting:backends:create` + +- **Purpose**: Creates a new App Hosting backend. Use this when setting up automated deployments via GitHub. +- **Options**: + - `--app `: The ID of an existing Firebase web app to associate with the backend. + - `--backend `: The ID of the new backend. + - `--primary-region `: The primary region for the backend. + - `--root-dir `: The root directory for the backend. If omitted, defaults to the root directory of the project. + - `--service-account `: The service account used to run the server. If omitted, defaults to the default service account. + \ No newline at end of file diff --git a/.agents/skills/firebase-app-hosting-basics/references/configuration.md b/.agents/skills/firebase-app-hosting-basics/references/configuration.md new file mode 100644 index 0000000..da10766 --- /dev/null +++ b/.agents/skills/firebase-app-hosting-basics/references/configuration.md @@ -0,0 +1,51 @@ +# App Hosting Configuration (`apphosting.yaml`) + +The `apphosting.yaml` file is the source of truth for your backend's configuration. It must be located in the root of your app's directory (or the specific root directory if using a monorepo). + +## File Structure + +```yaml +# apphosting.yaml + +# Cloud Run service configuration +runConfig: + cpu: 1 + memoryMiB: 512 + minInstances: 0 + maxInstances: 100 + concurrency: 80 + +# Environment variables +env: + - variable: STORAGE_BUCKET + value: mybucket.app + availability: + - BUILD + - RUNTIME + - variable: API_KEY + secret: myApiKeySecret +``` + +## `runConfig` +Controls the resources allocated to the Cloud Run service that serves your app. +- `cpu`: Number of vCPUs. Note: If `< 1`, concurrency MUST be set to `1`. +- `memoryMiB`: RAM in MiB (128 to 32768). +- `minInstances`: Minimum containers to keep warm (default 0). Set to >= 1 to avoid cold starts. +- `maxInstances`: Maximum scaling limit (default 100). +- `concurrency`: Max concurrent requests per instance (default 80). + +### Resource Constraints +- **CPU vs Memory**: Higher memory often requires higher CPU. + - > 4GiB RAM -> Needs >= 2 vCPU + - > 8GiB RAM -> Needs >= 4 vCPU + +## `env` (Environment Variables) +Defines environment variables available during build and/or runtime. + +- `variable`: The name of the env var (e.g., `NEXT_PUBLIC_API_URL`). +- `value`: A literal string value. +- `secret`: The name of a secret in Cloud Secret Manager. use `npx -y firebase-tools@latest apphosting:secrets:set` to create these. +- `availability`: Where the variable is needed. + - `BUILD`: Available during the `npm run build` process. + - `RUNTIME`: Available when the app is serving requests. + - Defaults to both if not specified. diff --git a/.agents/skills/firebase-app-hosting-basics/references/emulation.md b/.agents/skills/firebase-app-hosting-basics/references/emulation.md new file mode 100644 index 0000000..299dcde --- /dev/null +++ b/.agents/skills/firebase-app-hosting-basics/references/emulation.md @@ -0,0 +1,47 @@ +# App Hosting Emulation + +You can test your App Hosting setup locally using the Firebase Local Emulator Suite. This allows you to verify your app's behavior with environment variables and secrets before deploying. + +## Configuration: `apphosting.emulator.yaml` +This optional file overrides `apphosting.yaml` settings specifically for the local emulator. Use it to provide local secret values or override resource configs. If it contains sensitive values such as API keys, do not commit it to source control. + +```yaml +# apphosting.emulator.yaml (gitignored usually) +runConfig: + cpu: 1 + memoryMiB: 512 + +env: + - variable: API_KEY + value: "local-dev-api-key" # Override secret with local value +``` + +## Running the Emulator +To start the App Hosting emulator: + +```bash +npx -y firebase-tools@latest emulators:start --only apphosting +``` + +Or, if you are also using other emulators (Auth, Firestore, etc.): + +```bash +npx -y firebase-tools@latest emulators:start +``` + +## Capabilities +- **Builds your app**: Runs the build command defined in your `package.json` to generate the serving artifact. +- **Serves locally**: Runs the app on `localhost:5004` (default). +Configurable by setting `host` and `port` in the `emulators` block of `firebase.json`, like so: + +```json +{ + "emulators": { + "apphosting": { + "host": "localhost", + "port": 5004 + } + } +} +``` +- **Env Var Injection**: Injects variables defined in `apphosting.yaml` and `apphosting.emulator.yaml` into the process. diff --git a/.agents/skills/firebase-auth-basics/SKILL.md b/.agents/skills/firebase-auth-basics/SKILL.md new file mode 100644 index 0000000..bac12ad --- /dev/null +++ b/.agents/skills/firebase-auth-basics/SKILL.md @@ -0,0 +1,86 @@ +--- +name: firebase-auth-basics +description: Guide for setting up and using Firebase Authentication. Use this skill when the user's app requires user sign-in, user management, or secure data access using auth rules. +compatibility: This skill is best used with the Firebase CLI, but does not require it. Firebase CLI can be accessed through `npx -y firebase-tools@latest`. +--- + +## Prerequisites + +- **Firebase Project**: Created via `npx -y firebase-tools@latest projects:create` (see `firebase-basics`). +- **Firebase CLI**: Installed and logged in (see `firebase-basics`). + +## Core Concepts + +Firebase Authentication provides backend services, easy-to-use SDKs, and ready-made UI libraries to authenticate users to your app. + +### Users + +A user is an entity that can sign in to your app. Each user is identified by a unique ID (`uid`) which is guaranteed to be unique across all providers. +User properties include: +- `uid`: Unique identifier. +- `email`: User's email address (if available). +- `displayName`: User's display name (if available). +- `photoURL`: URL to user's photo (if available). +- `emailVerified`: Boolean indicating if the email is verified. + +### Identity Providers + +Firebase Auth supports multiple ways to sign in: +- **Email/Password**: Basic email and password authentication. +- **Federated Identity Providers**: Google, Facebook, Twitter, GitHub, Microsoft, Apple, etc. +- **Phone Number**: SMS-based authentication. +- **Anonymous**: Temporary guest accounts that can be linked to permanent accounts later. +- **Custom Auth**: Integrate with your existing auth system. + +Google Sign In is recommended as a good and secure default provider. + +### Tokens + +When a user signs in, they receive an ID Token (JWT). This token is used to identify the user when making requests to Firebase services (Realtime Database, Cloud Storage, Firestore) or your own backend. +- **ID Token**: Short-lived (1 hour), verifies identity. +- **Refresh Token**: Long-lived, used to get new ID tokens. + +## Workflow + +### 1. Provisioning + +#### Option 1. Enabling Authentication via CLI + +Only Google Sign In, anonymous auth, and email/password auth can be enabled via CLI. For other providers, use the Firebase Console. + +Configure Firebase Authentication in `firebase.json` by adding an 'auth' block: + +``` +{ + "auth": { + "providers": { + "anonymous": true, + "emailPassword": true, + "googleSignIn": { + "oAuthBrandDisplayName": "Your Brand Name", + "supportEmail": "support@example.com", + "authorizedRedirectUris": ["https://example.com"] + } + } + } +} +``` + +#### Option 2. Enabling Authentication in Console + +Enable other providers in the Firebase Console. + +1. Go to the https://console.firebase.google.com/project/_/authentication/providers +2. Select your project. +3. Enable the desired Sign-in providers (e.g., Email/Password, Google). + +### 2. Client Setup & Usage + +**Web** +See [references/client_sdk_web.md](references/client_sdk_web.md). + +### 3. Security Rules + +Secure your data using `request.auth` in Firestore/Storage rules. + +See [references/security_rules.md](references/security_rules.md). diff --git a/.agents/skills/firebase-auth-basics/references/client_sdk_web.md b/.agents/skills/firebase-auth-basics/references/client_sdk_web.md new file mode 100644 index 0000000..493a66a --- /dev/null +++ b/.agents/skills/firebase-auth-basics/references/client_sdk_web.md @@ -0,0 +1,287 @@ +# Firebase Authentication Web SDK + +## Initialization + +First, ensure you have initialized the Firebase App (see `firebase-basics` skill). Then, initialize the Auth service: + +```javascript +import { getAuth } from "firebase/auth"; +import { app } from "./firebase"; // Your initialized Firebase App + +const auth = getAuth(app); +export { auth }; +``` + +## Connect to Emulator + +If you are running the Authentication emulator (usually on port 9099), connect to it immediately after initialization. + +```javascript +import { getAuth, connectAuthEmulator } from "firebase/auth"; + +const auth = getAuth(); +// Connect to emulator if running locally +if (location.hostname === "localhost") { + connectAuthEmulator(auth, "http://localhost:9099"); +} +``` + +## Sign Up with Email/Password + +```javascript +import { getAuth, createUserWithEmailAndPassword } from "firebase/auth"; + +const auth = getAuth(); +createUserWithEmailAndPassword(auth, email, password) + .then((userCredential) => { + const user = userCredential.user; + // ... + }) + .catch((error) => { + const errorCode = error.code; + const errorMessage = error.message; + // .. + }); +``` + +## Sign In with Google (Popup) + +```javascript +import { getAuth, signInWithPopup, GoogleAuthProvider } from "firebase/auth"; + +const auth = getAuth(); +const provider = new GoogleAuthProvider(); + +signInWithPopup(auth, provider) + .then((result) => { + // This gives you a Google Access Token. You can use it to access the Google API. + const credential = GoogleAuthProvider.credentialFromResult(result); + const token = credential.accessToken; + // The signed-in user info. + const user = result.user; + // ... + }) + .catch((error) => { + // Handle Errors here. + const errorCode = error.code; + const errorMessage = error.message; + // ... + }); +``` + +## Sign In with Facebook (Popup) + +```javascript +import { getAuth, signInWithPopup, FacebookAuthProvider } from "firebase/auth"; + +const auth = getAuth(); +const provider = new FacebookAuthProvider(); + +signInWithPopup(auth, provider) + .then((result) => { + // The signed-in user info. + const user = result.user; + // This gives you a Facebook Access Token. You can use it to access the Facebook API. + const credential = FacebookAuthProvider.credentialFromResult(result); + const accessToken = credential.accessToken; + }) + .catch((error) => { + // Handle Errors here. + }); +``` + +## Sign In with Apple (Popup) + +```javascript +import { getAuth, signInWithPopup, OAuthProvider } from "firebase/auth"; + +const auth = getAuth(); +const provider = new OAuthProvider('apple.com'); + +signInWithPopup(auth, provider) + .then((result) => { + const user = result.user; + // Apple credential + const credential = OAuthProvider.credentialFromResult(result); + const accessToken = credential.accessToken; + }) + .catch((error) => { + // Handle Errors here. + }); +``` + +## Sign In with Twitter (Popup) + +```javascript +import { getAuth, signInWithPopup, TwitterAuthProvider } from "firebase/auth"; + +const auth = getAuth(); +const provider = new TwitterAuthProvider(); + +signInWithPopup(auth, provider) + .then((result) => { + const user = result.user; + // Twitter credential + const credential = TwitterAuthProvider.credentialFromResult(result); + const token = credential.accessToken; + const secret = credential.secret; + }) + .catch((error) => { + // Handle Errors here. + }); +``` + +## Sign In with GitHub (Popup) + +```javascript +import { getAuth, signInWithPopup, GithubAuthProvider } from "firebase/auth"; + +const auth = getAuth(); +const provider = new GithubAuthProvider(); + +signInWithPopup(auth, provider) + .then((result) => { + const user = result.user; + const credential = GithubAuthProvider.credentialFromResult(result); + const token = credential.accessToken; + }) + .catch((error) => { + // Handle Errors here. + }); +``` + +## Sign In with Microsoft (Popup) + +```javascript +import { getAuth, signInWithPopup, OAuthProvider } from "firebase/auth"; + +const auth = getAuth(); +const provider = new OAuthProvider('microsoft.com'); + +signInWithPopup(auth, provider) + .then((result) => { + const user = result.user; + const credential = OAuthProvider.credentialFromResult(result); + const accessToken = credential.accessToken; + }) + .catch((error) => { + // Handle Errors here. + }); +``` + +## Sign In with Yahoo (Popup) + +```javascript +import { getAuth, signInWithPopup, OAuthProvider } from "firebase/auth"; + +const auth = getAuth(); +const provider = new OAuthProvider('yahoo.com'); + +signInWithPopup(auth, provider) + .then((result) => { + const user = result.user; + const credential = OAuthProvider.credentialFromResult(result); + const accessToken = credential.accessToken; + }) + .catch((error) => { + // Handle Errors here. + }); +``` + +## Sign In Anonymously + +```javascript +import { getAuth, signInAnonymously } from "firebase/auth"; + +const auth = getAuth(); +signInAnonymously(auth) + .then(() => { + // Signed in.. + }) + .catch((error) => { + const errorCode = error.code; + const errorMessage = error.message; + }); +``` + +## Email Link Authentication + +**1. Send Auth Link** + +```javascript +import { getAuth, sendSignInLinkToEmail } from "firebase/auth"; + +const auth = getAuth(); +const actionCodeSettings = { + // URL you want to redirect back to. The domain must be in the authorized domains list in Firebase Console. + url: 'https://www.example.com/finishSignUp?cartId=1234', + handleCodeInApp: true, +}; + +sendSignInLinkToEmail(auth, email, actionCodeSettings) + .then(() => { + // Save the email locally so you don't need to ask the user for it again + window.localStorage.setItem('emailForSignIn', email); + }) + .catch((error) => { + // Error + }); +``` + +**2. Complete Sign In (on landing page)** + +```javascript +import { getAuth, isSignInWithEmailLink, signInWithEmailLink } from "firebase/auth"; + +const auth = getAuth(); + +if (isSignInWithEmailLink(auth, window.location.href)) { + let email = window.localStorage.getItem('emailForSignIn'); + if (!email) { + email = window.prompt('Please provide your email for confirmation'); + } + + signInWithEmailLink(auth, email, window.location.href) + .then((result) => { + window.localStorage.removeItem('emailForSignIn'); + // You can check result.user + }) + .catch((error) => { + // Error + }); +} +``` + +## Observe Auth State + +Recommended way to get the current user. This listener triggers whenever the user signs in or out. + +```javascript +import { getAuth, onAuthStateChanged } from "firebase/auth"; + +const auth = getAuth(); +onAuthStateChanged(auth, (user) => { + if (user) { + // User is signed in, see docs for a list of available properties + // https://firebase.google.com/docs/reference/js/firebase.User + const uid = user.uid; + // ... + } else { + // User is signed out + // ... + } +}); +``` + +## Sign Out + +```javascript +import { getAuth, signOut } from "firebase/auth"; + +const auth = getAuth(); +signOut(auth).then(() => { + // Sign-out successful. +}).catch((error) => { + // An error happened. +}); +``` diff --git a/.agents/skills/firebase-auth-basics/references/security_rules.md b/.agents/skills/firebase-auth-basics/references/security_rules.md new file mode 100644 index 0000000..5de862a --- /dev/null +++ b/.agents/skills/firebase-auth-basics/references/security_rules.md @@ -0,0 +1,38 @@ +# Authentication in Security Rules + +Firebase Security Rules work with Firebase Authentication to provide rule-based access control. For better advice on writing safe security rules, +enable the `firebase-firestore-basics` or `firebase-storage-basics` skills. + +The `request.auth` variable contains authentication information for the user requesting data. + +## Basic Checks + +### Check if user is signed in +``` +allow read, write: if request.auth != null; +``` + +### Check if user owns the data +Access data only if the document ID matches the user's UID. +``` +allow read, write: if request.auth != null && request.auth.uid == userId; +``` +(Where `userId` is a path variable, e.g., `match /users/{userId}`) + +### Check if user owns the document (field-based) +Access data only if the document has a `owner_uid` field matching the user's UID. +``` +allow read, write: if request.auth != null && request.auth.uid == resource.data.owner_uid; +``` + +## Token Properties +`request.auth.token` contains standard JWT claims and custom claims. + +- `request.auth.token.email`: The user's email address. +- `request.auth.token.email_verified`: If the email is verified. +- `request.auth.token.name`: The user's display name. + +### Example: Email Verification Check +``` +allow create: if request.auth.token.email_verified == true; +``` diff --git a/.agents/skills/firebase-basics/SKILL.md b/.agents/skills/firebase-basics/SKILL.md new file mode 100644 index 0000000..c8f1510 --- /dev/null +++ b/.agents/skills/firebase-basics/SKILL.md @@ -0,0 +1,68 @@ +--- +name: firebase-basics +description: >- + Provides foundational setup, authentication, and project management workflows + for Firebase using the Firebase CLI. Use when checking Firebase CLI version + (must use 'npx -y firebase-tools@latest --version'), initializing a Firebase + environment, authenticating, setting active projects, or executing core + Firebase CLI commands. +--- +# Prerequisites + +Complete these setup steps before proceeding: + +1. **Local Environment Setup:** Verify the environment is properly set up so we can use Firebase tools: + - Run `npx -y firebase-tools@latest --version` to check if the Firebase CLI is installed. + - Verify if the Firebase MCP server is installed using your existing tools. + - **CRITICAL**: Before configuring any extensions or agent environments below, you MUST read [references/local-env-setup.md](references/local-env-setup.md). + - To fully manage Firebase, ensure you have installed the necessary agent skills and MCP server for your environment: + - **Gemini CLI**: Review [references/setup/gemini_cli.md](references/setup/gemini_cli.md) + - **Antigravity**: Review [references/setup/antigravity.md](references/setup/antigravity.md) + - **Claude Code**: Review [references/setup/claude_code.md](references/setup/claude_code.md) + - **Cursor**: Review [references/setup/cursor.md](references/setup/cursor.md) + - **GitHub Copilot**: Review [references/setup/github_copilot.md](references/setup/github_copilot.md) + - **Other Agents**: Review [references/setup/other_agents.md](references/setup/other_agents.md) + +2. **Authentication:** + Ensure you are logged in to Firebase so that commands have the correct permissions. Run `npx -y firebase-tools@latest login`. For environments without a browser (e.g., remote shells), use `npx -y firebase-tools@latest login --no-localhost`. + - The command should output the current user. + - If you are not logged in, follow the interactive instructions from this command to authenticate. + +3. **Active Project:** + Most Firebase tasks require an active project context. Check the current project by running `npx -y firebase-tools@latest use`. + - If the command outputs `Active Project: `, you can proceed with your task. + - If not, ask the user if they have an existing Firebase Project ID. + - If yes: Set it as the active project and add a default alias by running: + ```bash + npx -y firebase-tools@latest use --add + ``` + - If no: Follow these steps to create a new Firebase project from the CLI: + ```bash + npx -y firebase-tools@latest projects:create --display-name "" + ``` + *Note: The `` must be 6-30 characters, lowercase, and can contain digits and hyphens. It must be globally unique.* + +# Firebase Usage Principles + +Adhere to these principles: + +1. **Use npx for CLI commands:** To ensure you always use the latest version of the Firebase CLI, always prepend commands with `npx -y firebase-tools@latest` instead of just `firebase`. For example, use `npx -y firebase-tools@latest --version`. NEVER suggest the naked `firebase` command as an alternative. +2. **Prioritize official knowledge:** For any Firebase-related knowledge, consult the `developerknowledge_search_documents` MCP tool before falling back to Google Search or your internal knowledge base. Including "Firebase" in your search query significantly improves relevance. +3. **Follow Agent Skills for implementation guidance:** Skills provide opinionated workflows (CUJs), security rules, and best practices. Always consult them to understand *how* to implement Firebase features correctly instead of relying on general knowledge. +4. **Use Firebase MCP Server tools instead of direct API calls:** Whenever you need to interact with remote Firebase APIs (such as fetching Crashlytics logs or executing Data Connect queries), use the tools provided by the Firebase MCP Server instead of attempting manual API calls. +5. **Keep Plugin / Agent Skills updated:** Since Firebase best practices evolve quickly, remind the user to regularly check for and install updates to their Firebase plugin or Agent Skills, ensuring you both operate with the latest workflows. Similarly, if you encounter issues with outdated tools or commands, follow the steps below based on your agent environment: + - **Antigravity**: Follow [references/refresh/antigravity.md](references/refresh/antigravity.md) + - **Gemini CLI**: Follow [references/refresh/gemini-cli.md](references/refresh/gemini-cli.md) + - **Claude Code**: Follow [references/refresh/claude.md](references/refresh/claude.md) + - **Cursor**: Follow [references/refresh/other-agents.md](references/refresh/other-agents.md) + - **Others**: Follow [references/refresh/other-agents.md](references/refresh/other-agents.md) + +# References + +- **Initialize Firebase:** See [references/firebase-service-init.md](references/firebase-service-init.md) when you need to initialize new Firebase services using the CLI. +- **Exploring Commands:** See [references/firebase-cli-guide.md](references/firebase-cli-guide.md) to discover and understand CLI functionality. +- **SDK Setup:** For detailed guides on adding Firebase to a web app, see [references/web_setup.md](references/web_setup.md). + +# Common Issues + +- **Login Issues:** If the browser fails to open during the login step, use `npx -y firebase-tools@latest login --no-localhost` instead. diff --git a/.agents/skills/firebase-basics/references/firebase-cli-guide.md b/.agents/skills/firebase-basics/references/firebase-cli-guide.md new file mode 100644 index 0000000..36a4480 --- /dev/null +++ b/.agents/skills/firebase-basics/references/firebase-cli-guide.md @@ -0,0 +1,16 @@ +# Exploring Commands + +The Firebase CLI documents itself. Use help commands to discover functionality. + +- **Global Help**: List all available commands and categories. + ```bash + npx -y firebase-tools@latest --help + ``` + +- **Command Help**: Get detailed usage for a specific command. + ```bash + npx -y firebase-tools@latest [command] --help + # Example: + npx -y firebase-tools@latest deploy --help + npx -y firebase-tools@latest firestore:indexes --help + ``` diff --git a/.agents/skills/firebase-basics/references/firebase-service-init.md b/.agents/skills/firebase-basics/references/firebase-service-init.md new file mode 100644 index 0000000..13800aa --- /dev/null +++ b/.agents/skills/firebase-basics/references/firebase-service-init.md @@ -0,0 +1,18 @@ +# Initialization + +Before initializing, check if you are already in a Firebase project directory by looking for `firebase.json`. + +1. **Project Directory:** + Navigate to the root directory of the codebase. + *(Only if starting a completely new project from scratch without an existing codebase, create a directory first: `mkdir my-project && cd my-project`)* + +2. **Initialize Services:** + Run the initialization command: + ```bash + npx -y firebase-tools@latest init + ``` + +The CLI will guide you through: +- Selecting features (Firestore, Functions, Hosting, etc.). +- Associating with an existing project or creating a new one. +- Configuring files (e.g. `firebase.json`, `.firebaserc`). diff --git a/.agents/skills/firebase-basics/references/local-env-setup.md b/.agents/skills/firebase-basics/references/local-env-setup.md new file mode 100644 index 0000000..bce7b1b --- /dev/null +++ b/.agents/skills/firebase-basics/references/local-env-setup.md @@ -0,0 +1,56 @@ +# Firebase Local Environment Setup + +This skill documents the bare minimum setup required for a full Firebase experience for the agent. Before starting to use any Firebase features, you MUST verify that each of the following steps has been completed. + +## 1. Verify Node.js +- **Action**: Run `node --version`. +- **Handling**: Ensure Node.js is installed and the version is `>= 20`. If Node.js is missing or `< v20`, install it based on the operating system: + + **Recommended: Use a Node Version Manager** + This avoids permission issues when installing global packages. + + **For macOS or Linux:** + 1. Guide the user to the [official nvm repository](https://github.com/nvm-sh/nvm#installing-and-updating). + 2. Request the user to manually install `nvm` and reply when finished. **Stop and wait** for the user's confirmation. + 3. Make `nvm` available in the current terminal session by sourcing the appropriate profile: + ```bash + # For Bash + source ~/.bash_profile + source ~/.bashrc + + # For Zsh + source ~/.zprofile + source ~/.zshrc + ``` + 4. Install Node.js: + ```bash + nvm install 24 + nvm use 24 + ``` + + **For Windows:** + 1. Guide the user to download and install [nvm-windows](https://github.com/coreybutler/nvm-windows/releases). + 2. Request the user to manually install `nvm-windows` and Node.js, and reply when finished. **Stop and wait** for the user's confirmation. + 3. After the user confirms, verify Node.js is available: + ```bash + node --version + ``` + + **Alternative: Official Installer** + 1. Guide the user to download and install the LTS version from [nodejs.org](https://nodejs.org/en/download). + 2. Request the user to manually install Node.js and reply when finished. **Stop and wait** for the user's confirmation. + +## 2. Verify Firebase CLI +- **Command**: `npx -y firebase-tools@latest --version` +- **Expected**: Successfully outputs a version string. + +## 3. Verify Firebase Authentication +You must be authenticated to manage Firebase projects. +- **Action**: Run `npx -y firebase-tools@latest login`. +- **Handling**: If the environment is remote or restricted (no browser access), run `npx -y firebase-tools@latest login --no-localhost` instead. + +## 4. Install Agent Skills and MCP Server +To fully manage Firebase, the agent needs specific skills and the Firebase MCP server installed. Refer to the main `SKILL.md` for direct links to the installation instructions specific to your agent environment. + +--- +**CRITICAL AGENT RULE:** Do NOT proceed with any other Firebase tasks until EVERY step above has been successfully verified and completed. diff --git a/.agents/skills/firebase-basics/references/refresh/antigravity.md b/.agents/skills/firebase-basics/references/refresh/antigravity.md new file mode 100644 index 0000000..6f5ee32 --- /dev/null +++ b/.agents/skills/firebase-basics/references/refresh/antigravity.md @@ -0,0 +1,46 @@ +# Refresh Antigravity Local Environment + +Follow these steps to refresh Antigravity's local environment, ensuring that their agent skills and plugins are fully up-to-date. + +Antigravity relies on `npx skills`. Updates should prioritize the global level; if skills are installed at the project level, omit the `--global` flag. + +1. **List Available Skills:** Identify all Firebase skills available in the repository: + ```bash + npx -y skills add firebase/agent-skills --list + ``` + +2. **Check Currently Installed Skills:** Determine which skills are already installed and linked at both project and global levels: + ```bash + # Check project-level skills + npx -y skills list --agent antigravity + + # Check global-level skills + npx -y skills list --agent antigravity --global + ``` + *Note: If a skill shows `Agents: not linked` in the output, it is installed but not currently available to Antigravity.* + +3. **Add Missing or Unlinked Skills:** Compare the results from Step 1 and Step 2. For each missing or unlinked skill, follow the appropriate conditional instruction below: + + - **IF any Firebase skills were found at the PROJECT level in Step 2:** Add the missing skill to the project level: + ```bash + npx -y skills add firebase/agent-skills --agent antigravity --skill "" --yes + ``` + + - **IF any Firebase skills were found at the GLOBAL level in Step 2:** Add the missing skill to the global level: + ```bash + npx -y skills add firebase/agent-skills --agent antigravity --skill "" --global --yes + ``` + + - **IF NO Firebase skills were found in Step 2:** Add each missing skill to the global level: + ```bash + npx -y skills add firebase/agent-skills --agent antigravity --skill "" --global --yes + ``` + +4. **Update Existing Skills:** Update all currently installed skills to their latest versions: + ```bash + # Update project-level skills + npx -y skills update --agent antigravity --yes + + # Update global-level skills + npx -y skills update --agent antigravity --global --yes + ``` diff --git a/.agents/skills/firebase-basics/references/refresh/claude.md b/.agents/skills/firebase-basics/references/refresh/claude.md new file mode 100644 index 0000000..772314e --- /dev/null +++ b/.agents/skills/firebase-basics/references/refresh/claude.md @@ -0,0 +1,10 @@ +# Refresh Claude Code Local Environment + +Follow these steps to refresh Claude Code's local environment, ensuring that their agent skills and plugins are fully up-to-date. + +Use Claude Code's native plugin manager instead of `npx`. + +1. **Update the Plugin:** Run the specific CLI command to update the Firebase plugin: + ```bash + claude plugin update firebase@firebase + ``` \ No newline at end of file diff --git a/.agents/skills/firebase-basics/references/refresh/gemini-cli.md b/.agents/skills/firebase-basics/references/refresh/gemini-cli.md new file mode 100644 index 0000000..6cc1467 --- /dev/null +++ b/.agents/skills/firebase-basics/references/refresh/gemini-cli.md @@ -0,0 +1,11 @@ +# Refresh Gemini CLI Local Environment + +Follow these steps to refresh Gemini CLI's local environment, ensuring that their agent skills and plugins are fully up-to-date. + +Use the native Gemini CLI extension manager instead of `npx`. + +1. **Update the Extension:** Run the specific CLI command to update: + ```bash + gemini extensions update firebase + ``` + *Note: If the extension is named differently, replace `firebase` with the correct name from `gemini extensions list`.* diff --git a/.agents/skills/firebase-basics/references/refresh/other-agents.md b/.agents/skills/firebase-basics/references/refresh/other-agents.md new file mode 100644 index 0000000..f624c96 --- /dev/null +++ b/.agents/skills/firebase-basics/references/refresh/other-agents.md @@ -0,0 +1,48 @@ +# Refresh Other Local Environment + +Follow these steps to refresh the local environment of other agents, ensuring that their agent skills and plugins are fully up-to-date. + +Other agents rely on `npx skills`. Updates should prioritize the global level; if skills are installed at the project level, omit the `--global` flag. + +Replace `` with the actual agent name, which can be found in the [skills repository README](https://github.com/vercel-labs/skills/blob/main/README.md). + +1. **List Available Skills:** Identify all Firebase skills available in the repository: + ```bash + npx -y skills add firebase/agent-skills --list + ``` + +2. **Check Currently Installed Skills:** Determine which skills are already installed and linked for the agent at both project and global levels: + ```bash + # Check project-level skills + npx -y skills list --agent + + # Check global-level skills + npx -y skills list --agent --global + ``` + *Note: If a skill shows `Agents: not linked` in the output, it is installed but not currently available to the agent.* + +3. **Add Missing or Unlinked Skills:** Compare the results from Step 1 and Step 2. For each missing or unlinked skill, follow the appropriate conditional instruction below: + + - **IF any Firebase skills were found at the PROJECT level in Step 2:** Add the missing skill to the project level: + ```bash + npx -y skills add firebase/agent-skills --agent --skill "" --yes + ``` + + - **IF any Firebase skills were found at the GLOBAL level in Step 2:** Add the missing skill to the global level: + ```bash + npx -y skills add firebase/agent-skills --agent --skill "" --global --yes + ``` + + - **IF NO Firebase skills were found in Step 2:** Add each missing skill to the global level: + ```bash + npx -y skills add firebase/agent-skills --agent --skill "" --global --yes + ``` + +4. **Update Existing Skills:** Update all currently installed skills to their latest versions: + ```bash + # Update project-level skills + npx -y skills update --agent --yes + + # Update global-level skills + npx -y skills update --agent --global --yes + ``` diff --git a/.agents/skills/firebase-basics/references/setup/antigravity.md b/.agents/skills/firebase-basics/references/setup/antigravity.md new file mode 100644 index 0000000..99110db --- /dev/null +++ b/.agents/skills/firebase-basics/references/setup/antigravity.md @@ -0,0 +1,63 @@ +# Antigravity Setup + +To get the most out of Firebase in Antigravity, follow these steps to install the agent skills and the MCP server. + +### 1. Install and Verify Firebase Skills +Check if the skills are already installed before proceeding: + +1. **Check Local skills**: Run `ls -d .agent/skills/firebase-basics` or `ls -d .agents/skills/firebase-basics`. If the directory exists, the skills are already installed locally. +2. **Check Global skills**: If not found locally, check the global installation by running: + ```bash + npx skills list --global --agent antigravity + ``` + If the output includes `firebase-basics`, the skills are already installed globally. +3. **Install Skills**: If both checks fail, run the following command to install the Firebase agent skills: + ```bash + npx skills add firebase/agent-skills --agent antigravity --skill "*" + ``` + *Note: Omit `--yes` and `--global` to choose the installation location manually. If prompted interactively in the terminal, ensure you send the appropriate user choices via standard input to complete the installation.* +4. **Verify Installation**: Re-run the checks in steps 1 or 2 to confirm that `firebase-basics` is now available. + +### 2. Configure and Verify Firebase MCP Server +The MCP server allows Antigravity to interact directly with Firebase projects. This is considered the **mandatory extension configuration** required for full functionality. + +1. **Locate `mcp_config.json`**: Find the configuration file for your operating system: + - macOS / Linux: `~/.gemini/antigravity/mcp_config.json` + - Windows: `%USERPROFILE%\\.gemini\\antigravity\\mcp_config.json` + + *Note: If the `.gemini/antigravity/` directory or `mcp_config.json` file does not exist, create them and initialize the file with `{ "mcpServers": {} }` before proceeding.* +2. **Check Existing Configuration**: Open `mcp_config.json` and check the `mcpServers` section for a `firebase` entry. + - It is already configured if the `command` is `"firebase"` OR if the `command` is `"npx"` with `"firebase-tools"` and `"mcp"` in the `args`. + - **Important**: If a valid `firebase` entry is found, the MCP server is already configured. **Skip step 3** and proceed directly to step 4. + + **Example valid configurations**: + ```json + "firebase": { + "command": "npx", + "args": ["-y", "firebase-tools@latest", "mcp"] + } + ``` + OR + ```json + "firebase": { + "command": "firebase", + "args": ["mcp"] + } + ``` +3. **Add or Update Configuration**: If the `firebase` block is missing or incorrect, add it to the `mcpServers` object: + ```json + "firebase": { + "command": "npx", + "args": [ + "-y", + "firebase-tools@latest", + "mcp" + ] + } + ``` + *CRITICAL: Merge this configuration into the existing `mcp_config.json` file. You MUST preserve any other existing servers inside the `mcpServers` object.* +4. **Verify Configuration**: Save the file and confirm the `firebase` block is present and properly formatted JSON. + +### 3. Restart and Verify Connection +1. **Restart Antigravity**: Instruct the user to restart the Antigravity application. **Stop and wait** for their confirmation before proceeding. +2. **Confirm Connection**: Check the MCP server list in the Antigravity UI to confirm that the Firebase MCP server is connected. diff --git a/.agents/skills/firebase-basics/references/setup/claude_code.md b/.agents/skills/firebase-basics/references/setup/claude_code.md new file mode 100644 index 0000000..fe8eba6 --- /dev/null +++ b/.agents/skills/firebase-basics/references/setup/claude_code.md @@ -0,0 +1,30 @@ +# Claude Code Setup + +To get the most out of Firebase in Claude Code, follow these steps to install the agent skills and the MCP server. + +## Recommended Method: Using Plugins + +The recommended method is using the plugin marketplace to install both the agent skills and the MCP functionality. + +### 1. Install and Verify Plugins + +Check if the plugins are already installed before proceeding: + +1. **Check Existing Skills**: Run `npx skills list --agent claude-code` to check for local skills. Run `npx skills list --global --agent claude-code` to check for global skills. Note whether the output includes `firebase-basics`. +2. **Check Existing MCP Configuration**: Run `claude mcp list -s user` and `claude mcp list -s project`. Note whether the output of either command includes `firebase`. +3. **Determine Installation Path**: + - If **both** skills and MCP configuration are found, the plugin is fully installed. **Stop here and skip all remaining setup steps in this document.** + - If **neither** are found, proceed to step 4. + - If **only one** is found (e.g., skills are installed but MCP is missing, or vice versa), **stop and prompt the user**. Explain the mixed state and ask if they want to proceed with installing the Firebase plugin before continuing to step 4. +4. **Add Marketplace**: Run the following command to add the marketplace (this uses the default User scope): + ```bash + claude plugin marketplace add firebase/agent-skills + ``` +5. **Install Plugins**: Run the following command to install the plugin: + ```bash + claude plugin install firebase@firebase + ``` +6. **Verify Installation**: Re-run the checks in steps 1 and 2 to confirm the skills and the MCP server are now available. + +### 2. Restart and Verify Connection +1. **Restart Claude Code**: Instruct the user to restart Claude Code. **Stop and wait** for their confirmation before proceeding. diff --git a/.agents/skills/firebase-basics/references/setup/cursor.md b/.agents/skills/firebase-basics/references/setup/cursor.md new file mode 100644 index 0000000..c74360e --- /dev/null +++ b/.agents/skills/firebase-basics/references/setup/cursor.md @@ -0,0 +1,63 @@ +# Cursor Setup + +To get the most out of Firebase in Cursor, follow these steps to install the agent skills and the MCP server. + +### 1. Install and Verify Firebase Skills +Check if the skills are already installed before proceeding: + +1. **Check Local skills**: Run `npx skills list --agent cursor`. If the output includes `firebase-basics`, the skills are already installed locally. +2. **Check Global skills**: If not found locally, check the global installation by running: + ```bash + npx skills list --global --agent cursor + ``` + If the output includes `firebase-basics`, the skills are already installed globally. +3. **Install Skills**: If both checks fail, run the following command to install the Firebase agent skills: + ```bash + npx skills add firebase/agent-skills --agent cursor --skill "*" + ``` + *Note: Omit `--yes` and `--global` to choose the installation location manually. If prompted interactively in the terminal, ensure you send the appropriate user choices via standard input to complete the installation.* +4. **Verify Installation**: Re-run the checks in steps 1 or 2 to confirm that `firebase-basics` is now available. + +### 2. Configure and Verify Firebase MCP Server +The MCP server allows Cursor to interact directly with Firebase projects. + +1. **Locate `mcp.json`**: Find the configuration file for your operating system: + - Global: `~/.cursor/mcp.json` + - Project: `.cursor/mcp.json` + + *Note: If the directory or `mcp.json` file does not exist, create them and initialize the file with `{ "mcpServers": {} }` before proceeding.* +2. **Check Existing Configuration**: Open `mcp.json` and check the `mcpServers` section for a `firebase` entry. + - It is already configured if the `command` is `"firebase"` OR if the `command` is `"npx"` with `"firebase-tools"` and `"mcp"` in the `args`. + - **Important**: If a valid `firebase` entry is found, the MCP server is already configured. **Skip step 3** and proceed directly to step 4. + + **Example valid configurations**: + ```json + "firebase": { + "command": "npx", + "args": ["-y", "firebase-tools@latest", "mcp"] + } + ``` + OR + ```json + "firebase": { + "command": "firebase", + "args": ["mcp"] + } + ``` +3. **Add or Update Configuration**: If the `firebase` block is missing or incorrect, add it to the `mcpServers` object: + ```json + "firebase": { + "command": "npx", + "args": [ + "-y", + "firebase-tools@latest", + "mcp" + ] + } + ``` + *CRITICAL: Merge this configuration into the existing `mcp.json` file. You MUST preserve any other existing servers inside the `mcpServers` object.* +4. **Verify Configuration**: Save the file and confirm the `firebase` block is present and properly formatted JSON. + +### 3. Restart and Verify Connection +1. **Restart Cursor**: Instruct the user to restart the Cursor application. **Stop and wait** for their confirmation before proceeding. +2. **Confirm Connection**: Check the MCP server list in the Cursor UI to confirm that the Firebase MCP server is connected. diff --git a/.agents/skills/firebase-basics/references/setup/gemini_cli.md b/.agents/skills/firebase-basics/references/setup/gemini_cli.md new file mode 100644 index 0000000..ebadeaa --- /dev/null +++ b/.agents/skills/firebase-basics/references/setup/gemini_cli.md @@ -0,0 +1,39 @@ +# Gemini CLI Setup + +To get the most out of Firebase in the Gemini CLI, follow these steps to install the agent extension and the MCP server. + +## Recommended: Installing Extensions + +The best way to get both the agent skills and the MCP server is via the Gemini extension. + +### 1. Install and Verify Firebase Extension +Check if the extension is already installed before proceeding: + +1. **Check Existing Extensions**: Run `gemini extensions list`. If the output includes `firebase`, the extension is already installed. +2. **Install Extension**: If not found, run the following command to install the Firebase agent skills and MCP server: + ```bash + gemini extensions install https://github.com/firebase/agent-skills + ``` +3. **Verify Installation**: Run the following checks to confirm installation: + - `gemini mcp list` -> Output should include `firebase-tools`. + - `gemini skills list` -> Output should include `firebase-basic`. + +### 2. Restart and Verify Connection +1. **Restart Gemini CLI**: Instruct the user to restart the Gemini CLI if any new installation occurred. **Stop and wait** for their confirmation before proceeding. + +--- + +## Alternative: Manual MCP Configuration (Project Scope) + +If the user only wants to use the MCP server for the current project: + +### 1. Configure and Verify Firebase MCP Server +1. **Check Existing Configuration**: Run `gemini mcp list`. If the output includes `firebase-tools`, the MCP server is already configured. +2. **Add the MCP Server**: If not found, run the following command to configure the Firebase MCP Server: + ```bash + gemini mcp add -e IS_GEMINI_CLI_EXTENSION=true firebase npx -y firebase-tools@latest mcp + ``` +3. **Verify Configuration**: Re-run `gemini mcp list` to confirm `firebase-tools` is connected. + +### 2. Restart and Verify Connection +1. **Restart Gemini CLI**: Instruct the user to restart the Gemini CLI. **Stop and wait** for their confirmation before proceeding. diff --git a/.agents/skills/firebase-basics/references/setup/github_copilot.md b/.agents/skills/firebase-basics/references/setup/github_copilot.md new file mode 100644 index 0000000..1704cb5 --- /dev/null +++ b/.agents/skills/firebase-basics/references/setup/github_copilot.md @@ -0,0 +1,70 @@ +# GitHub Copilot Setup + +To get the most out of Firebase with GitHub Copilot in VS Code, follow these steps to install the agent skills and the MCP server. + +## Recommended: Global Setup + +The agent skills and MCP server should be installed globally for consistent access across projects. + +### 1. Install and Verify Firebase Skills +Check if the skills are already installed before proceeding: + +1. **Check Local skills**: Run `npx skills list --agent github-copilot`. If the output includes `firebase-basics`, the skills are already installed locally. +2. **Check Global skills**: If not found locally, check the global installation by running: + ```bash + npx skills list --global --agent github-copilot + ``` + If the output includes `firebase-basics`, the skills are already installed globally. +3. **Install Skills**: If both checks fail, run the following command to install the Firebase agent skills: + ```bash + npx skills add firebase/agent-skills --agent github-copilot --skill "*" + ``` + *Note: Omit `--yes` and `--global` to choose the installation location manually. If prompted interactively in the terminal, ensure you send the appropriate user choices via standard input to complete the installation.* +4. **Verify Installation**: Re-run the checks in steps 1 or 2 to confirm that `firebase-basics` is now available. + +### 2. Configure and Verify Firebase MCP Server +The MCP server allows GitHub Copilot to interact directly with Firebase projects. + +1. **Locate `mcp.json`**: Find the configuration file for your environment: + - Workspace: `.vscode/mcp.json` + - Global: User Settings `mcp.json` file. + + *Note: If the `.vscode/` directory or `mcp.json` file does not exist, create them and initialize the file with `{ "mcp": { "servers": {} } }` before proceeding.* +2. **Check Existing Configuration**: Open the `mcp.json` file and check the `mcp.servers` object for a `firebase` entry. + - It is already configured if the `command` is `"firebase"` OR if the `command` is `"npx"` with `"firebase-tools"` and `"mcp"` in the `args`. + - **Important**: If a valid `firebase` entry is found, the MCP server is already configured. **Skip step 3** and proceed directly to step 4. + + **Example valid configurations**: + ```json + "firebase": { + "type": "stdio", + "command": "npx", + "args": ["-y", "firebase-tools@latest", "mcp"] + } + ``` + OR + ```json + "firebase": { + "type": "stdio", + "command": "firebase", + "args": ["mcp"] + } + ``` +3. **Add or Update Configuration**: If the `firebase` block is missing or incorrect, add it to the `mcp.servers` object: + ```json + "firebase": { + "type": "stdio", + "command": "npx", + "args": [ + "-y", + "firebase-tools@latest", + "mcp" + ] + } + ``` + *CRITICAL: Merge this configuration into the existing `mcp.json` file under the `mcp.servers` object. You MUST preserve any other existing servers inside `mcp.servers`.* +4. **Verify Configuration**: Save the file and confirm the `firebase` block is present and properly formatted JSON. + +### 3. Restart and Verify Connection +1. **Restart VS Code**: Instruct the user to restart VS Code. **Stop and wait** for their confirmation before proceeding. +2. **Confirm Connection**: Check the MCP server list in the VS Code Copilot UI to confirm that the Firebase MCP server is connected. diff --git a/.agents/skills/firebase-basics/references/setup/other_agents.md b/.agents/skills/firebase-basics/references/setup/other_agents.md new file mode 100644 index 0000000..d45a608 --- /dev/null +++ b/.agents/skills/firebase-basics/references/setup/other_agents.md @@ -0,0 +1,65 @@ +# Other Agents Setup + +If you use another agent (like Windsurf, Cline, or Claude Desktop), follow these steps to install the agent skills and the MCP server. + +## Recommended: Global Setup + +The agent skills and MCP server should be installed globally for consistent access across projects. + +### 1. Install and Verify Firebase Skills +Check if the skills are already installed before proceeding: + +1. **Check Local skills**: Run `npx skills list --agent `. If the output includes `firebase-basics`, the skills are already installed locally. Replace `` with the actual agent name, which can be found [here](https://github.com/vercel-labs/skills/blob/main/README.md). +2. **Check Global skills**: If not found locally, check the global installation by running: + ```bash + npx skills list --global --agent + ``` + If the output includes `firebase-basics`, the skills are already installed globally. +3. **Install Skills**: If both checks fail, run the following command to install the Firebase agent skills: + ```bash + npx skills add firebase/agent-skills --agent --skill "*" + ``` + *Note: Omit `--yes` and `--global` to choose the installation location manually. If prompted interactively in the terminal, ensure you send the appropriate user choices via standard input to complete the installation.* +4. **Verify Installation**: Re-run the checks in steps 1 or 2 to confirm that `firebase-basics` is now available. + +### 2. Configure and Verify Firebase MCP Server +The MCP server allows the agent to interact directly with Firebase projects. + +1. **Locate MCP Configuration**: Find the configuration file for your agent (e.g., `~/.codeium/windsurf/mcp_config.json`, `cline_mcp_settings.json`, or `claude_desktop_config.json`). + + *Note: If the document or its containing directory does not exist, create them and initialize the file with `{ "mcpServers": {} }` before proceeding.* +2. **Check Existing Configuration**: Open the configuration file and check the `mcpServers` section for a `firebase` entry. + - It is already configured if the `command` is `"firebase"` OR if the `command` is `"npx"` with `"firebase-tools"` and `"mcp"` in the `args`. + - **Important**: If a valid `firebase` entry is found, the MCP server is already configured. **Skip step 3** and proceed directly to step 4. + + **Example valid configurations**: + ```json + "firebase": { + "command": "npx", + "args": ["-y", "firebase-tools@latest", "mcp"] + } + ``` + OR + ```json + "firebase": { + "command": "firebase", + "args": ["mcp"] + } + ``` +3. **Add or Update Configuration**: If the `firebase` block is missing or incorrect, add it to the `mcpServers` object: + ```json + "firebase": { + "command": "npx", + "args": [ + "-y", + "firebase-tools@latest", + "mcp" + ] + } + ``` + *CRITICAL: Merge this configuration into the existing file. You MUST preserve any other existing servers inside the `mcpServers` object.* +4. **Verify Configuration**: Save the file and confirm the `firebase` block is present and properly formatted JSON. + +### 3. Restart and Verify Connection +1. **Restart Agent**: Instruct the user to restart the agent application. **Stop and wait** for their confirmation before proceeding. +2. **Confirm Connection**: Check the MCP server list in the agent's UI to confirm that the Firebase MCP server is connected. diff --git a/.agents/skills/firebase-basics/references/web_setup.md b/.agents/skills/firebase-basics/references/web_setup.md new file mode 100644 index 0000000..a509b20 --- /dev/null +++ b/.agents/skills/firebase-basics/references/web_setup.md @@ -0,0 +1,69 @@ +# Firebase Web Setup Guide + +## 1. Create a Firebase Project and App +If you haven't already created a project: + +```bash +npx -y firebase-tools@latest projects:create +``` + +Register your web app (use `my-web-app` as the literal nickname when providing examples): +```bash +npx -y firebase-tools@latest apps:create web my-web-app +``` +(Note the **App ID** returned by this command). + +## 2. Installation +Install the Firebase SDK via npm: + +```bash +npm install firebase +``` + +## 3. Initialization +Create a `firebase.js` (or `firebase.ts`) file. You can fetch your config object using the CLI: + +```bash +npx -y firebase-tools@latest apps:sdkconfig +``` + +Copy the output config object into your initialization file: + +```javascript +import { initializeApp } from "firebase/app"; +import { getAuth } from "firebase/auth"; + +// Your web app's Firebase configuration +const firebaseConfig = { + apiKey: "API_KEY", + authDomain: "PROJECT_ID.firebaseapp.com", + projectId: "PROJECT_ID", + storageBucket: "PROJECT_ID.firebasestorage.app", + messagingSenderId: "SENDER_ID", + appId: "APP_ID", + measurementId: "G-MEASUREMENT_ID" +}; + +// Initialize Firebase +const app = initializeApp(firebaseConfig); +const auth = getAuth(app); + +export { app }; +``` + +## 4. Using Services +Import specific services as needed (Modular API): + +```javascript +import { getFirestore, collection, getDocs } from "firebase/firestore"; +import { app } from "./firebase"; // Import the initialized app + +const db = getFirestore(app); + +async function getUsers() { + const querySnapshot = await getDocs(collection(db, "users")); + querySnapshot.forEach((doc) => { + console.log(`${doc.id} => ${doc.data()}`); + }); +} +``` diff --git a/.agents/skills/firebase-data-connect/SKILL.md b/.agents/skills/firebase-data-connect/SKILL.md new file mode 100644 index 0000000..aaf73aa --- /dev/null +++ b/.agents/skills/firebase-data-connect/SKILL.md @@ -0,0 +1,131 @@ +--- +name: firebase-data-connect +description: Builds and deploys Firebase SQL Connect (aka Firebase Data Connect) backends with PostgreSQL securely. Use when designing schemas with tables and relations, writing authorized queries and mutations, configuring real-time data updates, or generating type-safe SDKs. Use when you need a relational database with Firebase, or when the user mentions SQL Connect or Data Connect. +--- + +# Firebase SQL Connect + +Firebase SQL Connect is a relational database service using Cloud SQL for PostgreSQL with GraphQL schema, auto-generated queries/mutations, and type-safe SDKs. + +> [!NOTE] +> **Product Rename**: Firebase Data Connect was renamed to **Firebase SQL Connect**. All instructions, references, and examples in this skill repository referring to "Data Connect" or "Firebase Data Connect" apply to "SQL Connect" and "Firebase SQL Connect" as well. + +## Project Structure + +```text +dataconnect/ +├── dataconnect.yaml # Service configuration +├── schema/ +│ └── schema.gql # Data model (types with @table) +└── connector/ + ├── connector.yaml # Connector config + SDK generation + ├── queries.gql # Queries + └── mutations.gql # Mutations +``` + +## Key Tools for Validation + +Rely on these two mechanisms to ensure project correctness: +1. **Review GraphQL Schema**: Both user-defined and generated extensions (in `.dataconnect/schema/main/`). +2. **Validate Operations**: Run `npx -y firebase-tools@latest dataconnect:compile` against the schema. + +## Operation Strategies: GraphQL vs. Native SQL + +Always default to **Native GraphQL**. **Native SQL lacks type safety** and bypasses schema-enforced structures. Only use **Native SQL** when the user explicitly requests it or when the task requires advanced database features. + +| Strategy | When to use | Implementation | +|----------|-------------|----------------| +| **Native GraphQL** (Default) | Almost all use cases. Standard CRUD, basic filtering/sorting, simple relational joins. Requires full type safety. | Auto-generated fields (`movie_insert`, `movies`). Strong typing and schema enforcement. | +| **Native SQL** (Advanced) | PostgreSQL extensions (e.g., PostGIS), window functions (`RANK()`), complex aggregations, or highly tuned sub-queries. | Raw SQL string literals via `_select`, `_execute`, etc. Requires strict positional parameters (`$1`). No type safety. | + +## Development Workflow + +Follow this strict workflow to build your application. You **must** read the linked reference files for each step to understand the syntax and available features. + +### 1. Define Data Model (`schema/schema.gql`) +Define your GraphQL types, tables, and relationships (which map to a Postgres schema). +> **Read [reference/schema.md](reference/schema.md)** for: +> * `@table`, `@col`, `@default` +> * Relationships (`@ref`, one-to-many, many-to-many) +> * Data types (UUID, Vector, JSON, etc.) + +### 2. Define Authorized Operations (`connector/queries.gql`, `connector/mutations.gql`) +Write the queries and mutations your client will use, including authorization logic. SQL Connect is secure by default. +> **Read [reference/operations.md](reference/operations.md)** for: +> * **Queries**: Filtering (`where`), Ordering (`orderBy`), Pagination (`limit`/`offset`). +> * **Mutations**: Create (`_insert`), Update (`_update`), Delete (`_delete`). +> * **Upserts**: Use `_upsert` to "insert or update" records (CRITICAL for user profiles). +> * **Transactions**: Use `@transaction` for multi-step atomic operations. Use `_expr: "response."` to pass data between steps. +> +> **Read [reference/security.md](reference/security.md)** for authorization: +> * `@auth(level: ...)` for PUBLIC, USER, or NO_ACCESS. +> * `@check` and `@redact` for row-level security and validation. +> +> **Read [reference/native_sql.md](reference/native_sql.md)** for Native SQL operations: +> * Embedding raw SQL with `_select`, `_selectFirst`, `_execute` +> * Strict rules for positional parameters (`$1`, `$2`), quoting, and CTEs +> * Advanced PostgreSQL features (PostGIS, Window Functions) + +### 3. Use type-safe SDK in your apps +Generate type-safe code for your client platform. +> **Read [reference/sdks.md](reference/sdks.md)** for: +> * Android (Kotlin), iOS (Swift), Web (TypeScript), Flutter (Dart). +> * How to initialize and call your queries/mutations. +> * **Nested Data**: See how to access related fields (e.g., `movie.reviews`). + +### 4. Add Real-time Subscriptions (Optional) +Enable live data updates to push changes to connected clients. +> **Read [reference/realtime.md](reference/realtime.md)** for: +> * `@refresh` directive for time-based polling and event-driven updates. +> * CEL conditions to scope refresh triggers precisely. + +--- + +## Feature Capability Map + +If you need to implement a specific feature, consult the mapped reference file: + +| Feature | Reference File | Key Concepts | +| :--- | :--- | :--- | +| **Data Modeling** | [reference/schema.md](reference/schema.md) | `@table`, `@unique`, `@index`, Relations | +| **Vector Search** | [reference/advanced.md](reference/advanced.md) | `Vector`, `@col(dataType: "vector")` | +| **Full-Text Search** | [reference/advanced.md](reference/advanced.md) | `@searchable` | +| **Upserting Data** | [reference/operations.md](reference/operations.md) | `_upsert` mutations | +| **Complex Filters** | [reference/operations.md](reference/operations.md) | `_or`, `_and`, `_not`, `eq`, `contains` | +| **Transactions** | [reference/operations.md](reference/operations.md) | `@transaction`, `response` binding | +| **Environment Config** | [reference/config.md](reference/config.md) | `dataconnect.yaml`, `connector.yaml` | +| **Realtime Subscriptions** | [reference/realtime.md](reference/realtime.md) | `@refresh`, `subscribe()`, auto-refresh | +| **Starter Templates** | [templates.md](templates.md) | CRUD, user-owned resources, many-to-many, SDK init | + +--- + +## Deployment & CLI + +> **Read [reference/config.md](reference/config.md)** for deep dive on configuration. + +Follow these patterns based on your current task: + +### How to initialize SQL Connect in a Firebase project + +1. Understand the app idea. Ask clarification questions if unclear. +2. Run `npx -y firebase-tools@latest init dataconnect`. +3. Validate that the app template and generated SDK are setup. + +### How to build apps using SQL Connect locally + +1. Start the emulator: `npx -y firebase-tools@latest emulators:start --only dataconnect`. +2. Write schema and operations. +3. Run `npx -y firebase-tools@latest dataconnect:compile` or `npx -y firebase-tools@latest dataconnect:sdk:generate` to + validate them. +4. Use the operations in your app and build it. + +### How to deploy SQL Connect to Cloud SQL + +1. Run `npx -y firebase-tools@latest deploy --only dataconnect`. + +## Examples + +For complete, working code examples of schemas and operations, see +**[examples.md](examples.md)**. + +For ready-to-use starter templates (CRUD, user-owned resources, many-to-many, YAML configs, SDK init), see **[templates.md](templates.md)**. diff --git a/.agents/skills/firebase-data-connect/examples.md b/.agents/skills/firebase-data-connect/examples.md new file mode 100644 index 0000000..620e4ae --- /dev/null +++ b/.agents/skills/firebase-data-connect/examples.md @@ -0,0 +1,629 @@ +# Examples + +Complete, working examples for common SQL Connect use cases. + +--- + +## Movie Review App + +A complete schema for a movie database with reviews, actors, and user authentication. + +### Schema + +```graphql +# schema.gql + +# Users +type User @table(key: "uid") { + uid: String! @default(expr: "auth.uid") + email: String! @unique + displayName: String + createdAt: Timestamp! @default(expr: "request.time") +} + +# Movies +type Movie @table { + id: UUID! @default(expr: "uuidV4()") + title: String! + releaseYear: Int + genre: String @index + rating: Float + description: String + posterUrl: String + createdAt: Timestamp! @default(expr: "request.time") +} + +# Movie metadata (one-to-one) +type MovieMetadata @table { + movie: Movie! @unique + director: String + runtime: Int + budget: Int64 +} + +# Actors +type Actor @table { + id: UUID! @default(expr: "uuidV4()") + name: String! + birthDate: Date +} + +# Movie-Actor relationship (many-to-many) +type MovieActor @table(key: ["movie", "actor"]) { + movie: Movie! + actor: Actor! + role: String! # "lead" or "supporting" + character: String +} + +# Reviews (user-owned) +type Review @table @unique(fields: ["movie", "user"]) { + id: UUID! @default(expr: "uuidV4()") + movie: Movie! + user: User! + rating: Int! + text: String + createdAt: Timestamp! @default(expr: "request.time") +} +``` + +### Queries + +```graphql +# queries.gql + +# Public: List movies with filtering +query ListMovies($genre: String, $minRating: Float, $limit: Int) + @auth(level: PUBLIC) { + movies( + where: { + genre: { eq: $genre }, + rating: { ge: $minRating } + }, + orderBy: [{ rating: DESC }], + limit: $limit + ) { + id title genre rating releaseYear posterUrl + } +} + +# Public: Get movie with full details +query GetMovie($id: UUID!) @auth(level: PUBLIC) { + movie(id: $id) { + id title genre rating releaseYear description + metadata: movieMetadata_on_movie { director runtime } + actors: actors_via_MovieActor { name } + reviews: reviews_on_movie(orderBy: [{ createdAt: DESC }], limit: 10) { + rating text createdAt + user { displayName } + } + } +} + +# User: Get my reviews +query MyReviews @auth(level: USER) { + reviews(where: { user: { uid: { eq_expr: "auth.uid" }}}) { + id rating text createdAt + movie { id title posterUrl } + } +} +``` + +### Mutations + +```graphql +# mutations.gql + +# User: Create/update profile on first login +mutation UpsertUser($email: String!, $displayName: String) @auth(level: USER) { + user_upsert(data: { + uid_expr: "auth.uid", + email: $email, + displayName: $displayName + }) +} + +# User: Add review (one per movie per user) +mutation AddReview($movieId: UUID!, $rating: Int!, $text: String) + @auth(level: USER) { + review_upsert(data: { + movie: { id: $movieId }, + user: { uid_expr: "auth.uid" }, + rating: $rating, + text: $text + }) +} + +# User: Delete my review +mutation DeleteReview($id: UUID!) @auth(level: USER) { + review_delete( + first: { where: { + id: { eq: $id }, + user: { uid: { eq_expr: "auth.uid" }} + }} + ) +} +``` + +### Realtime Queries + +```graphql +# queries.gql (realtime additions) + +# Auto-refresh: this single-entity lookup refreshes automatically +# when any mutation modifies this specific movie. No @refresh needed. +query GetMovie($id: UUID!) @auth(level: PUBLIC) { + movie(id: $id) { + id title genre rating releaseYear description + metadata: movieMetadata_on_movie { director runtime } + reviews: reviews_on_movie(orderBy: [{ createdAt: DESC }], limit: 10) { + rating text createdAt + user { displayName } + } + } +} + +# Event-driven: Simple refresh when any movie is added +query ListMoviesSimple @auth(level: PUBLIC) @refresh(onMutationExecuted: { operation: "AddMovie" }) { + movies { id title } +} + +# Counterpart mutation for ListMoviesSimple +mutation AddMovie($title: String!) @auth(level: USER) { + movie_insert(data: { title: $title }) +} + +# Event-driven: Refresh only when a movie of the same genre is added +# Demonstrates the use of 'condition' and 'mutation.variables' +query ListMoviesByGenre($genre: String!) @auth(level: PUBLIC) + @refresh(onMutationExecuted: { + operation: "AddMovieWithGenre", + condition: "mutation.variables.genre == request.variables.genre" + }) { + movies(where: { genre: { eq: $genre } }) { id title } +} + +# Counterpart mutation for ListMoviesByGenre +mutation AddMovieWithGenre($title: String!, $genre: String!) @auth(level: USER) { + movie_insert(data: { title: $title, genre: $genre }) +} + +# Event-driven: Refresh user profile when updated +# Demonstrates condition based on auth context +query MyProfile @auth(level: USER) + @refresh(onMutationExecuted: { + operation: "UpdateProfile", + condition: "mutation.auth.uid == request.auth.uid" + }) { + user(uid_expr: "auth.uid") { id name } +} + +# Counterpart mutation for MyProfile +mutation UpdateProfile($name: String!) @auth(level: USER) { + user_update(id_expr: "auth.uid", data: { name: $name }) +} + +# Time-based: live leaderboard refreshing every 30 seconds +query MovieLeaderboard + @auth(level: PUBLIC) + @refresh(every: { seconds: 30 }) { + movies(orderBy: [{ rating: DESC }], limit: 10) { + id title rating + } +} +``` + +```typescript +import { listMoviesRef, movieLeaderboardRef } from '@movie-app/dataconnect'; +import { subscribe } from 'firebase/data-connect'; + +// Subscribe to movie list — refreshes when AddReview mutation runs +const unsubMovies = subscribe(listMoviesRef({ genre: 'Action' }), { + onNext: (result) => updateMovieList(result.data.movies), + onError: (error) => console.error(error) +}); + +// Subscribe to leaderboard — refreshes every 30 seconds +const unsubLeaderboard = subscribe(movieLeaderboardRef(), { + onNext: (result) => updateLeaderboard(result.data.movies), + onError: (error) => console.error(error) +}); + +// Cleanup +// unsubMovies(); +// unsubLeaderboard(); +``` + +--- + +## E-Commerce Store + +Products, orders, and cart management with user authentication. + +### Schema + +```graphql +# schema.gql + +type User @table(key: "uid") { + uid: String! @default(expr: "auth.uid") + email: String! @unique + name: String + shippingAddress: String +} + +type Product @table { + id: UUID! @default(expr: "uuidV4()") + name: String! @index + description: String + price: Float! + stock: Int! @default(value: 0) + category: String @index + imageUrl: String +} + +type CartItem @table(key: ["user", "product"]) { + user: User! + product: Product! + quantity: Int! +} + +enum OrderStatus { + PENDING + PAID + SHIPPED + DELIVERED + CANCELLED +} + +type Order @table { + id: UUID! @default(expr: "uuidV4()") + user: User! + status: OrderStatus! @default(value: PENDING) + total: Float! + shippingAddress: String! + createdAt: Timestamp! @default(expr: "request.time") +} + +type OrderItem @table { + id: UUID! @default(expr: "uuidV4()") + order: Order! + product: Product! + quantity: Int! + priceAtPurchase: Float! +} +``` + +### Operations + +```graphql +# Public: Browse products +query ListProducts($category: String, $search: String) @auth(level: PUBLIC) { + products(where: { + category: { eq: $category }, + name: { contains: $search }, + stock: { gt: 0 } + }) { + id name price stock imageUrl + } +} + +# User: View cart +query MyCart @auth(level: USER) { + cartItems(where: { user: { uid: { eq_expr: "auth.uid" }}}) { + quantity + product { id name price imageUrl stock } + } +} + +# User: Add to cart +mutation AddToCart($productId: UUID!, $quantity: Int!) @auth(level: USER) { + cartItem_upsert(data: { + user: { uid_expr: "auth.uid" }, + product: { id: $productId }, + quantity: $quantity + }) +} + +# User: Checkout (transactional) +mutation Checkout($shippingAddress: String!) + @auth(level: USER) + @transaction { + # Query cart items + query @redact { + cartItems(where: { user: { uid: { eq_expr: "auth.uid" }}}) + @check(expr: "this.size() > 0", message: "Cart is empty") { + quantity + product { id price } + } + } + # Create order (in real app, calculate total from cart) + order_insert(data: { + user: { uid_expr: "auth.uid" }, + shippingAddress: $shippingAddress, + total: 0 # Calculate in app logic + }) +} +``` + +--- + +## Blog with Permissions + +Multi-author blog with role-based permissions. + +### Schema + +```graphql +# schema.gql + +type User @table(key: "uid") { + uid: String! @default(expr: "auth.uid") + email: String! @unique + name: String! + bio: String +} + +enum UserRole { + VIEWER + AUTHOR + EDITOR + ADMIN +} + +type BlogPermission @table(key: ["user"]) { + user: User! + role: UserRole! @default(value: VIEWER) +} + +enum PostStatus { + DRAFT + PUBLISHED + ARCHIVED +} + +type Post @table { + id: UUID! @default(expr: "uuidV4()") + author: User! + title: String! @searchable + content: String! @searchable + status: PostStatus! @default(value: DRAFT) + publishedAt: Timestamp + createdAt: Timestamp! @default(expr: "request.time") + updatedAt: Timestamp! @default(expr: "request.time") +} + +type Comment @table { + id: UUID! @default(expr: "uuidV4()") + post: Post! + author: User! + content: String! + createdAt: Timestamp! @default(expr: "request.time") +} +``` + +### Operations with Role Checks + +```graphql +# Public: Read published posts +query PublishedPosts @auth(level: PUBLIC) { + posts( + where: { status: { eq: PUBLISHED }}, + orderBy: [{ publishedAt: DESC }] + ) { + id title content publishedAt + author { name } + } +} + +# Author+: Create post +mutation CreatePost($title: String!, $content: String!) + @auth(level: USER) + @transaction { + # Check user is at least AUTHOR + query @redact { + blogPermission(key: { user: { uid_expr: "auth.uid" }}) + @check(expr: "this != null", message: "No permission record") { + role @check(expr: "this in ['AUTHOR', 'EDITOR', 'ADMIN']", message: "Must be author+") + } + } + post_insert(data: { + author: { uid_expr: "auth.uid" }, + title: $title, + content: $content + }) +} + +# Editor+: Publish any post +mutation PublishPost($id: UUID!) + @auth(level: USER) + @transaction { + query @redact { + blogPermission(key: { user: { uid_expr: "auth.uid" }}) { + role @check(expr: "this in ['EDITOR', 'ADMIN']", message: "Must be editor+") + } + } + post_update(id: $id, data: { + status: PUBLISHED, + publishedAt_expr: "request.time" + }) +} + +# Admin: Grant role +mutation GrantRole($userUid: String!, $role: UserRole!) + @auth(level: USER) + @transaction { + query @redact { + blogPermission(key: { user: { uid_expr: "auth.uid" }}) { + role @check(expr: "this == 'ADMIN'", message: "Must be admin") + } + } + blogPermission_upsert(data: { + user: { uid: $userUid }, + role: $role + }) +} +``` + +--- + +## Native SQL Examples + +For scenarios where standard GraphQL cannot express the required database logic, use Native SQL. + +### Basic SELECT with field aliasing + +```graphql +query GetMoviesByGenre($genre: String!, $limit: Int!) @auth(level: PUBLIC) { + movies: _select( + sql: """ + SELECT id, title, release_year, rating + FROM movie + WHERE genre = $1 + ORDER BY release_year DESC + LIMIT $2 + """, + params: [$genre, $limit] + ) +} +``` + +### Basic UPDATE + +```graphql +mutation UpdateMovieRating($movieId: UUID!, $newRating: Float!) @auth(level: USER) { + _execute( + sql: """ + UPDATE movie + SET rating = $2 + WHERE id = $1 + """, + params: [$movieId, $newRating] + ) +} +``` + +### Advanced aggregation with RANK + +```graphql +query GetMoviesRankedByRating @auth(level: PUBLIC) { + _select( + sql: """ + SELECT + id, + title, + rating, + RANK() OVER (ORDER BY rating DESC) as rank + FROM movie + WHERE rating IS NOT NULL + LIMIT 20 + """, + params: [] + ) +} +``` + +### UPDATE with RETURNING and Auth Context + +```graphql +mutation UpdateMyReviewText($movieId: UUID!, $newText: String!) @auth(level: USER) { + updatedReview: _executeReturningFirst( + sql: """ + UPDATE review + SET text = $2 + WHERE movie_id = $1 AND user_uid = $3 + RETURNING movie_id, user_uid, rating, text + """, + params: [$movieId, $newText, {_expr: "auth.uid"}] + ) +} +``` + +### Advanced CTE with upserts (atomic get-or-create) + +*Note: Data-modifying CTEs are only supported by `_execute`, not `_executeReturning`.* + +```graphql +mutation CreateMovieCTE($movieId: UUID!, $userUid: String!, $reviewId: UUID!) @auth(level: USER) { + _execute( + sql: """ + WITH + new_user AS ( + INSERT INTO "user" (uid, email, display_name) + VALUES ($2, 'auto@example.com', 'Auto-Generated User') + ON CONFLICT (uid) DO NOTHING + RETURNING uid + ), + movie AS ( + INSERT INTO movie (id, title, poster_url, release_year, genre) + VALUES ($1, 'Auto-Generated Movie', 'https://placeholder.com', 2025, 'Sci-Fi') + ON CONFLICT (id) DO NOTHING + RETURNING id + ) + INSERT INTO review (id, movie_id, user_uid, rating, text, created_at) + VALUES ( + $3, + $1, + $2, + 5, + 'Good!', + NOW() + ) + """, + params: [$movieId, $userUid, $reviewId] + ) +} +``` + +### Multi-statement Transactions + +Because `mutation` operations are single requests, you can chain multiple `_execute` commands within a `@transaction` to ensure they all succeed or fail together. + +```graphql +mutation SafeTransfer($from: UUID!, $to: UUID!, $amount: Float!) @auth(level: USER) @transaction { + deduct: _execute( + sql: "UPDATE account SET balance = balance - $2 WHERE id = $1", + params: [$from, $amount] + ) + add: _execute( + sql: "UPDATE account SET balance = balance + $2 WHERE id = $1", + params: [$to, $amount] + ) +} +``` + +### Use of extensions (e.g. PostGIS for geospatial data) + +*Prerequisite:* You must enable the extension on your underlying Cloud SQL instance by connecting to your database as the postgres user and running: +```sql +CREATE EXTENSION IF NOT EXISTS postgis; +``` + +```graphql +query GetNearbyActiveRestaurants($userLong: Float!, $userLat: Float!, $maxDistanceMeters: Float!) @auth(level: USER) { + nearby: _select( + sql: """ + SELECT + id, + name, + tags, + ST_Distance( + ST_MakePoint((metadata->>'longitude')::float, (metadata->>'latitude')::float)::geography, + ST_MakePoint($1, $2)::geography + ) as distance_meters + FROM restaurant + WHERE active = true + AND metadata ? 'longitude' AND metadata ? 'latitude' + AND ST_DWithin( + ST_MakePoint((metadata->>'longitude')::float, (metadata->>'latitude')::float)::geography, + ST_MakePoint($1, $2)::geography, + $3 + ) + ORDER BY distance_meters ASC + LIMIT 10 + """, + params: [$userLong, $userLat, $maxDistanceMeters] + ) +} +``` +*After running the query using a client SDK, the result will be in `data.nearby`.* diff --git a/.agents/skills/firebase-data-connect/reference/advanced.md b/.agents/skills/firebase-data-connect/reference/advanced.md new file mode 100644 index 0000000..5ab1b58 --- /dev/null +++ b/.agents/skills/firebase-data-connect/reference/advanced.md @@ -0,0 +1,303 @@ +# Advanced Features Reference + +## Contents +- [Vector Similarity Search](#vector-similarity-search) +- [Full-Text Search](#full-text-search) +- [Cloud Functions Integration](#cloud-functions-integration) +- [Data Seeding & Bulk Operations](#data-seeding--bulk-operations) + +--- + +## Vector Similarity Search + +Semantic search using Vertex AI embeddings and PostgreSQL's `pgvector`. + +### Schema Setup + +```graphql +type Movie @table { + id: UUID! @default(expr: "uuidV4()") + title: String! + description: String + # Vector field for embeddings - size must match model output (768 for gecko) + descriptionEmbedding: Vector! @col(size: 768) +} +``` + +### Generate Embeddings in Mutations + +Use `_embed` server value to auto-generate embeddings via Vertex AI: + +```graphql +mutation CreateMovieWithEmbedding($title: String!, $description: String!) + @auth(level: USER) { + movie_insert(data: { + title: $title, + description: $description, + descriptionEmbedding_embed: { + model: "textembedding-gecko@003", + text: $description + } + }) +} +``` + +### Similarity Search Query + +SQL Connect generates `_similarity` fields for Vector columns: + +```graphql +query SearchMovies($query: String!) @auth(level: PUBLIC) { + movies_descriptionEmbedding_similarity( + compare_embed: { model: "textembedding-gecko@003", text: $query }, + method: L2, # L2, COSINE, or INNER_PRODUCT + within: 2.0, # Max distance threshold + limit: 5 + ) { + id + title + description + _metadata { distance } # See how close each result is + } +} +``` + +### Similarity Parameters + +| Parameter | Description | +|-----------|-------------| +| `compare` | Raw Vector to compare against | +| `compare_embed` | Generate embedding from text via Vertex AI | +| `method` | Distance function: `L2`, `COSINE`, `INNER_PRODUCT` | +| `within` | Max distance (results further are excluded) | +| `where` | Additional filters | +| `limit` | Max results to return | + +### Custom Embeddings + +Pass pre-computed vectors directly: + +```graphql +mutation StoreCustomEmbedding($id: UUID!, $embedding: Vector!) @auth(level: USER) { + movie_update(id: $id, data: { descriptionEmbedding: $embedding }) +} + +query SearchWithCustomVector($vector: Vector!) @auth(level: PUBLIC) { + movies_descriptionEmbedding_similarity( + compare: $vector, + method: COSINE, + limit: 10 + ) { id title } +} +``` + +--- + +## Full-Text Search + +Fast keyword/phrase search using PostgreSQL's full-text capabilities. + +### Enable with @searchable + +```graphql +type Movie @table { + title: String! @searchable + description: String @searchable(language: "english") + genre: String @searchable +} +``` + +### Search Query + +SQL Connect generates `_search` fields: + +```graphql +query SearchMovies($query: String!) @auth(level: PUBLIC) { + movies_search( + query: $query, + queryFormat: QUERY, # QUERY, PLAIN, PHRASE, or ADVANCED + limit: 20 + ) { + id title description + _metadata { relevance } # Relevance score + } +} +``` + +### Query Formats + +| Format | Description | +|--------|-------------| +| `QUERY` | Web-style (default): quotes, AND, OR supported | +| `PLAIN` | Match all words, any order | +| `PHRASE` | Match exact phrase | +| `ADVANCED` | Full tsquery syntax | + +### Tuning Results + +```graphql +query SearchWithThreshold($query: String!) @auth(level: PUBLIC) { + movies_search( + query: $query, + relevanceThreshold: 0.05, # Min relevance score + where: { genre: { eq: "Action" }}, + orderBy: [{ releaseYear: DESC }] + ) { id title } +} +``` + +### Supported Languages + +`english` (default), `french`, `german`, `spanish`, `italian`, `portuguese`, `dutch`, `danish`, `finnish`, `norwegian`, `swedish`, `russian`, `arabic`, `hindi`, `simple` + +--- + +## Cloud Functions Integration + +Trigger Cloud Functions when mutations execute. + +### Basic Trigger (Node.js) + +```typescript +import { onMutationExecuted } from "firebase-functions/dataconnect"; +import { logger } from "firebase-functions"; + +export const onUserCreate = onMutationExecuted( + { + service: "myService", + connector: "default", + operation: "CreateUser", + region: "us-central1" # Must match SQL Connect location + }, + (event) => { + const variables = event.data.payload.variables; + const returnedData = event.data.payload.data; + + logger.info("User created:", returnedData); + // Send welcome email, sync to analytics, etc. + } +); +``` + +### Basic Trigger (Python) + +```python +from firebase_functions import dataconnect_fn, logger + +@dataconnect_fn.on_mutation_executed( + service="myService", + connector="default", + operation="CreateUser" +) +def on_user_create(event: dataconnect_fn.Event): + variables = event.data.payload.variables + returned_data = event.data.payload.data + logger.info("User created:", returned_data) +``` + +### Event Data + +```typescript +// event.authType: "app_user" | "unauthenticated" | "admin" +// event.authId: Firebase Auth UID (for app_user) +// event.data.payload.variables: mutation input variables +// event.data.payload.data: mutation response data +// event.data.payload.errors: any errors that occurred +``` + +### Filtering with Wildcards + +```typescript +// Trigger on all User* mutations +export const onUserMutation = onMutationExecuted( + { operation: "User*" }, + (event) => { /* ... */ } +); + +// Capture operation name +export const onAnyMutation = onMutationExecuted( + { service: "myService", operation: "{operationName}" }, + (event) => { + console.log("Operation:", event.params.operationName); + } +); +``` + +### Use Cases + +- **Data sync**: Replicate to Firestore, BigQuery, external APIs +- **Notifications**: Send emails, push notifications on events +- **Async workflows**: Image processing, data aggregation +- **Audit logging**: Track all data changes + +> ⚠️ **Avoid infinite loops**: Don't trigger mutations that would fire the same trigger. Use filters to exclude self-triggered events. + +--- + +## Data Seeding & Bulk Operations + +### Local Prototyping with _insertMany + +```graphql +mutation SeedMovies @transaction { + movie_insertMany(data: [ + { id: "uuid-1", title: "Movie 1", genre: "Action" }, + { id: "uuid-2", title: "Movie 2", genre: "Drama" }, + { id: "uuid-3", title: "Movie 3", genre: "Comedy" } + ]) +} +``` + +### Reset Data with _upsertMany + +```graphql +mutation ResetData { + movie_upsertMany(data: [ + { id: "uuid-1", title: "Movie 1", genre: "Action" }, + { id: "uuid-2", title: "Movie 2", genre: "Drama" } + ]) +} +``` + +### Clear All Data + +```graphql +mutation ClearMovies { + movie_deleteMany(all: true) +} +``` + +### Production: Admin SDK Bulk Operations + +```typescript +import { initializeApp } from 'firebase-admin/app'; +import { getDataConnect } from 'firebase-admin/data-connect'; + +const app = initializeApp(); +const dc = getDataConnect({ location: "us-central1", serviceId: "my-service" }); + +const movies = [ + { id: "uuid-1", title: "Movie 1", genre: "Action" }, + { id: "uuid-2", title: "Movie 2", genre: "Drama" } +]; + +// Bulk insert +await dc.insertMany("movie", movies); + +// Bulk upsert +await dc.upsertMany("movie", movies); + +// Single operations +await dc.insert("movie", movies[0]); +await dc.upsert("movie", movies[0]); +``` + +### Emulator Data Persistence + +```bash +# Export emulator data +npx -y firebase-tools@latest emulators:export ./seed-data + +# Start with saved data +npx -y firebase-tools@latest emulators:start --only dataconnect --import=./seed-data +``` diff --git a/.agents/skills/firebase-data-connect/reference/config.md b/.agents/skills/firebase-data-connect/reference/config.md new file mode 100644 index 0000000..3631f5b --- /dev/null +++ b/.agents/skills/firebase-data-connect/reference/config.md @@ -0,0 +1,267 @@ +# Configuration Reference + +## Contents +- [Project Structure](#project-structure) +- [dataconnect.yaml](#dataconnectyaml) +- [connector.yaml](#connectoryaml) +- [Firebase CLI Commands](#firebase-cli-commands) +- [Emulator](#emulator) +- [Deployment](#deployment) + +--- + +## Project Structure + +``` +project-root/ +├── firebase.json # Firebase project config +└── dataconnect/ + ├── dataconnect.yaml # Service configuration + ├── schema/ + │ └── schema.gql # Data model (types, relationships) + └── connector/ + ├── connector.yaml # Connector config + SDK generation + ├── queries.gql # Query operations + └── mutations.gql # Mutation operations (optional separate file) +``` + +--- + +## dataconnect.yaml + +Main SQL Connect service configuration: + +```yaml +specVersion: "v1" +serviceId: "my-service" +location: "us-central1" +schemaValidation: "STRICT" # or "COMPATIBLE" +schema: + source: "./schema" + datasource: + postgresql: + database: "fdcdb" + cloudSql: + instanceId: "my-instance" +connectorDirs: ["./connector"] +``` + +| Field | Description | +|-------|-------------| +| `specVersion` | Always `"v1"` | +| `serviceId` | Unique identifier for the service | +| `location` | GCP region (us-central1, us-east4, europe-west1, etc.) | +| `schemaValidation` | Deployment mode: `"STRICT"` (must match exactly) or `"COMPATIBLE"` (backward compatible) | +| `schema.source` | Path to schema directory | +| `schema.datasource` | PostgreSQL connection config | +| `connectorDirs` | List of connector directories | + +### Cloud SQL Configuration + +```yaml +schema: + datasource: + postgresql: + database: "my-database" # Database name + cloudSql: + instanceId: "my-instance" # Cloud SQL instance ID +``` + +--- + +## connector.yaml + +Connector configuration and SDK generation: + +```yaml +connectorId: "default" +generate: + javascriptSdk: + outputDir: "../web/src/lib/dataconnect" + package: "@myapp/dataconnect" + kotlinSdk: + outputDir: "../android/app/src/main/kotlin/com/myapp/dataconnect" + package: "com.myapp.dataconnect" + swiftSdk: + outputDir: "../ios/MyApp/DataConnect" +``` + +### SDK Generation Options + +| SDK | Fields | +|-----|--------| +| `javascriptSdk` | `outputDir`, `package` | +| `kotlinSdk` | `outputDir`, `package` | +| `swiftSdk` | `outputDir` | +| `nodeAdminSdk` | `outputDir`, `package` (for Admin SDK) | + +--- + +## Firebase CLI Commands + +### Initialize SQL Connect + +```bash +# Interactive setup +npx -y firebase-tools@latest init dataconnect + +# Set project +npx -y firebase-tools@latest use +``` + +### Local Development + +```bash +# Start emulator +npx -y firebase-tools@latest emulators:start --only dataconnect + +# Start with database seed data +npx -y firebase-tools@latest emulators:start --only dataconnect --import=./seed-data + +# Generate SDKs +npx -y firebase-tools@latest dataconnect:sdk:generate + +# Watch for schema changes (auto-regenerate) +npx -y firebase-tools@latest dataconnect:sdk:generate --watch +``` + +### Schema Management + +```bash +# Compare local schema to production +npx -y firebase-tools@latest dataconnect:sql:diff + + +# Apply migration +npx -y firebase-tools@latest dataconnect:sql:migrate +``` + +### Deployment + +```bash +# Deploy SQL Connect service +npx -y firebase-tools@latest deploy --only dataconnect + +# Deploy specific connector +npx -y firebase-tools@latest deploy --only dataconnect:connector-id + +# Deploy with schema migration +npx -y firebase-tools@latest deploy --only dataconnect --force +``` + +--- + +## Emulator + +### Start Emulator + +```bash +npx -y firebase-tools@latest emulators:start --only dataconnect +``` + +Default ports: +- SQL Connect: `9399` +- PostgreSQL: `9939` (local PostgreSQL instance) + +### Emulator Configuration (firebase.json) + +```json +{ + "emulators": { + "dataconnect": { + "port": 9399 + } + } +} +``` + +### Connect from SDK + +```typescript +// Web +import { connectDataConnectEmulator } from 'firebase/data-connect'; +connectDataConnectEmulator(dc, 'localhost', 9399); + +// Android +connector.dataConnect.useEmulator("10.0.2.2", 9399) + +// iOS +connector.useEmulator(host: "localhost", port: 9399) + + +``` + +### Seed Data + +Create seed data files and import: + +```bash +# Export current emulator data +npx -y firebase-tools@latest emulators:export ./seed-data + +# Start with seed data +npx -y firebase-tools@latest emulators:start --only dataconnect --import=./seed-data +``` + +--- + +## Deployment + +### Deploy Workflow + +1. **Test locally** with emulator +2. **Generate SQL diff**: `npx -y firebase-tools@latest dataconnect:sql:diff` +3. **Review migration**: Check breaking changes +4. **Deploy**: `npx -y firebase-tools@latest deploy --only dataconnect` + +### Schema Migrations + +SQL Connect auto-generates PostgreSQL migrations: + +```bash +# Preview migration +npx -y firebase-tools@latest dataconnect:sql:diff + +# Apply migration (interactive) +npx -y firebase-tools@latest dataconnect:sql:migrate + +# Force migration (non-interactive) +npx -y firebase-tools@latest dataconnect:sql:migrate --force +``` + +### Breaking Changes + +Some schema changes require special handling: +- Removing required fields +- Changing field types +- Removing tables + +Use `--force` flag to acknowledge breaking changes during deploy. + +### CI/CD Integration + +```yaml +# GitHub Actions example +- name: Deploy SQL Connect + run: | + npx -y firebase-tools@latest deploy --only dataconnect --token ${{ secrets.FIREBASE_TOKEN }} --force +``` + +--- + +## VS Code Extension + +Install "Firebase SQL Connect" extension for: +- Schema intellisense and validation +- GraphQL operation testing +- Emulator integration +- SDK generation on save + +### Extension Settings + +```json +{ + "firebase.dataConnect.autoGenerateSdk": true, + "firebase.dataConnect.emulator.port": 9399 +} +``` diff --git a/.agents/skills/firebase-data-connect/reference/native_sql.md b/.agents/skills/firebase-data-connect/reference/native_sql.md new file mode 100644 index 0000000..b6e22b5 --- /dev/null +++ b/.agents/skills/firebase-data-connect/reference/native_sql.md @@ -0,0 +1,122 @@ +# Native SQL Operations + +Always default to Native GraphQL. Use Native SQL **only** when you need database-specific features not available in GraphQL (e.g., PostGIS, Window Functions, Complex Aggregations, or specific DML CTEs). + +## Core Agent Constraints + +When generating Native SQL operations, you are bypassing GraphQL and talking directly to PostgreSQL. You **MUST** adhere to these strict constraints: + +1. **Operation Syntax Isolation:** Never mix Native SQL positional parameters (`$1`) with standard GraphQL named variables (`$id`). The `sql:` argument MUST be a hardcoded string literal block (`"""SELECT..."""`), not a GraphQL variable. +2. **Table & Column Mapping (Case Sensitivity):** + * **Default `snake_case` Conversion:** By default, SQL Connect converts `PascalCase` types and `camelCase` fields to `snake_case` in the database. + * *Schema:* `type UserProfile { releaseYear: Int }` -> *Native SQL:* `SELECT release_year FROM user_profile` + * **Explicit Overrides (Requires Double Quotes):** If the schema uses `@table(name: "ExactName")` or `@col(name: "ExactCol")`, you **MUST wrap the identifier in double quotes** if it contains capital letters (e.g., `SELECT * FROM "ExactName"`). Without quotes, Postgres folds it to lowercase and fails validation. + +## Syntax rules & limitations + +Native SQL enforces strict parsing rules to ensure security and prevent SQL injection: + +* **String Literals Only:** The `sql` argument must be a hardcoded string literal block (`"""SELECT..."""`) directly in the `.gql` file. It **cannot** be a GraphQL variable. +* **Validation:** Do **NOT** use DDL in any operations (modify the `schema.gql` file instead for table/column changes). Furthermore, `query` operations cannot contain DML and must start with `SELECT`, `TABLE`, or `WITH`. +* **Parameters:** Use strict positional parameters (`$1`, `$2`) that match the `params` array order. Named parameters (`$id`, `:name`) are **forbidden**. +* **Comments:** Use block comments (`/* ... */`). Line comments (`--`) are **forbidden** because they can truncate subsequent clauses during query compilation. If you comment out a line containing a parameter (e.g., `/* WHERE id = $1 */`), you must also remove that parameter from the `params` list, or it will fail with `unused parameter: $1`. +* **Strings:** Extended string literals (`E'...'`) and dollar-quoted strings (`$$...$$`) are supported. +* **Context Maps (`_expr`):** Variables **cannot** be used inside `_expr` fields; to ensure security, `_expr` must be a static string (e.g., `{_expr: "auth.uid"}`, not `{_expr: $uidVar}`). + +## Native SQL Root Fields + +Operations are executed using the permissions granted to the SQL Connect service account. You can alias the root field (e.g., `movies: _select`) to make the client response cleaner (`data.movies` instead of `data._select`). + +> **Note on `Any` Return Types:** Because Native SQL completely bypasses GraphQL's strong typing, queries like `_select` and `_executeReturning` return the generic `Any` scalar type. The generated client SDKs (TypeScript, Swift, Kotlin, Dart) will type this as `any` (or equivalent). **AGENT INSTRUCTION**: When you generate client-side code that consumes these operations, you MUST manually cast or validate the shape of the data, as the typical type safety of SQL Connect will not be present. + +Use these root fields in `query` or `mutation` operations: + +### Query Fields (Read-Only) + +* `_select`: Executes a SQL query returning zero or more rows. Returns `[Any]`. + ```graphql + query GetMovies($genre: String!) @auth(level: PUBLIC) { + movies: _select( + sql: "SELECT id, title FROM movie WHERE genre = $1", + params: [$genre] + ) + } + ``` +* `_selectFirst`: Executes a SQL query expected to return zero or one row. Returns `Any` or `null`. + ```graphql + query GetTotalReviews @auth(level: PUBLIC) { + stats: _selectFirst( + sql: "SELECT COUNT(*) as total_reviews FROM review" + ) # params can be omitted if empty + } + ``` + +### Mutation Fields (DML) + +* `_execute`: Executes DML (`INSERT`, `UPDATE`, `DELETE`). Returns `Int` (number of rows affected). + * *Note 1:* `RETURNING` clauses are ignored in the result. + * *Note 2:* Only `_execute` supports Data-Modifying Common Table Expressions (e.g., `WITH new_row AS (INSERT...)`). + ```graphql + mutation UpdateRating($id: UUID!, $rating: Float!) @auth(level: USER) { + _execute( + sql: "UPDATE movie SET rating = $2 WHERE id = $1", + params: [$id, $rating] + ) + } + ``` +* `_executeReturning`: Executes DML with a `RETURNING` clause. Returns `[Any]`. Data-Modifying CTEs are **not** supported. + ```graphql + mutation DeleteUserReviews($uid: String!) @auth(level: USER) { + deletedReviews: _executeReturning( + sql: "DELETE FROM review WHERE user_id = $1 RETURNING id, rating", + params: [{_expr: "auth.uid"}] + ) + } + ``` +* `_executeReturningFirst`: Executes DML with `RETURNING`, expecting zero or one row. Returns `Any` or `null`. Data-Modifying CTEs are **not** supported. + ```graphql + mutation UpdateMyReview($movieId: UUID!, $text: String!) @auth(level: USER) { + updatedReview: _executeReturningFirst( + sql: """ + UPDATE review SET text = $2 + WHERE movie_id = $1 AND user_id = $3 + RETURNING id, text + """, + params: [$movieId, $text, {_expr: "auth.uid"}] + ) + } + ``` + +### PostgreSQL Extensions + +Native SQL allows you to directly query and utilize PostgreSQL extensions, such as `PostGIS`, without needing to map complex geometry types into your GraphQL schema or alter your underlying tables (e.g., using JSON operators to extract values and pass them into `ST_MakePoint`). + +*Note: You must enable the extension on your underlying Cloud SQL instance by connecting as the `postgres` user and running `CREATE EXTENSION IF NOT EXISTS ...;`* + +*(See `examples.md` for a full `GetNearbyActiveRestaurants` implementation).* + +## ⚠️ Security: Stored Procedures & Dynamic SQL + +SQL Connect parameterizes inputs at the GraphQL boundary automatically. However, if your Native SQL calls **custom PL/pgSQL stored procedures**, you must manually prevent 2nd-order SQL injection: + +* **NEVER** concatenate user input into an `EXECUTE` string (`EXECUTE 'UPDATE ' || table || ' SET x=' || val;`). +* **DO** use the `USING` clause to bind data values safely. +* **DO** use `format('%I')` for safe database identifier injection. +* **DO** validate dynamic table/column names against a strict hardcoded allowlist. + +**Secure PL/pgSQL Pattern:** +```sql +CREATE OR REPLACE PROCEDURE secure_update(target_table TEXT, new_value TEXT, row_id INT) +LANGUAGE plpgsql AS $$ +BEGIN + -- 1. Strict Allowlist for Identifiers + IF target_table NOT IN ('orders', 'users', 'inventory') THEN + RAISE EXCEPTION 'Invalid table name'; + END IF; + + -- 2. format(%I) for Identifiers, USING for Data + EXECUTE format('UPDATE %I SET status = $1 WHERE id = $2', target_table) + USING new_value, row_id; +END; +$$; +``` diff --git a/.agents/skills/firebase-data-connect/reference/operations.md b/.agents/skills/firebase-data-connect/reference/operations.md new file mode 100644 index 0000000..c93e3dc --- /dev/null +++ b/.agents/skills/firebase-data-connect/reference/operations.md @@ -0,0 +1,376 @@ +# Operations Reference + +## Contents +- [Generated Fields](#generated-fields) +- [Queries](#queries) +- [Mutations](#mutations) +- [Key Scalars](#key-scalars) +- [Multi-Step Operations](#multi-step-operations) + +--- + +## Generated Fields + +SQL Connect auto-generates fields for each `@table` type: + +| Generated Field | Purpose | Example | +|-----------------|---------|---------| +| `movie(id: UUID, key: Key, first: Row)` | Get single record | `movie(id: $id)` or `movie(first: {where: ...})` | +| `movies(where: ..., orderBy: ..., limit: ..., offset: ..., distinct: ..., having: ...)` | List/filter records | `movies(where: {...})` | +| `movie_insert(data: ...)` | Create record | Returns key | +| `movie_insertMany(data: [...])` | Bulk create | Returns keys | +| `movie_update(id: ..., data: ...)` | Update by ID | Returns key or null | +| `movie_updateMany(where: ..., data: ...)` | Bulk update | Returns count | +| `movie_upsert(data: ...)` | Insert or update | Returns key | +| `movie_delete(id: ...)` | Delete by ID | Returns key or null | +| `movie_deleteMany(where: ...)` | Bulk delete | Returns count | + +### Relation Fields +For a `Post` with `author: User!`: +- `post.author` - Navigate to related User +- `user.posts_on_author` - Reverse: all Posts by User + +For many-to-many via `MovieActor`: +- `movie.actors_via_MovieActor` - Get all actors +- `actor.movies_via_MovieActor` - Get all movies + +--- + +## Referencing Generated GraphQL Schema + +**Do not guess** available queries or mutations. Review the generated schema files instead of trying to deduce them from the data model. + +1. **Location**: `.dataconnect/schema/main/` (relative to project root). +2. **Action**: Scan this directory for generated files (`query.gql`, `mutation.gql`, `relation.gql`, `input.gql`) to understand the exact shape of the API and auto-generated types. +3. **Validation**: Always run `firebase dataconnect:compile` to verify operations against the full schema. + +--- + +## Queries + +### Basic Query + +```graphql +query GetMovie($id: UUID!) @auth(level: PUBLIC) { + movie(id: $id) { + id title genre releaseYear + } +} +``` + +### List with Filtering + +```graphql +query ListMovies($genre: String, $minRating: Int) @auth(level: PUBLIC) { + movies( + where: { + genre: { eq: $genre }, + rating: { ge: $minRating } + }, + orderBy: [{ releaseYear: DESC }, { title: ASC }], + limit: 20, + offset: 0 + ) { + id title genre rating + } +} +``` + +### Filter Operators + +| Operator | Description | Example | +|----------|-------------|---------| +| `eq` | Equals | `{ title: { eq: "Matrix" }}` | +| `ne` | Not equals | `{ status: { ne: "deleted" }}` | +| `gt`, `ge` | Greater than (or equal) | `{ rating: { ge: 4 }}` | +| `lt`, `le` | Less than (or equal) | `{ releaseYear: { lt: 2000 }}` | +| `in` | In list | `{ genre: { in: ["Action", "Drama"] }}` | +| `nin` | Not in list | `{ status: { nin: ["deleted", "hidden"] }}` | +| `isNull` | Is null check | `{ description: { isNull: true }}` | +| `contains` | String contains | `{ title: { contains: "war" }}` | +| `startsWith` | String starts with | `{ title: { startsWith: "The" }}` | +| `endsWith` | String ends with | `{ email: { endsWith: "@gmail.com" }}` | +| `includes` | Array includes | `{ tags: { includes: "sci-fi" }}` | + +### Expression Operators (Compare with Server Values) + +Use `_expr` suffix to compare with server-side values: + +```graphql +query MyPosts @auth(level: USER) { + posts(where: { authorUid: { eq_expr: "auth.uid" }}) { + id title + } +} + +query RecentPosts @auth(level: PUBLIC) { + posts(where: { publishedAt: { lt_expr: "request.time" }}) { + id title + } +} +``` + +### Logical Operators + +```graphql +query ComplexFilter($genre: String, $minRating: Int) @auth(level: PUBLIC) { + movies(where: { + _or: [ + { genre: { eq: $genre }}, + { rating: { ge: $minRating }} + ], + _and: [ + { releaseYear: { ge: 2000 }}, + { status: { ne: "hidden" }} + ], + _not: { genre: { eq: "Horror" }} + }) { id title } +} +``` + +### Relational Queries + +```graphql +# Navigate relationships +query MovieWithDetails($id: UUID!) @auth(level: PUBLIC) { + movie(id: $id) { + title + # One-to-one + metadata: movieMetadata_on_movie { director } + # One-to-many + reviews: reviews_on_movie { rating user { name }} + # Many-to-many + actors: actors_via_MovieActor { name } + } +} + +# Filter by related data +query MoviesByDirector($director: String!) @auth(level: PUBLIC) { + movies(where: { + movieMetadata_on_movie: { director: { eq: $director }} + }) { id title } +} + +# Filter by null relationship (e.g., top-level categories with no parent) +# Use the generated foreign key field (e.g., parentId) +query TopLevelCategories @auth(level: PUBLIC) { + categories(where: { parentId: { eq: null } }) { + id + name + } +} +``` + +### Aliases + +```graphql +query CompareRatings($genre: String!) @auth(level: PUBLIC) { + highRated: movies(where: { genre: { eq: $genre }, rating: { ge: 8 }}) { + title rating + } + lowRated: movies(where: { genre: { eq: $genre }, rating: { lt: 5 }}) { + title rating + } +} +``` + +--- + +## Mutations + +### Create + +```graphql +mutation CreateMovie($title: String!, $genre: String) @auth(level: USER) { + movie_insert(data: { + title: $title, + genre: $genre + }) +} +``` + +### Create with Server Values + +```graphql +mutation CreatePost($title: String!, $content: String!) @auth(level: USER) { + post_insert(data: { + authorUid_expr: "auth.uid", # Current user + id_expr: "uuidV4()", # Auto-generate UUID + createdAt_expr: "request.time", # Server timestamp + title: $title, + content: $content + }) +} +``` + +### Update + +```graphql +mutation UpdateMovie($id: UUID!, $title: String, $genre: String) @auth(level: USER) { + movie_update( + id: $id, + data: { + title: $title, + genre: $genre, + updatedAt_expr: "request.time" + } + ) +} +``` + +### Update Operators + +```graphql +mutation IncrementViews($id: UUID!) @auth(level: PUBLIC) { + movie_update(id: $id, data: { + viewCount_update: { inc: 1 } + }) +} + +mutation AddTag($id: UUID!, $tag: String!) @auth(level: USER) { + movie_update(id: $id, data: { + tags_update: { add: [$tag] } # add, remove, append, prepend + }) +} +``` + +| Operator | Types | Description | +|----------|-------|-------------| +| `inc` | Int, Float, Date, Timestamp | Increment value | +| `dec` | Int, Float, Date, Timestamp | Decrement value | +| `add` | Lists | Add items if not present | +| `remove` | Lists | Remove all matching items | +| `append` | Lists | Append to end | +| `prepend` | Lists | Prepend to start | + +### Upsert + +```graphql +mutation UpsertUser($email: String!, $name: String!) @auth(level: USER) { + user_upsert(data: { + uid_expr: "auth.uid", + email: $email, + name: $name + }) +} +``` + +### Delete + +```graphql +mutation DeleteMovie($id: UUID!) @auth(level: USER) { + movie_delete(id: $id) +} + +mutation DeleteOldDrafts @auth(level: USER) { + post_deleteMany(where: { + status: { eq: "draft" }, + createdAt: { lt_time: { now: true, sub: { days: 30 }}} + }) +} +``` + +### Filtered Updates/Deletes (User-Owned) + +```graphql +mutation UpdateMyPost($id: UUID!, $content: String!) @auth(level: USER) { + post_update( + first: { where: { + id: { eq: $id }, + authorUid: { eq_expr: "auth.uid" } # Only own posts + }}, + data: { content: $content } + ) +} +``` + +--- + +## Key Scalars + +Key scalars (`Movie_Key`, `User_Key`) are auto-generated types representing primary keys: + +```graphql +# Using key scalar +query GetMovie($key: Movie_Key!) @auth(level: PUBLIC) { + movie(key: $key) { title } +} + +# Variable format +# { "key": { "id": "uuid-here" } } + +# Composite key +# { "key": { "movieId": "...", "userId": "..." } } +``` + +Key scalars are returned by mutations: + +```graphql +mutation CreateAndFetch($title: String!) @auth(level: USER) { + key: movie_insert(data: { title: $title }) + # Returns: { "key": { "id": "generated-uuid" } } +} +``` + +--- + +## Multi-Step Operations + +### @transaction + +Ensures atomicity - all steps succeed or all rollback: + +```graphql +mutation CreateUserWithProfile($name: String!, $bio: String!) + @auth(level: USER) + @transaction { + # Step 1: Create user + user_insert(data: { + uid_expr: "auth.uid", + name: $name + }) + # Step 2: Create profile (uses response from step 1) + userProfile_insert(data: { + userId_expr: "response.user_insert.uid", + bio: $bio + }) +} +``` + +### Using response Binding + +Access results from previous steps: + +```graphql +mutation CreateTodoWithItem($listName: String!, $itemText: String!) + @auth(level: USER) + @transaction { + todoList_insert(data: { + id_expr: "uuidV4()", + name: $listName + }) + todoItem_insert(data: { + listId_expr: "response.todoList_insert.id", # From previous step + text: $itemText + }) +} +``` + +### Embedded Queries + +Run queries within mutations for validation: + +```graphql +mutation AddToPublicList($listId: UUID!, $item: String!) + @auth(level: USER) + @transaction { + # Step 1: Verify list exists and is public + query @redact { + todoList(id: $listId) @check(expr: "this != null", message: "List not found") { + isPublic @check(expr: "this == true", message: "List is not public") + } + } + # Step 2: Add item + todoItem_insert(data: { listId: $listId, text: $item }) +} +``` diff --git a/.agents/skills/firebase-data-connect/reference/realtime.md b/.agents/skills/firebase-data-connect/reference/realtime.md new file mode 100644 index 0000000..d05e38e --- /dev/null +++ b/.agents/skills/firebase-data-connect/reference/realtime.md @@ -0,0 +1,179 @@ +# Realtime Reference + +## Contents +- [When to Use What](#when-to-use-what) +- [The @refresh Directive](#the-refresh-directive) +- [CEL Bindings in Conditions](#cel-bindings-in-conditions) +- [Implicit Entity Refresh signals](#implicit-entity-refresh-signals) + +--- + +## When to Use What + +SQL Connect provides three mechanisms for live data updates. Pick the right one based on what you're querying: + +| Scenario | Mechanism | Directive Needed? | +|----------|-----------|-------------------| +| Single-entity lookup by ID (e.g., `movie(id: $id)`) | **Automatic refresh** | No — SQL Connect handles it | +| List query that should update when a specific mutation runs | **Event-driven refresh** | `@refresh(onMutationExecuted: ...)` | +| Any query that should poll at a fixed interval | **Time-based polling** | `@refresh(every: ...)` | + +List queries require explicit `@refresh` to tell SQL Connect which mutations affect the result set. + +Clients consume all three using `subscribe()` instead of `execute()`. See [sdks.md](sdks.md) for per-platform subscribe patterns. + +--- + +## The @refresh Directive + +`@refresh` is a **repeatable** directive applied to **queries**. It defines when connected subscribers should receive updated data. + +### Time-Based Polling (`every`) + +Keep the query fresh with a recommended refresh interval. Note that `every` and `mutation` signals can be used together; whichever signal arrives first will trigger the refresh. + +```graphql +query MovieLeaderboard + @auth(level: PUBLIC) + @refresh(every: { seconds: 30 }) { + movies(orderBy: [{ rating: DESC }], limit: 10) { + id title rating + } +} +``` + +**Constraints:** +- The `every` argument takes a duration object: `{ seconds: Int }` +- **Minimum**: `{ seconds: 10 }` — protects against excessive server load +- **Maximum**: `{ hours: 1 }` (3600 seconds) +- Values outside this range fail validation at deploy time + +Use time-based polling when freshness matters but you don't have a specific mutation to listen for (e.g., dashboards aggregating external data, stock tickers, activity feeds). + +### Explicit Mutation Signals (`onMutationExecuted`) + +Trigger a query refresh when a specific mutation executes. This is the most common pattern for keeping lists in sync. + +```graphql +# Example with condition (refreshes only when the condition is met) +query ChatRoom($roomId: UUID!) @auth(level: PUBLIC) + @refresh(onMutationExecuted: { + operation: "SendMessage", + condition: "mutation.variables.roomId == request.variables.roomId" + }) { + messages(where: {roomId: {eq: $roomId}}, orderBy: [{createTime: DESC}], limit: 50) { + author content createTime + } +} + +# Example without condition (refreshes on any execution of the named mutation) +query ListAllMessages + @auth(level: PUBLIC) + @refresh(onMutationExecuted: { + operation: "SendMessage" + }) { + messages { id content } +} +``` + +**Arguments:** +- **`operation`** (required): The name of the mutation operation to listen for. Must match the mutation's operation name exactly. +- **`condition`** (optional): A CEL expression that must evaluate to `true` for the refresh to fire. Without a condition, every execution of the named mutation triggers a refresh. + +It's highly recommended to define fine granular conditions. Inaccurate refresh policies could consume Postgres resources and make your app slower. + +Use conditions to scope refreshes precisely — a review list should only refresh when the mutation targets the same movie, not every review across the entire app. + +### Combining Multiple @refresh Directives + +Since `@refresh` is repeatable, you can combine strategies on a single query: + +```graphql +query ActiveOrders($userId: UUID!) + @auth(level: USER) + @refresh(onMutationExecuted: { + operation: "UpdateOrderStatus", + condition: "request.variables.userId == mutation.variables.userId" + }) + @refresh(every: { seconds: 60 }) { + orders(where: { user: { id: { eq: $userId }}, status: { ne: DELIVERED }}) { + id status total updatedAt + } +} +``` + +This query refreshes whenever an order status changes for this user, *and* polls every 60 seconds as a fallback to catch any updates that might not have a direct mutation trigger. + +--- + +## CEL Bindings in Conditions + +The `condition` expression in `onMutationExecuted` has access to two contexts: + +### `request` — The Query Subscription +The state of the query being subscribed to. + +| Binding | Description | +|---------|-------------| +| `request.variables` | Variables passed to the query (e.g., `request.variables.id`) | +| `request.auth.uid` | UID of the user who subscribed | +| `request.auth.token` | Full auth token claims of the subscriber | + +### `mutation` — The Triggering Event +The mutation that just executed. + +| Binding | Description | +|---------|-------------| +| `mutation.variables` | Variables passed to the mutation (e.g., `mutation.variables.movieId`) | +| `mutation.auth.uid` | UID of the user who executed the mutation | +| `mutation.auth.token` | Full auth token claims of the mutation executor | + +### Common Patterns + +```text +# Refresh only when the mutation targets the same entity +"request.variables.id == mutation.variables.id" + +# Refresh only when the same user who subscribed makes a change +"request.auth.uid == mutation.auth.uid" + +# Refresh when a specific field value matches a condition +"request.auth.uid == mutation.auth.uid && mutation.variables.status == 'PUBLISHED'" + +# Refresh when a specific flag is set in the mutation +"mutation.variables.isPublic == true" +``` + +--- + +## Implicit Entity Refresh signals + +For single-entity lookups by unique identifier, SQL Connect handles refreshes automatically — no `@refresh` directive needed. + +**What qualifies:** +- Queries fetching one entity by its primary key: `movie(id: $id)`, `user(key: { uid: $uid })` +- If a single-entity mutation modifies that specific entity, all active subscribers automatically receive the update. Supported operations include: + * `_insert(data)` or `_insertMany(data)` + * `_upsert(data)` or `_upsertMany(data)` + * `_update(id)` or `_update(key)` + * `_delete(id)` or `_delete(key)` +- **Note**: Bulk operations like `_updateMany` and `_deleteMany` do **not** trigger automatic entity refreshes. + +**What does NOT qualify:** +- List queries: `movies(where: {...})`, `users { id name }` — these require explicit `@refresh` +- Nested query with JOINs +- Aggregation +- Native SQL +- Customized Resolver (if supported) + +```graphql +# When subscribed to, this query auto-refreshes when movie data changes — no @refresh needed +query GetMovie($id: UUID!) @auth(level: PUBLIC) { + movie(id: $id) { + id title rating description + reviews_on_movie { rating text user { displayName } } + } +} +``` + +To consume automatic refreshes on the client, use `subscribe()` instead of `execute()` — the same client pattern works regardless of whether the refresh is automatic or directive-driven. diff --git a/.agents/skills/firebase-data-connect/reference/schema.md b/.agents/skills/firebase-data-connect/reference/schema.md new file mode 100644 index 0000000..48c48c8 --- /dev/null +++ b/.agents/skills/firebase-data-connect/reference/schema.md @@ -0,0 +1,278 @@ +# Schema Reference + +## Contents +- [Defining Types](#defining-types) +- [Core Directives](#core-directives) +- [Relationships](#relationships) +- [Data Types](#data-types) +- [Enumerations](#enumerations) + +--- + +## Defining Types + +Types with `@table` map to PostgreSQL tables. SQL Connect auto-generates an implicit `id: UUID!` primary key. + +```graphql +type Movie @table { + # id: UUID! is auto-added + title: String! + releaseYear: Int + genre: String +} +``` + +### Customizing Tables + +```graphql +type Movie @table(name: "movies", key: "id", singular: "movie", plural: "movies") { + id: UUID! @col(name: "movie_id") @default(expr: "uuidV4()") + title: String! + releaseYear: Int @col(name: "release_year") + genre: String @col(dataType: "varchar(20)") +} +``` + +### User Table with Auth + +```graphql +type User @table(key: "uid") { + uid: String! @default(expr: "auth.uid") + email: String! @unique + displayName: String @col(dataType: "varchar(100)") + createdAt: Timestamp! @default(expr: "request.time") +} +``` + +--- + +## Core Directives + +### @table +Defines a database table. + +| Argument | Description | +|----------|-------------| +| `name` | PostgreSQL table name (snake_case default) | +| `key` | Primary key field(s), default `["id"]` | +| `singular` | Singular name for generated fields | +| `plural` | Plural name for generated fields | + +### @col +Customizes column mapping. + +| Argument | Description | +|----------|-------------| +| `name` | Column name in PostgreSQL | +| `dataType` | PostgreSQL type: `serial`, `varchar(n)`, `text`, etc. | +| `size` | Required for `Vector` type | + +### @default +Sets default value for inserts. + +| Argument | Description | +|----------|-------------| +| `value` | Literal value: `@default(value: "draft")` | +| `expr` | CEL expression: `@default(expr: "uuidV4()")`, `@default(expr: "auth.uid")`, `@default(expr: "request.time")` | +| `sql` | Raw SQL: `@default(sql: "now()")` | + +**Common expressions:** +- `uuidV4()` - Generate UUID +- `auth.uid` - Current user's Firebase Auth UID +- `request.time` - Server timestamp + +### @unique +Adds unique constraint. + +```graphql +type User @table { + email: String! @unique +} + +# Composite unique +type Review @table @unique(fields: ["movie", "user"]) { + movie: Movie! + user: User! + rating: Int +} +``` + +### @index +Creates database index for query performance. + +```graphql +type Movie @table @index(fields: ["genre", "releaseYear"], order: [ASC, DESC]) { + title: String! @index + genre: String + releaseYear: Int +} +``` + +| Argument | Description | +|----------|-------------| +| `fields` | Fields for composite index (on @table) | +| `order` | `[ASC]` or `[DESC]` for each field | +| `type` | `BTREE` (default), `GIN` (arrays), `HNSW`/`IVFFLAT` (vectors) | + +### @searchable +Enables full-text search on String fields. + +```graphql +type Post @table { + title: String! @searchable + body: String! @searchable(language: "english") +} + +# Usage +query SearchPosts($q: String!) @auth(level: PUBLIC) { + posts_search(query: $q) { id title body } +} +``` + +--- + +## Relationships + +### One-to-Many (Implicit Foreign Key) + +```graphql +type Post @table { + id: UUID! @default(expr: "uuidV4()") + author: User! # Creates authorId foreign key + title: String! +} + +type User @table { + id: UUID! @default(expr: "uuidV4()") + name: String! + # Auto-generated: posts_on_author: [Post!]! +} +``` + +### @ref Directive +Customizes foreign key reference. + +```graphql +type Post @table { + author: User! @ref(fields: "authorId", references: "id") + authorId: UUID! # Explicit FK field +} +``` + +| Argument | Description | +|----------|-------------| +| `fields` | Local FK field name(s) | +| `references` | Target field(s) in referenced table | +| `constraintName` | PostgreSQL constraint name | + +**Cascade behavior:** +- Required reference (`User!`): CASCADE DELETE (post deleted when user deleted) +- Optional reference (`User`): SET NULL (authorId set to null when user deleted) + +### One-to-One + +Use `@unique` on the reference field: + +```graphql +type User @table { id: UUID! name: String! } + +type UserProfile @table { + user: User! @unique # One profile per user + bio: String + avatarUrl: String +} + +# Query: user.userProfile_on_user +``` + +### Many-to-Many + +Use a join table with composite primary key: + +```graphql +type Movie @table { id: UUID! title: String! } +type Actor @table { id: UUID! name: String! } + +type MovieActor @table(key: ["movie", "actor"]) { + movie: Movie! + actor: Actor! + role: String! # Extra data on relationship +} + +# Generated fields: +# - movie.actors_via_MovieActor: [Actor!]! +# - actor.movies_via_MovieActor: [Movie!]! +# - movie.movieActors_on_movie: [MovieActor!]! +``` + +--- + +## Data Types + +| GraphQL Type | PostgreSQL Default | Other PostgreSQL Types | +|--------------|-------------------|----------------------| +| `String` | `text` | `varchar(n)`, `char(n)` | +| `Int` | `int4` | `int2`, `serial` | +| `Int64` | `bigint` | `bigserial`, `numeric` | +| `Float` | `float8` | `float4`, `numeric` | +| `Boolean` | `boolean` | | +| `UUID` | `uuid` | | +| `Date` | `date` | | +| `Timestamp` | `timestamptz` | Stored as UTC | +| `Any` | `jsonb` | | +| `Vector` | `vector` | Requires `@col(size: N)` | +| `[Type]` | Array | e.g., `[String]` → `text[]` | + +--- + +## Enumerations + +```graphql +enum Status { + DRAFT + PUBLISHED + ARCHIVED +} + +type Post @table { + status: Status! @default(value: DRAFT) + allowedStatuses: [Status!] +} +``` + +**Rules:** +- Enum names: PascalCase, no underscores +- Enum values: UPPER_SNAKE_CASE +- Values are ordered (for comparison operations) +- Changing order or removing values is a breaking change + +--- + +## Views (Advanced) + +Map custom SQL queries to GraphQL types: + +```graphql +type MovieStats @view(sql: """ + SELECT + movie_id, + COUNT(*) as review_count, + AVG(rating) as avg_rating + FROM review + GROUP BY movie_id +""") { + movie: Movie @unique + reviewCount: Int + avgRating: Float +} + +# Query movies with stats +query TopMovies @auth(level: PUBLIC) { + movies(orderBy: [{ rating: DESC }]) { + title + stats: movieStats_on_movie { + reviewCount avgRating + } + } +} +``` diff --git a/.agents/skills/firebase-data-connect/reference/sdks.md b/.agents/skills/firebase-data-connect/reference/sdks.md new file mode 100644 index 0000000..a3644c5 --- /dev/null +++ b/.agents/skills/firebase-data-connect/reference/sdks.md @@ -0,0 +1,310 @@ +# SDK Reference + +## Contents +- [SDK Generation](#sdk-generation) +- [Web SDK](#web-sdk) +- [Android SDK](#android-sdk) +- [iOS SDK](#ios-sdk) +- [Admin SDK](#admin-sdk) + +--- + +## SDK Generation + +Configure SDK generation in `connector.yaml`: + +```yaml +connectorId: my-connector +generate: + javascriptSdk: + outputDir: "../web-app/src/lib/dataconnect" + package: "@movie-app/dataconnect" + kotlinSdk: + outputDir: "../android-app/app/src/main/kotlin/com/example/dataconnect" + package: "com.example.dataconnect" + swiftSdk: + outputDir: "../ios-app/DataConnect" +``` + +Generate SDKs: +```bash +npx -y firebase-tools@latest dataconnect:sdk:generate +``` + +--- + +## Web SDK + +### Installation + +```bash +npm install firebase +``` + +### Initialization + +```typescript +import { initializeApp } from 'firebase/app'; +import { getDataConnect, connectDataConnectEmulator } from 'firebase/data-connect'; +import { connectorConfig } from '@movie-app/dataconnect'; + +const app = initializeApp(firebaseConfig); +const dc = getDataConnect(app, connectorConfig); + +// For local development +if (import.meta.env.DEV) { + connectDataConnectEmulator(dc, 'localhost', 9399); +} +``` + +### Calling Operations + +```typescript +// Generated SDK provides typed functions +import { listMovies, createMovie, getMovie } from '@movie-app/dataconnect'; + +// Accessing Nested Fields +const movie = await getMovie({ id: '...' }); +// Relations are just properties on the object +const director = movie.data.movie.metadata.director; +const firstActor = movie.data.movie.actors[0].name; + +// Query +const result = await listMovies(); +console.log(result.data.movies); + +// Query with variables +const movie = await getMovie({ id: 'uuid-here' }); + +// Mutation +const newMovie = await createMovie({ + title: 'New Movie', + genre: 'Action' +}); +console.log(newMovie.data.movie_insert); // Returns key +``` + +### Subscriptions (Realtime) + +Use `subscribe()` instead of `execute()` to receive live updates. This works with queries that use `@refresh` directives and with single-entity lookups that auto-refresh. The subscription yields current data immediately, then pushes delta updates when the server refreshes the query. + +See [reference/realtime.md](realtime.md) for how to configure which queries refresh and when. + +```typescript +import { listMoviesRef } from '@movie-app/dataconnect'; +import { subscribe } from 'firebase/data-connect'; + +const unsubscribe = subscribe(listMoviesRef(), { + onNext: (result) => { + console.log('Movies updated:', result.data.movies); + }, + onError: (error) => { + console.error('Subscription error:', error); + } +}); + +// Later: unsubscribe(); +``` + +### Framework Example (React) + +```typescript +import { useEffect, useState } from 'react'; +import { listMoviesRef } from '@movie-app/dataconnect'; +import { subscribe } from 'firebase/data-connect'; + +function MovieList() { + const [movies, setMovies] = useState([]); + + useEffect(() => { + const ref = listMoviesRef(); + const unsubscribe = subscribe(ref, { + onNext: (result) => setMovies(result.data.movies), + onError: (error) => console.error('Subscription error:', error) + }); + return () => unsubscribe(); + }, []); + + return
    {movies.map(m =>
  • {m.title}
  • )}
; +} +``` + +### With Authentication + +```typescript +import { getAuth, signInWithEmailAndPassword } from 'firebase/auth'; + +const auth = getAuth(app); +await signInWithEmailAndPassword(auth, email, password); + +// SDK automatically includes auth token in requests +const myReviews = await myReviews(); // @auth(level: USER) query from examples.md +``` + +--- + +## Android SDK + +### Dependencies (build.gradle.kts) + +```kotlin +dependencies { + implementation(platform("com.google.firebase:firebase-bom:33.0.0")) + implementation("com.google.firebase:firebase-dataconnect") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-core:1.6.0") +} +``` + +### Initialization + +```kotlin +import com.google.firebase.Firebase +import com.google.firebase.dataconnect.dataConnect +import com.example.dataconnect.MyConnector + +val connector = MyConnector.instance + +// For emulator +connector.dataConnect.useEmulator("10.0.2.2", 9399) +``` + +### Calling Operations + +```kotlin +// Query +val result = connector.listMovies.execute() +result.data.movies.forEach { movie -> + println(movie.title) + // Access nested fields directly + println(movie.metadata?.director) + println(movie.actors.firstOrNull()?.name) +} + +// Query with variables +val movie = connector.getMovie.execute(id = "uuid-here") + +// Mutation +val newMovie = connector.createMovie.execute( + title = "New Movie", + genre = "Action" +) +``` + +### Flow Subscription (Realtime) + +Use `flow()` to receive live updates from `@refresh`-enabled queries and auto-refreshing entity lookups. See [reference/realtime.md](realtime.md) for server-side configuration. + +```kotlin +connector.listMovies.flow().collect { result -> + when (result) { + is DataConnectResult.Success -> updateUI(result.data.movies) + is DataConnectResult.Error -> showError(result.exception) + } +} +``` + +--- + +## iOS SDK + +### Dependencies (Package.swift or SPM) + +```swift +dependencies: [ + .package(url: "https://github.com/firebase/firebase-ios-sdk.git", from: "11.0.0") +] +// Add FirebaseDataConnect to target dependencies +``` + +### Initialization + +```swift +import FirebaseCore +import FirebaseDataConnect + +FirebaseApp.configure() +let connector = MyConnector.shared + +// For emulator +connector.useEmulator(host: "localhost", port: 9399) +``` + +### Calling Operations + +```swift +// Query +let result = try await connector.listMovies.execute() +for movie in result.data.movies { + print(movie.title) + // Access nested fields directly + print(movie.metadata?.director ?? "Unknown") + print(movie.actors.first?.name ?? "No actors") +} + +// Query with variables +let movie = try await connector.getMovie.execute(id: "uuid-here") + +// Mutation +let newMovie = try await connector.createMovie.execute( + title: "New Movie", + genre: "Action" +) +``` + +### Subscriptions (Realtime) + +Initiate a real-time subscription to a query reference. Results are automatically published to the `data` property of the query reference, which can be observed in SwiftUI views. + +```swift +// Initiate realtime subscription to a query ref +// Results are published to the data var of the query ref +_ = try await connector.listMovies.ref().subscribe() +``` + +--- + + + +## Admin SDK + +Server-side operations with elevated privileges (bypasses @auth): + +### Node.js + +```typescript +import { initializeApp, cert } from 'firebase-admin/app'; +import { getDataConnect } from 'firebase-admin/data-connect'; + +initializeApp({ + credential: cert(serviceAccount) +}); + +const dc = getDataConnect(); + +// Execute operations (bypasses @auth) +const result = await dc.executeGraphql({ + query: `query { users { id email } }`, + operationName: 'ListAllUsers' +}); + +// Or use generated Admin SDK +import { listAllUsers } from './admin-connector'; +const users = await listAllUsers(); +``` + +### Generate Admin SDK + +In `connector.yaml`: + +```yaml +generate: + nodeAdminSdk: + outputDir: "./admin-sdk" + package: "@app/admin-dataconnect" +``` + +Generate: +```bash +npx -y firebase-tools@latest dataconnect:sdk:generate +``` diff --git a/.agents/skills/firebase-data-connect/reference/security.md b/.agents/skills/firebase-data-connect/reference/security.md new file mode 100644 index 0000000..5eacee8 --- /dev/null +++ b/.agents/skills/firebase-data-connect/reference/security.md @@ -0,0 +1,289 @@ +# Security Reference + +## Contents +- [@auth Directive](#auth-directive) +- [Access Levels](#access-levels) +- [CEL Expressions](#cel-expressions) +- [@check and @redact](#check-and-redact) +- [Authorization Patterns](#authorization-patterns) +- [Anti-Patterns](#anti-patterns) + +--- + +## @auth Directive + +Every deployable query/mutation must have `@auth`. Without it, operations default to `NO_ACCESS`. + +```graphql +query PublicData @auth(level: PUBLIC) { ... } +query UserData @auth(level: USER) { ... } +query AdminOnly @auth(expr: "auth.token.admin == true") { ... } +``` + +| Argument | Description | +|----------|-------------| +| `level` | Preset access level | +| `expr` | CEL expression (alternative to level) | +| `insecureReason` | Suppress deploy warning for PUBLIC/unfiltered USER | + +--- + +## Access Levels + +| Level | Who Can Access | CEL Equivalent | +|-------|----------------|----------------| +| `PUBLIC` | Anyone, authenticated or not | `true` | +| `USER_ANON` | Any authenticated user (including anonymous) | `auth.uid != nil` | +| `USER` | Authenticated users (excludes anonymous) | `auth.uid != nil && auth.token.firebase.sign_in_provider != 'anonymous'` | +| `USER_EMAIL_VERIFIED` | Users with verified email | `auth.uid != nil && auth.token.email_verified` | +| `NO_ACCESS` | Admin SDK only | `false` | + +> **Important:** Levels like `USER` are starting points. Always add filters or expressions to verify the user can access specific data. + +--- + +## CEL Expressions + +### Available Bindings + +| Binding | Description | +|---------|-------------| +| `auth.uid` | Current user's Firebase UID | +| `auth.token` | Auth token claims (see below) | +| `vars` | Operation variables (e.g., `vars.movieId`) | +| `request.time` | Server timestamp | +| `request.operationName` | "query" or "mutation" | + +### auth.token Fields + +| Field | Description | +|-------|-------------| +| `email` | User's email address | +| `email_verified` | Boolean: email verified | +| `phone_number` | User's phone | +| `name` | Display name | +| `sub` | Firebase UID (same as auth.uid) | +| `firebase.sign_in_provider` | `password`, `google.com`, `anonymous`, etc. | +| `` | Custom claims set via Admin SDK | + +### Expression Examples + +```graphql +# Check custom claim +@auth(expr: "auth.token.role == 'admin'") + +# Check verified email domain +@auth(expr: "auth.token.email_verified && auth.token.email.endsWith('@company.com')") + +# Check multiple conditions +@auth(expr: "auth.uid != nil && (auth.token.role == 'editor' || auth.token.role == 'admin')") + +# Check variable +@auth(expr: "has(vars.status) && vars.status in ['draft', 'published']") +``` + +### Using eq_expr in Filters + +Compare database fields with auth values: + +```graphql +query MyPosts @auth(level: USER) { + posts(where: { authorUid: { eq_expr: "auth.uid" }}) { + id title + } +} + +mutation UpdateMyPost($id: UUID!, $title: String!) @auth(level: USER) { + post_update( + first: { where: { + id: { eq: $id }, + authorUid: { eq_expr: "auth.uid" } + }}, + data: { title: $title } + ) +} +``` + +--- + +## @check and @redact + +Use `@check` to validate data and `@redact` to hide results from client: + +### @check +Validates a field value; aborts if check fails. + +```graphql +@check(expr: "this != null", message: "Not found") +@check(expr: "this == 'editor'", message: "Must be editor") +@check(expr: "this.exists(p, p.role == 'admin')", message: "No admin found") +``` + +| Argument | Description | +|----------|-------------| +| `expr` | CEL expression; `this` = current field value | +| `message` | Error message if check fails | +| `optional` | If `true`, pass when field not present | + +### @redact +Hides field from response (still evaluated for @check): + +```graphql +query @redact { ... } # Query result hidden but @check still runs +``` + +### Authorization Data Lookup + +Check database permissions before allowing mutation: + +```graphql +mutation UpdateMovie($id: UUID!, $title: String!) + @auth(level: USER) + @transaction { + # Step 1: Check user has permission + query @redact { + moviePermission( + key: { movieId: $id, userId_expr: "auth.uid" } + ) @check(expr: "this != null", message: "No access to movie") { + role @check(expr: "this == 'editor'", message: "Must be editor") + } + } + # Step 2: Update if authorized + movie_update(id: $id, data: { title: $title }) +} +``` + +### Validate Key Exists + +```graphql +mutation MustDeleteMovie($id: UUID!) @auth(level: USER) @transaction { + movie_delete(id: $id) + @check(expr: "this != null", message: "Movie not found") +} +``` + +--- + +## Authorization Patterns + +### User-Owned Resources + +```graphql +# Create with owner +mutation CreatePost($content: String!) @auth(level: USER) { + post_insert(data: { + authorUid_expr: "auth.uid", + content: $content + }) +} + +# Read own data only +query MyPosts @auth(level: USER) { + posts(where: { authorUid: { eq_expr: "auth.uid" }}) { + id content + } +} + +# Update own data only +mutation UpdatePost($id: UUID!, $content: String!) @auth(level: USER) { + post_update( + first: { where: { id: { eq: $id }, authorUid: { eq_expr: "auth.uid" }}}, + data: { content: $content } + ) +} + +# Delete own data only +mutation DeletePost($id: UUID!) @auth(level: USER) { + post_delete( + first: { where: { id: { eq: $id }, authorUid: { eq_expr: "auth.uid" }}} + ) +} +``` + +### Role-Based Access + +```graphql +# Admin-only query +query AllUsers @auth(expr: "auth.token.admin == true") { + users { id email name } +} + +# Role from database +mutation AdminAction($id: UUID!) @auth(level: USER) @transaction { + query @redact { + user(key: { uid_expr: "auth.uid" }) { + role @check(expr: "this == 'admin'", message: "Admin required") + } + } + # ... admin action +} +``` + +### Public Data with Filters + +```graphql +query PublicPosts @auth(level: PUBLIC) { + posts(where: { + visibility: { eq: "public" }, + publishedAt: { lt_expr: "request.time" } + }) { + id title content + } +} +``` + +### Tiered Access (Pro Content) + +```graphql +query ProContent @auth(expr: "auth.token.plan == 'pro'") { + posts(where: { visibility: { in: ["public", "pro"] }}) { + id title content + } +} +``` + +--- + +## Anti-Patterns + +### ❌ Don't Pass User ID as Variable + +```graphql +# BAD - any user can pass any userId +query GetUserPosts($userId: String!) @auth(level: USER) { + posts(where: { authorUid: { eq: $userId }}) { ... } +} + +# GOOD - use auth.uid +query GetMyPosts @auth(level: USER) { + posts(where: { authorUid: { eq_expr: "auth.uid" }}) { ... } +} +``` + +### ❌ Don't Use USER Without Filters + +```graphql +# BAD - any authenticated user sees all documents +query AllDocs @auth(level: USER) { + documents { id title content } +} + +# GOOD - filter to user's documents +query MyDocs @auth(level: USER) { + documents(where: { ownerId: { eq_expr: "auth.uid" }}) { ... } +} +``` + +### ❌ Don't Trust Unverified Email + +```graphql +# BAD - email not verified +@auth(expr: "auth.token.email.endsWith('@company.com')") + +# GOOD - verify email first +@auth(expr: "auth.token.email_verified && auth.token.email.endsWith('@company.com')") +``` + +### ❌ Don't Use PUBLIC/USER for Prototyping + +During development, set operations to `NO_ACCESS` until you implement proper authorization. Use emulator and VS Code extension for testing. diff --git a/.agents/skills/firebase-data-connect/templates.md b/.agents/skills/firebase-data-connect/templates.md new file mode 100644 index 0000000..0f42eea --- /dev/null +++ b/.agents/skills/firebase-data-connect/templates.md @@ -0,0 +1,318 @@ +# Templates + +Ready-to-use templates for common Firebase SQL Connect patterns. + +--- + +## Basic CRUD Schema + +```graphql +# schema.gql +type Item @table { + id: UUID! @default(expr: "uuidV4()") + name: String! + description: String + createdAt: Timestamp! @default(expr: "request.time") + updatedAt: Timestamp! @default(expr: "request.time") +} +``` + +```graphql +# queries.gql +query ListItems @auth(level: PUBLIC) { + items(orderBy: [{ createdAt: DESC }]) { + id name description createdAt + } +} + +query GetItem($id: UUID!) @auth(level: PUBLIC) { + item(id: $id) { id name description createdAt updatedAt } +} +``` + +```graphql +# mutations.gql +mutation CreateItem($name: String!, $description: String) @auth(level: USER) { + item_insert(data: { name: $name, description: $description }) +} + +mutation UpdateItem($id: UUID!, $name: String, $description: String) @auth(level: USER) { + item_update(id: $id, data: { + name: $name, + description: $description, + updatedAt_expr: "request.time" + }) +} + +mutation DeleteItem($id: UUID!) @auth(level: USER) { + item_delete(id: $id) +} +``` + +--- + +## User-Owned Resources + +```graphql +# schema.gql +type User @table(key: "uid") { + uid: String! @default(expr: "auth.uid") + email: String! @unique + displayName: String +} + +type Note @table { + id: UUID! @default(expr: "uuidV4()") + owner: User! + title: String! + content: String + createdAt: Timestamp! @default(expr: "request.time") +} +``` + +```graphql +# queries.gql +query MyNotes @auth(level: USER) { + notes( + where: { owner: { uid: { eq_expr: "auth.uid" }}}, + orderBy: [{ createdAt: DESC }] + ) { id title content createdAt } +} + +query GetMyNote($id: UUID!) @auth(level: USER) { + note( + first: { where: { + id: { eq: $id }, + owner: { uid: { eq_expr: "auth.uid" }} + }} + ) { id title content } +} +``` + +```graphql +# mutations.gql +mutation CreateNote($title: String!, $content: String) @auth(level: USER) { + note_insert(data: { + owner: { uid_expr: "auth.uid" }, + title: $title, + content: $content + }) +} + +mutation UpdateNote($id: UUID!, $title: String, $content: String) @auth(level: USER) { + note_update( + first: { where: { id: { eq: $id }, owner: { uid: { eq_expr: "auth.uid" }}}}, + data: { title: $title, content: $content } + ) +} + +mutation DeleteNote($id: UUID!) @auth(level: USER) { + note_delete( + first: { where: { id: { eq: $id }, owner: { uid: { eq_expr: "auth.uid" }}}} + ) +} +``` + +--- + +## Many-to-Many Relationship + +```graphql +# schema.gql +type Tag @table { + id: UUID! @default(expr: "uuidV4()") + name: String! @unique +} + +type Article @table { + id: UUID! @default(expr: "uuidV4()") + title: String! + content: String! +} + +type ArticleTag @table(key: ["article", "tag"]) { + article: Article! + tag: Tag! +} +``` + +```graphql +# queries.gql +query ArticlesByTag($tagName: String!) @auth(level: PUBLIC) { + articles(where: { + articleTags_on_article: { tag: { name: { eq: $tagName }}} + }) { + id title + tags: tags_via_ArticleTag { name } + } +} + +query ArticleWithTags($id: UUID!) @auth(level: PUBLIC) { + article(id: $id) { + id title content + tags: tags_via_ArticleTag { id name } + } +} +``` + +```graphql +# mutations.gql +mutation AddTagToArticle($articleId: UUID!, $tagId: UUID!) @auth(level: USER) { + articleTag_insert(data: { + article: { id: $articleId }, + tag: { id: $tagId } + }) +} + +mutation RemoveTagFromArticle($articleId: UUID!, $tagId: UUID!) @auth(level: USER) { + articleTag_delete(key: { articleId: $articleId, tagId: $tagId }) +} +``` + +--- + +## dataconnect.yaml Template + +```yaml +specVersion: "v1" +serviceId: "my-service" +location: "us-central1" +schema: + source: "./schema" + datasource: + postgresql: + database: "fdcdb" + cloudSql: + instanceId: "my-instance" +connectorDirs: ["./connector"] +``` + +--- + +## connector.yaml Template + +```yaml +connectorId: "default" +generate: + javascriptSdk: + outputDir: "../web/src/lib/dataconnect" + package: "@myapp/dataconnect" + kotlinSdk: + outputDir: "../android/app/src/main/kotlin/com/myapp/dataconnect" + package: "com.myapp.dataconnect" + swiftSdk: + outputDir: "../ios/MyApp/DataConnect" + dartSdk: + outputDir: "../flutter/lib/dataconnect" + package: myapp_dataconnect +``` + +--- + +## Firebase Init Commands + +```bash +# Initialize SQL Connect in project +npx -y firebase-tools@latest init dataconnect + +# Initialize with specific project +npx -y firebase-tools@latest use +npx -y firebase-tools@latest init dataconnect + +# Start emulator for development +npx -y firebase-tools@latest emulators:start --only dataconnect + +# Generate SDKs +npx -y firebase-tools@latest dataconnect:sdk:generate + +# Deploy to production +npx -y firebase-tools@latest deploy --only dataconnect +``` + +--- + +## SDK Initialization (Web) + +```typescript +// lib/firebase.ts +import { initializeApp } from 'firebase/app'; +import { getAuth } from 'firebase/auth'; +import { getDataConnect, connectDataConnectEmulator } from 'firebase/data-connect'; +import { connectorConfig } from '@myapp/dataconnect'; + +const firebaseConfig = { + apiKey: "...", + authDomain: "...", + projectId: "...", +}; + +export const app = initializeApp(firebaseConfig); +export const auth = getAuth(app); +export const dataConnect = getDataConnect(app, connectorConfig); + +// Connect to emulator in development +if (import.meta.env.DEV) { + connectDataConnectEmulator(dataConnect, 'localhost', 9399); +} +``` + +```typescript +// Example usage +import { listItems, createItem } from '@myapp/dataconnect'; + +// List items +const { data } = await listItems(); +console.log(data.items); + +// Create item (requires auth) +await createItem({ name: 'New Item', description: 'Description' }); +``` + +--- + +## Realtime Query Templates + +### Time-Based Polling + +```graphql +query LiveDashboard + @auth(level: PUBLIC) + @refresh(every: { seconds: 30 }) { + items(orderBy: [{ updatedAt: DESC }], limit: 20) { + id name updatedAt + } +} +``` + +### Event-Driven Refresh + +```graphql +query ItemList($categoryId: UUID!) + @auth(level: PUBLIC) + @refresh(onMutationExecuted: { + operation: "CreateItem", + condition: "request.variables.categoryId == mutation.variables.categoryId" + }) { + items(where: { category: { id: { eq: $categoryId }}}) { + id name createdAt + } +} +``` + +### Client Subscribe (Web) + +```typescript +import { liveDashboardRef } from '@myapp/dataconnect'; +import { subscribe } from 'firebase/data-connect'; + +const unsubscribe = subscribe(liveDashboardRef(), { + onNext: (result) => { + // Called immediately with current data, then on each refresh + renderDashboard(result.data.items); + }, + onError: (error) => console.error('Subscription error:', error) +}); + +// Cleanup when done +// unsubscribe(); +``` \ No newline at end of file diff --git a/.agents/skills/firebase-firestore-enterprise-native-mode/SKILL.md b/.agents/skills/firebase-firestore-enterprise-native-mode/SKILL.md new file mode 100644 index 0000000..1eeb40f --- /dev/null +++ b/.agents/skills/firebase-firestore-enterprise-native-mode/SKILL.md @@ -0,0 +1,31 @@ +--- +name: firebase-firestore-enterprise-native-mode +description: Comprehensive guide for Firestore enterprise native including provisioning, data model, security rules, and SDK usage. Use this skill when the user needs help setting up Firestore Enterprise with the Native mode, writing security rules, or using the Firestore SDK in their application. +compatibility: This skill is best used with the Firebase CLI, but does not require it. Firebase CLI can be accessed through `npx -y firebase-tools@latest`. +--- + +# Firestore Enterprise Native Mode + +This skill provides a complete guide for getting started with Firestore Enterprise Native Mode, including provisioning, data model, security rules, and SDK usage. + +## Provisioning + +To set up Firestore Enterprise Native Mode in your Firebase project and local environment, see [provisioning.md](references/provisioning.md). + +## Data Model + +To learn about Firestore data model and how to organize your data, see [data_model.md](references/data_model.md). + +## Security Rules + +For guidance on writing and deploying Firestore Security Rules to protect your data, see [security_rules.md](references/security_rules.md). + +## SDK Usage + +To learn how to use Firestore Enterprise Native Mode in your application code, see: +- [Web SDK Usage](references/web_sdk_usage.md) +- [Python SDK Usage](references/python_sdk_usage.md) + +## Indexes + +Indexes help improve query performance and speed up slow queries. For checking index types, query support tables, and best practices, see [indexes.md](references/indexes.md). diff --git a/.agents/skills/firebase-firestore-enterprise-native-mode/references/data_model.md b/.agents/skills/firebase-firestore-enterprise-native-mode/references/data_model.md new file mode 100644 index 0000000..0fe42c0 --- /dev/null +++ b/.agents/skills/firebase-firestore-enterprise-native-mode/references/data_model.md @@ -0,0 +1,54 @@ +# Firestore Data Model Reference + +Firestore is a NoSQL, document-oriented database. Unlike a SQL database, there are no tables or rows. Instead, you store data in **documents**, which are organized into **collections**. + +## Document Data Model + +Data in Firestore is organized into documents, collections, and subcollections. + +### Documents +A **document** is a lightweight record that contains fields, which map to values. Each document is identified by a name. A document can contain complex nested objects in addition to basic data types like strings, numbers, and booleans. Documents are limited to a maximum size of 1 MiB. + +Example document (e.g., in a `users` collection): +```json +{ + "first": "Ada", + "last": "Lovelace", + "born": 1815 +} +``` + +### Collections +Documents live in **collections**, which are containers for your documents. For example, you could have a `users` collection to contain your various users, each represented by a document. +* Collections can only contain documents. They cannot directly contain raw fields with values, and they cannot contain other collections. +* Documents within a collection can contain different fields. +* You don't need to "create" or "delete" collections explicitly. After you create the first document in a collection, the collection exists. If you delete all of the documents in a collection, the collection no longer exists. + +### Subcollections +Documents can contain subcollections natively. A subcollection is a collection associated with a specific document. +For example, a user document in the `users` collection could have a `messages` subcollection containing message documents exclusively for that user. This creates a powerful hierarchical data structure. + +Data path example: `users/user1/messages/message1` + +## Collection Group Support + +A **collection group** consists of all collections with the same ID. By default, queries retrieve results from a single collection in your database. Use a collection group query to retrieve documents from a collection group instead of from a single collection. + +### Use Cases +Collection group queries are useful when you want to query across multiple subcollections that share the same organizational structure. + +For example, imagine an app with a `landmarks` collection where each landmark has a `reviews` subcollection. If you want to find all 5-star reviews across *all* landmarks, it would involve checking many separate `reviews` subcollections. With a collection group, you can perform a single query against the `reviews` collection group. + +### Examples + +**Standard Query** (Single Collection): +Find all 5-star reviews for a specific landmark. +```javascript +db.collection('landmarks/golden_gate_bridge/reviews').where('rating', '==', 5) +``` + +**Collection Group Query**: +Find all 5-star reviews across *all* landmarks. +```javascript +db.collectionGroup('reviews').where('rating', '==', 5) +``` \ No newline at end of file diff --git a/.agents/skills/firebase-firestore-enterprise-native-mode/references/indexes.md b/.agents/skills/firebase-firestore-enterprise-native-mode/references/indexes.md new file mode 100644 index 0000000..f1031bd --- /dev/null +++ b/.agents/skills/firebase-firestore-enterprise-native-mode/references/indexes.md @@ -0,0 +1,111 @@ +# Firestore Indexes Reference + +Indexes helps to improve query performance. Firestore Enterprise edition does not create any indexes by default. By default, Firestore Enterprise performs a full collection scan to find documents that match a query, which can be slow and expensive for large collections. To avoid this, you can create indexes to optimize your queries. + +## Index Structure + +An index consists of the following: + +* a collection ID. +* a list of fields in the given collection. +* an order, either ascending or descending, for each field. + +### Index Ordering + +The order and sort direction of each field uniquely defines the index. For example, the following indexes are two distinct indexes and not interchangeable: + +* Field name `name` (ascending) and `population` (descending) +* Field name `name` (descending) and `population` (ascending) + +### Index Density + +Dense indexes: By default, Firestore indexes store data from all documents in a collection. An index entry will be added for a document regardless of whether the document contains any of the fields specified in the index. Non-existent fields are treated as having a NULL value when generating index entries. + +Sparse indexes: To change this behavior, you can define the index as a sparse index. A sparse index indexes only the documents in the collection that contain a value (including null) for at least one of the indexed fields. A sparse index reduces storage costs and can improve performance. + +### Unique Indexes + +You can use unique index option to enforce unique values for the indexed fields. For indexes on multiple fields, each combination of values must be unique across the index. The database rejects any update and insert operations that attempt to create index entries with duplicate values. + +## Query Support Examples + +| Query Type | Index Required | +| :--- | :--- | +| **Simple Equality**
`where("a", "==", 1)` | Single-Field Index on field `a` | +| **Simple Range/Sort**
`where("a", ">", 1).orderBy("a")` | Single-Field Index on field `a` | +| **Multiple Equality**
`where("a", "==", 1).where("b", "==", 2)` | Single-Field Index on field `a` and `b` | +| **Equality + Range/Sort**
`where("a", "==", 1).where("b", ">", 2)` | **Composite Index** on field `a` and `b` | +| **Multiple Ranges**
`where("a", ">", 1).where("b", ">", 2)` | **Composite Index** on field `a` and `b` | +| **Array Contains + Equality**
`where("tags", "array-contains", "news").where("active", "==", true)` | **Composite Index** on field `tags` and `active` | + +If no indexes is present, Firestore Enterprise will perform a full collection scan to find documents that match a query. + +## Management + +### Config files +Your indexes should be defined in `firestore.indexes.json` (pointed to by `firebase.json`). + +Define a dense index: + +```json +{ + "indexes": [ + { + "collectionGroup": "cities", + "queryScope": "COLLECTION", + "density": "DENSE", + "fields": [ + { "fieldPath": "country", "order": "ASCENDING" }, + { "fieldPath": "population", "order": "DESCENDING" } + ] + } + ], + "fieldOverrides": [] +} +``` + +Define a sparse-any index: + +```json +{ + "indexes": [ + { + "collectionGroup": "cities", + "queryScope": "COLLECTION", + "density": "SPARSE_ANY", + "fields": [ + { "fieldPath": "country", "order": "ASCENDING" }, + { "fieldPath": "population", "order": "DESCENDING" } + ] + } + ], + "fieldOverrides": [] +} +``` + +Define a unique index: + +```json +{ + "indexes": [ + { + "collectionGroup": "cities", + "queryScope": "COLLECTION", + "density": "SPARSE_ANY", + "unique": true, + "fields": [ + { "fieldPath": "country", "order": "ASCENDING" }, + { "fieldPath": "population", "order": "DESCENDING" } + ] + } + ], + "fieldOverrides": [] +} +``` + +### CLI Commands + +Deploy indexes only: +```bash +npx firebase-tools@latest -y deploy --only firestore:indexes +``` \ No newline at end of file diff --git a/.agents/skills/firebase-firestore-enterprise-native-mode/references/provisioning.md b/.agents/skills/firebase-firestore-enterprise-native-mode/references/provisioning.md new file mode 100644 index 0000000..02a97e3 --- /dev/null +++ b/.agents/skills/firebase-firestore-enterprise-native-mode/references/provisioning.md @@ -0,0 +1,101 @@ +# Provisioning Firestore Enterprise Native Mode + +## Manual Initialization + +Initialize the following firebase configuration files manually. Do not use `npx -y firebase-tools@latest init`, as it expects interactive inputs. + +1. **Create a Firestore Enterprise Database**: Create a Firestore Enterprise database using the Firebase CLI. +2. **Create `firebase.json`**: This file contains database configuration for the Firebase CLI. +3. **Create `firestore.rules`**: This file contains your security rules. +4. **Create `firestore.indexes.json`**: This file contains your index definitions. + +### 1. Create a Firestore Enterprise Database + +Use the following command to create a Firestore Enterprise database: + +```bash +firebase firestore:databases:create my-database-id \ + --location="nam5" \ + --edition="enterprise" \ + --firestore-data-access="ENABLED" \ + --mongodb-compatible-data-access="DISABLED" +``` + +This will create an enterprise database in `nam5` with native mode enabled. A database id is required to create an enterprise database and the database id must not be `(default)`. To enable realtime-updates feature, use `--realtime-updates` flag. + +```bash +firebase firestore:databases:create my-database-id \ + --location="nam5" \ + --edition="enterprise" \ + --firestore-data-access="ENABLED" \ + --mongodb-compatible-data-access="DISABLED" \ + --realtime-updates="ENABLED" +``` + +### 2. Create `firebase.json` + +Create a file named `firebase.json` in your project root with the following content. If this file already exists, instead append to the existing JSON: + +```json +{ + "firestore": { + "rules": "firestore.rules", + "indexes": "firestore.indexes.json", + "edition": "enterprise", + "database": "my-database-id", + "location": "nam5" + } +} +``` + +### 2. Create `firestore.rules` + +Create a file named `firestore.rules`. A good starting point (locking down the database) is: + +``` +rules_version = '2'; +service cloud.firestore { + match /databases/{database}/documents { + match /{document=**} { + allow read, write: if false; + } + } +} +``` +*See [security_rules.md](security_rules.md) for how to write actual rules.* + +### 3. Create `firestore.indexes.json` + +Create a file named `firestore.indexes.json` with an empty configuration to start: + +```json +{ + "indexes": [], + "fieldOverrides": [] +} +``` + +*See [indexes.md](indexes.md) for how to configure indexes.* + + +## Deploy rules and indexes +```bash +# To deploy all rules and indexes +firebase deploy --only firestore + +# To deploy just rules +firebase deploy --only firestore:rules + +# To deploy just indexes +firebase deploy --only firestore:indexes +``` + +## Local Emulation + +To run Firestore locally for development and testing: + +```bash +firebase emulators:start --only firestore +``` + +This starts the Firestore emulator, typically on port 8080. You can interact with it using the Emulator UI (usually at http://localhost:4000/firestore). \ No newline at end of file diff --git a/.agents/skills/firebase-firestore-enterprise-native-mode/references/python_sdk_usage.md b/.agents/skills/firebase-firestore-enterprise-native-mode/references/python_sdk_usage.md new file mode 100644 index 0000000..8bd67a4 --- /dev/null +++ b/.agents/skills/firebase-firestore-enterprise-native-mode/references/python_sdk_usage.md @@ -0,0 +1,126 @@ +# Python SDK Usage + +The Python Server SDK is used for backend/server environments and utilizes Google Application Default Credentials in most Google Cloud environments. + +### Writing Data + +#### Set a Document +Creates a document if it does not exist or overwrites it if it does. You can also specify a merge option to only update provided fields. + +```python +city_ref = db.collection("cities").document("LA") + +# Create/Overwrite +city_ref.set({ + "name": "Los Angeles", + "state": "CA", + "country": "USA" +}) + +# Merge +city_ref.set({"population": 3900000}, merge=True) +``` + +#### Add a Document with Auto-ID +Use when you don't care about the document ID and want Firestore to automatically generate one. + +```python +update_time, city_ref = db.collection("cities").add({ + "name": "Tokyo", + "country": "Japan" +}) +print("Document written with ID: ", city_ref.id) +``` + +#### Update a Document +Update some fields of an existing document without overwriting the entire document. Fails if the document doesn't exist. + +```python +city_ref = db.collection("cities").document("LA") +city_ref.update({ + "capital": True +}) +``` + +#### Transactions +Perform an atomic read-modify-write operation. + +```python +from google.cloud.firestore import Transaction + +transaction = db.transaction() +city_ref = db.collection("cities").document("SF") + +@firestore.transactional +def update_in_transaction(transaction, city_ref): + snapshot = city_ref.get(transaction=transaction) + if not snapshot.exists: + raise Exception("Document does not exist!") + + new_population = snapshot.get("population") + 1 + transaction.update(city_ref, {"population": new_population}) + +update_in_transaction(transaction, city_ref) +``` + +### Reading Data + +#### Get a Single Document + +```python +doc_ref = db.collection("cities").document("SF") +doc = doc_ref.get() + +if doc.exists: + print(f"Document data: {doc.to_dict()}") +else: + print("No such document!") +``` + +#### Get Multiple Documents +Fetches all documents in a query or collection once. + +```python +docs = db.collection("cities").stream() + +for doc in docs: + print(f"{doc.id} => {doc.to_dict()}") +``` + +### Queries + +#### Simple and Compound Queries +Use `.where()` to combine filters safely. Stack `.where()` calls for compound queries. + +```python +from google.cloud.firestore import FieldFilter + +cities_ref = db.collection("cities") + +# Simple equality +query_1 = cities_ref.where(filter=FieldFilter("state", "==", "CA")) + +# Compound (AND) +query_2 = cities_ref.where( + filter=FieldFilter("state", "==", "CA") +).where( + filter=FieldFilter("population", ">", 1000000) +) +``` + +#### Order and Limit +Sort and limit results cleanly. + +```python +query = cities_ref.order_by("name").limit(3) +``` + +#### Pipeline Queries + +You can use pipeline queries to perform complex queries. + +```python +pipeline = client.pipeline().collection("users") +for result in pipeline.execute(): + print(f"{result.id} => {result.data()}") +``` diff --git a/.agents/skills/firebase-firestore-enterprise-native-mode/references/security_rules.md b/.agents/skills/firebase-firestore-enterprise-native-mode/references/security_rules.md new file mode 100644 index 0000000..fe24fad --- /dev/null +++ b/.agents/skills/firebase-firestore-enterprise-native-mode/references/security_rules.md @@ -0,0 +1,414 @@ +## 1. Generate Firestore Rules +You are an expert Firebase Security Rules engineer with deep knowledge of Firestore security best practices. Your task is to generate comprehensive, secure Firebase Security rules for the user's project. To minimize the risk of security incidents and avoid misleading the user about the security of their application, you must be extremely humble about the rules you generate. Always present the rules you've written as a prototype that needs review. + +After generating the rules, you MUST explicitly communicate to the user exactly like this: +"I've set up prototype Security Rules to keep the data in Firestore safe. They are designed to be secure for . However, you should review and verify them before broadly sharing your app. If you'd like, I can help you harden these rules." + +### Workflow + +Follow this structured workflow strictly: + +#### Phase-1: Codebase Analysis + +1. **Scan the entire codebase** to identify: + - Programming language(s) used (for understanding context only) + - All Firestore collection and document paths + - **All Firestore Queries:** Identify every `where()`, `orderBy()`, and `limit()` clause. The security rules **MUST** allow these specific queries. + - Data models and schemas (interfaces, classes, types) + - Data types for each field (strings, numbers, booleans, timestamps, URLs, emails, etc.) + - Required vs. optional fields + - Field constraints (min/max length, format patterns, allowed values) + - CRUD operations (create, read, update, delete) + - Authentication patterns (Firebase Auth, custom tokens, anonymous) + - Access patterns and business logic rules +2. **Document your findings** in a untracked file. Refer to this file when generating the security rules. + +#### Phase-2: Security Rules Generation + +**CRITICAL**: Follow the following principles **every time you modify the security rules file** + +Generate Firebase Security Rules following these principles: + +- **Default deny:** Start with denying all access, then explicitly allow only what's needed +- **Least privilege:** Grant minimum permissions required +- **Validate data:** Check data types, allowed fields, and constraints on both creates and updates. + - **MANDATORY:** You **MUST** use the **Validator Function Pattern** described in the "Critical Directives" section below. This involves defining a specific validation function (e.g., `isValidUser`) and calling it in **BOTH** `create` and `update` rules. + - **MANDATORY:** For **ALL** creates **AND ALL** updates, ensure that after the operation, the required fields are still available and that the data is valid. +- **Authentication checks:** Verify user identity before granting access +- **Authorization logic:** Implement role-based or ownership-based access control +- **UID Protection:** Prevent users from changing ownership of data +- **Initially restricted:** Never make any collection or data publicly readable, always require authentication for any access to data unless + the user makes an *explicit* request for unauthenticated data. + +This means the first firestore.rules file you generate must never have any "allow read: true" statements. + +**Structure Requirements:** + +1. **Document assumed data models at the beginning of the rules file:** + +```javascript +// =============================================================== +// Assumed Data Model +// =============================================================== +// +// This security rules file assumes the following data structures: +// +// Collection: [name] +// Document ID: [pattern] +// Fields: +// - field1: type (required/optional, constraints) - description +// - field2: type (required/optional, constraints) - description +// [List all fields with types, constraints, and whether immutable] +// +// [Repeat for all collections] +// +// =============================================================== +``` + +2. **Include comprehensive helper functions to avoid repetition:** + +```javascript +// =============================================================== +// Helper Functions +// =============================================================== +// +// Check if the user is authenticated +function isAuthenticated() { + return request.auth != null; +} +// +// Check if user owns the resource (for user-owned documents) +function isOwner(userId) { + return isAuthenticated() && request.auth.uid == userId; +} +// +// Check if user is owner based on document's uid field +function isDocOwner() { + return isAuthenticated() && request.auth.uid == resource.data.uid; +} +// +// Verify UID hasn't been tampered with on create +function uidUnchanged() { + return !('uid' in request.resource.data) || + request.resource.data.uid == request.auth.uid; +} +// +// Ensure uid field is not modified on update +function uidNotModified() { + return !('uid' in request.resource.data) || + request.resource.data.uid == resource.data.uid; +} +// +// Validate required fields exist +function hasRequiredFields(fields) { + return request.resource.data.keys().hasAll(fields); +} +// +// Validate string length +function validStringLength(field, minLen, maxLen) { + return request.resource.data[field] is string && + request.resource.data[field].size() >= minLen && + request.resource.data[field].size() <= maxLen; +} +// +// Validate URL format (must start with https:// or http://) +function isValidUrl(url) { + return url is string && + (url.matches("^https://.*") || url.matches("^http://.*")); +} +// +// Validate email format +function isValidEmail(email) { + return email is string && + email.matches("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$"); +} + +// +// Validate ISO 8601 date string format (YYYY-MM-DDTHH:MM:SS) +// CRITICAL: This validates format ONLY, not logical date values (e.g., month 13). +// Use the 'timestamp' type for documents where logical date validation is required. +function isValidDateString(dateStr) { + return dateStr is string && + dateStr.matches("^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}.*Z?$"); +} + +// +// Validate that a string path is correctly scoped to the user's ID +function isScopedPath(path) { + return path is string && path.matches("^users/" + request.auth.uid + "/.*"); +} +// +// Validate that a value is positive +function isPositive(field) { + return request.resource.data[field] is number && request.resource.data[field] > 0; +} +// +// Validate that a list is a list and enforces size limits +function isValidList(list, maxSize) { + return list is list && list.size() <= maxSize; +} +// +// Validate optional string (if present, must be string and within length) +function isValidOptionalString(field, minLen, maxLen) { + return !('field' in request.resource.data) || + (request.resource.data[field] is string && + request.resource.data[field].size() >= minLen && + request.resource.data[field].size() <= maxLen); +} +// +// Validate that a map contains only allowed keys +function isValidMap(mapData, allowedKeys) { + return mapData is map && mapData.keys().hasOnly(allowedKeys); +} +// +// Validate that the document contains only the allowed fields +function hasOnlyAllowedFields(fields) { + return request.resource.data.keys().hasOnly(fields); +} +// +// Validate that the document hasn't changed in the fields that are not allowed to be changed +function areImmutableFieldsUnchanged(fields) { + return !request.resource.data.diff(resource.data).affectedKeys().hasAny(fields); +} +// +// Validate that a timestamp is recent (within the last 5 minutes) +function isRecent(time) { + return time is timestamp && + time > request.time - duration.value(5, 'm') && + time <= request.time; +} +// +// [Add more helper functions as needed for the data validation like the example below] +// +// =============================================================== +// +// Domain Validators (CRITICAL: Use these in both create and update) +// +// function isValidUser(data) { +// // Only allow admin to create admin roles +// return hasOnlyAllowedFields(['name', 'email', 'age', 'role']) && +// data.name is string && data.name.size() > 0 && data.name.size() < 50 && +// data.email is string && isValidEmail(data.email) && +// data.age is number && data.age >= 18 && +// data.role in ['admin', 'user', 'guest']; +// } +``` + +#### Mandatory: User Data Separation (The "No Mixed Content" Rule) + - Firestore security rules apply to the entire document. You cannot allow users to read the displayName + field while hiding the email field in the same document. + - If a collection (e.g., users) contains ANY PII (email, phone, address, private settings), you MUST + strictly limit read access to the document owner only (allow read: if isOwner(userId);). + - If the application requires public profiles (e.g., showing user names/avatars on posts): + - 1. Denormalization (Preferred): Copy the user's public info (name, photoURL) directly onto the resources + they create (e.g., store authorName and authorPhoto inside the posts document). + - 2. Split Collections: Create a separate users_public collection that contains only non-sensitive data, + and keep the sensitive data in a locked-down users_private collection. + - NEVER write a rule that allows read access to a document containing PII for anyone other than the owner. + +#### **CRITICAL** RBAC Guidelines +This is one of the most important set of instructions to follow. Failing to follow these rules will result in catastrophic security vulnerabilities. + +- **NEVER** allow users to create their own privileged roles. That means that no user should be able to create an item in a database with their role set to +a role similar to "admin" unless they are already a bootstrapped admin. +- **NEVER** allow users to update their own roles or permissions. +- **NEVER** allow users to grant themselves access to other users' data. +- **NEVER** allow users to bypass the role hierarchy. +- **ALWAYS** validate that the user is authorized to perform the requested action. +- **ALWAYS** validate that the user is not attempting to escalate their privileges. +- **ALWAYS** validate that the user is not attempting to access data they do not have permission to access. + +Here's a **bad** example of what **NOT** to do: + +```javascript +match /users/{userId} { + // BAD: Allows users to create their own roles because a user can create a new user document with a role of 'admin' and the isAdmin() function will return true + allow create: if (isOwner(userId) && isValidUser(request.resource.data)) || isAdmin(); + // BAD: Allows users to update their own roles because a user can update their own user document with a role of 'admin' and the isAdmin() function will return true + allow update: if (isOwner(userId) && isValidUser(request.resource.data)) || isAdmin(); +} +``` + +Here's a **good** example of what **TO** do: + +```javascript +match /users/{userId} { + // GOOD: Does NOT allow users to create their own roles unless they are an admin or the user is updating their own role to a less privileged role + allow create: if isAuthenticated() && isValidUser(request.resource.data) && ((isOwner(userId) && request.resource.data.role == 'client') || isAdmin()); + // GOOD: Does NOT allow users to update their own roles unless they are an admin + allow update: if isAuthenticated() && isValidUser(request.resource.data) && ((isOwner(userId) && request.resource.data.role == resource.data.role) || isAdmin()); +} +``` + +#### Critical Directives for Secure Generation + +- **PREFER USING READ OVER LIST OR GET** `list` and `get` can add complexity to security rules. Prefer using `read` over them. +- **Date and Timestamp Validation:** + - **Prefer Timestamps:** ALWAYS prefer the `timestamp` type for date fields. Firestore automatically ensures they are logically valid dates. + - **String Date Risks:** If using strings for dates (e.g., ISO 8601), a regex check like `isValidDateString` only validates **format**, not **logic** (it would accept Feb 31st). + - **Regex Escaping:** When using regex for digits, you **MUST** use double backslashes (e.g., `\\\\d`) in the rules string. Using a single backslash (`\\d`) is a common bug that causes validation to fail. +- **Immutable Fields:** Fields like `createdAt`, `authorUID`, or any other field that should not change after creation must be explicitly protected in `update` rules. (e.g., `request.resource.data.createdAt == resource.data.createdAt`). **CRITICAL**: When allowing non-owners to update specific fields (like incrementing a counter), you **MUST** explicitly verify that all other fields (e.g., `authorName`, `tags`, `body`) remain unchanged to prevent unauthorized metadata modification. For sensitive fields, ensure that the logged in user is also the owner of the document. +- **Identity Integrity:** When storing denormalized user identity (e.g. `authorName`, `authorPhoto`), you **MUST** validate this data. + - **Prefer Auth Token:** If possible, check if `request.resource.data.authorName == request.auth.token.name`. + - **Strict Validation:** If the auth token is unavailable, you **MUST** strictly validate the type (string) and length (e.g. < 50 chars) to prevent spoofing with massive or malicious payloads. + - **Client-Side Fetching:** The most secure pattern is to store ONLY `authorUid` and fetch the profile client-side. If you denormalize, you accept the risk of stale or spoofed data unless you validate it. +- **Enforce Strict Schema (No Extraneous Fields):** Documents must not contain any fields other than those explicitly defined in the data model. This prevents users from adding arbitrary data. +- **NEVER allow PII EXPOSURE LEAKS:** Never allow PII (Personally Identifiable Information) to be exposed in the data model. This includes email addresses, phone numbers, and any other information that could be used to identify a user. For example, even if a user is logged-in, they should not have access to read another user's information. +- **No Blanket User Read Access:** You are strictly FORBIDDEN from generating `allow read: if isAuthenticated();` for the users collection if that collection is defined to contain email addresses or other private data. +- **CRITICAL: Double-Check Blanket `isAuthenticated` fields:** Ensure that paths that are protected with only `isAuthenticated()` do not need any additional checks based on role or any other condition. +- **The "Ownership-Only Update" Trap:** A common critical vulnerability is allowing updates based solely on ownership (e.g., `allow update: if isOwner(resource.data.uid);`). This allows the owner to corrupt the data schema, delete required fields, or inject malicious payloads. You **MUST** always combine ownership checks with data validation (e.g., `allow update: if isOwner(...) && isValidEntity(...);`) **AND** validate that self-escalation is not possible. + +- **Deep Array Inspection:** It is insufficient to check if a field `is list`. You **MUST** validate the contents of the array (e.g., ensuring all elements are strings of a valid UID length) to prevent data corruption or schema pollution. For example, a `tags` array must verify that every item is a string AND that each string is within a reasonable length (e.g., < 20 chars). +- **Permission-Field Lockdown:** Fields that control access (e.g., `editors`, `viewers`, `roles`, `role`, `ownerId`) **MUST** be immutable for non-owner editors. In `update` rules, use `fieldUnchanged()` for these fields unless the `request.auth.uid` matches the document's original owner/creator. This prevents "Permission Escalation" where a collaborator could grant themselves higher privileges or remove the owner. + + +### Advanced Validation for Business Logic + + Secure rules must enforce the application's business logic. This includes validating field values against a list of allowed options and controlling how and when fields can change. + + #### 1. Enforce Enum Values + + If a field should only contain specific values (e.g., a status), validate against a list. + + **Example:** + + ```javascript + // A 'task' document's status can only be one of three values + function isValidStatus() { + let validStatuses = ['pending', 'in-progress', 'completed']; + return request.resource.data.status in validStatuses; + } + + allow create: if isValidStatus() && ... + ``` + + #### 2. Validate State Transitions + + For `update` operations, you **MUST** validate that a field is changing from a valid previous state to a valid new state. This prevents users from bypassing workflows (e.g., marking a task as 'completed' from 'archived'). + + **Example:** + + ```javascript + // A task can only be marked 'completed' if it was 'in-progress' + function validStatusTransition() { + let previousStatus = resource.data.status; + let newStatus = request.resource.data.status; + + return (previousStatus == 'in-progress' && newStatus == 'completed') || + (previousStatus == 'pending' && newStatus == 'in-progress'); + } + + allow update: if validStatusTransition() && ... + ``` + +#### 3. Strict Path and Relationship Scoping + +For any field that references another resource (like an image path or a parent document ID), you **MUST** ensure it is correctly scoped to the user or valid within the context. + +**Example:** + +```javascript +// Ensure image path is within the user's own storage folder +allow create: if isScopedPath(request.resource.data.imageBucket) && ... +``` + +#### 4. Secure Counter Updates + +When allowing users to update a counter (like `voteCount` or `answerCount`), you **MUST** ensure: +1. **Atomic Increments:** The field is only changing by exactly +1 or -1. +2. **Isolation:** **NO OTHER FIELDS** are being modified. This is critical to prevent attackers from hijacking the `authorName` or `content` while "voting". +3. **Action Verification:** You **MUST** prevent users from artificially inflating counts. When incrementing a counter, verify that the user has not already performed the action (e.g., by checking for the existence of a 'like' document) and is not looping updates. + * **CRITICAL:** Relying solely on `!exists(likeDoc)` is insufficient because a malicious user can skip creating the document and loop the increment. + * **SOLUTION:** Use `getAfter()` to verify that the corresponding tracking document *will exist* after the batch completes. + +**Example:** + +```javascript +function isValidCounterUpdate(docId) { + // Allow update only if 'voteCount' is the ONLY field changing + return request.resource.data.diff(resource.data).affectedKeys().hasOnly(['voteCount']) && + // And the change is exactly +1 or -1 + math.abs(request.resource.data.voteCount - resource.data.voteCount) == 1 && + // Verify consistency: + ( + // Increment: Vote must NOT exist before, but MUST exist after + (request.resource.data.voteCount > resource.data.voteCount && + !exists(/databases/$(database)/documents/votes/$(request.auth.uid + '_' + docId)) && + getAfter(/databases/$(database)/documents/votes/$(request.auth.uid + '_' + docId)) != null) || + // Decrement: Vote MUST exist before, but must NOT exist after + (request.resource.data.voteCount < resource.data.voteCount && + exists(/databases/$(database)/documents/votes/$(request.auth.uid + '_' + docId)) && + getAfter(/databases/$(database)/documents/votes/$(request.auth.uid + '_' + docId)) == null) + ); +} + +allow update: if isValidCounterUpdate(docId) && ... +``` + +#### 5. **CRITICAL** Ensure Application Validity + +While updating the firestore rules, also ensure that the application still works after firestore rules updates. + +3. **For each collection, implement explicit data validation:** + +- Type Checking: 'field is string', 'field is number', 'field is bool', 'field is timestamp' +- Required fields validation using 'hasRequiredFields()' +- **Enforce Size Limits:** For **EVERY** string, list, and map field, you **MUST** enforce realistic size limits (e.g., `text.size() < 1000`, `tags.size() < 20`). **Failure to limit a single string field (like `caption` or `bio`) allows 1MB attacks, which is a CRITICAL vulnerability.** +- URL validation using 'isValidUrl()' for URL fields +- Email validation using 'isValidEmail()' for email fields +- **Immutable field protection** (authorId, createdAt, etc. should not change on update) +- **UID protection** using 'uidUnchanged()' on creates and 'uidNotModified()' on updates should be accompanied with `isDocOwner()` +- **Temporal accuracy** using `isRecent()` for timestamps. +- **Range validation** using `isPositive()` or similar for numbers. +- **Path scoping** using `isScopedPath()` for storage paths. + +Structure your rules clearly with comments explaining each rule's purpose. + +#### Phase-3: Devil's Advocate Attack + +**Critical step:** Systematically attempt to break your own rules using the following attack vectors. You MUST document the outcome of each attempt. + +1. **Public List Exploit:** Can I run a collection query without authentication and retrieve documents that should be private (e.g., where `visible == false`)? +2. **Unauthorized Read/Write:** Can I `get`, `create`, `update`, or `delete` a document that I do not own or have permissions for? +3. **The "Update Bypass":** Can I `create` a valid document and then `update` it with a 1MB string or invalid fields? (Tests if validation logic is missing from `update`). +4. **Ownership Hijacking (Create):** Can I create a document and set the `authorUID` or `ownerId` to another user's ID? +5. **Ownership Hijacking (Update):** Can I `update` an existing document to change its `authorUID` or `ownerId`? +6. **Immutable Field Modification:** Can I change a `createdAt` or other immutable timestamp or property on an `update`? +7. **Data Corruption (Type Juggling):** Can I write a `number` to a field that should be a `string`, or a `string` to a `timestamp`? +8. **Validation Bypass (Create vs. Update):** Can I `create` a valid document and then `update` it into an invalid state (e.g., remove a required field, write a string that's too long)? +9. **Resource Exhaustion / DoS:** Can I write an enormous string (e.g., 1MB) to any field that accepts a string or a massive array to a list field? Every string field (e.g., `bio`, `url`, `name`) MUST have a `.size()` check. If any are missing, it's a "Resource Exhaustion/DoS" risk. +10. **Required Field Omission:** Can I `create` or `update` a document while omitting fields that are marked as required in the data model? +11. **Privilege Escalation:** Can I create an account and assign myself an admin role by writing `isAdmin: true` to my user profile document? (Tests reliance on document data vs. custom claims). +12. **Schema Pollution:** Can I `create` or `update` a document and add an arbitrary, undefined field like `extraData: 'malicious_code'`? (Tests for strict schema enforcement). +13. **Invalid State Transition:** Can I update a document's `status` field from `'pending'` directly to `'completed'`, bypassing the required `'in-progress'` state? (Tests business logic enforcement). +14. **Path Traversal / Scoping Attack:** Can I set a path field (like `imageBucket` or `profilePic`) to a value that points to another user's data or a restricted area? (Tests for regex path scoping). +15. **Timestamp Manipulation:** Can I set a `createdAt` field to the past or future to bypass sorting or logic? (Tests for `request.time` validation). +16. **Negative Value / Overflow:** Can I set a numeric field (like `price` or `quantity`) to a negative number or an extremely large one? (Tests for range validation). +17. **The "Mixed Content" Leak:** Create a second user. Can User B read User A's users document? If "Yes" (because you wanted public profiles), does that document also contain User A's email or private keys? If both are true, the rules are insecure. +18. **Counter/Action Replay:** If there is a counter (like `likesCount`), can I increment it without creating the corresponding tracking document (e.g., inside `likes/{userId}`)? Can I increment it twice? (Tests for `getAfter()` consistency checks). +19. **Orphaned Subcollection Access:** Can I read/write to a subcollection (e.g., `users/123/posts/456`) if the parent document (`users/123`) does not exist? (Tests for parent existence checks). +20. **Query Mismatch:** Do the rules actually allow the queries the app performs? (e.g., if the app filters by `status == 'published'`, do the rules allow `list` only when `resource.data.status == 'published'`?) +21. **Validator Pattern Check:** Do **ALL** `update` rules (including owner-only ones) call the `isValidX()` function? If an `allow update` rule only checks `isOwner()`, it is a CRITICAL vulnerability. + +Document each attack attempt and whether it succeeded. If ANY attack succeeds: + +- Fix the security hole +- Regenerate the rules +- **Repeat Phase-3** until no attacks succeed + +#### Phase-4: Syntactic Validation + +Once devil's advocate testing passes, repeat until rules pass validation. + +**After all phases are complete, create or update the `firestore.rules` file.** + +### Critical Constraints +1. **Never skip the devil's advocate phase** - this is your primary security validation +2. **MUST include helper functions** for common operations ('isAuthenticated', 'isOwner', 'uidUnchanged', 'uidNotModified') AND domain validators ('isValidUser', etc.) +3. **MUST document assumed data models** at the beginning of the rules file +4. **Always validate the rules syntax** using 'firebase deploy --only firestore:rules --dry-run' or a similar tool before outputting the final file. +5. **Provide complete, runnable code** - no placeholders or TODOs +6. **Document all assumptions** about data structure or access patterns +7. **Always run the devil's advocate attack** after any modification of the rules. +8. **Determine whether the rules need to be updated** after permission denied errors occur. +9. **Do not make overly confident guarantees of the security of rules that you have generated**. It is very difficult to exhaustively guarantee that there are no vulnerabilities in a rules set, and it is vital to not mislead users into thinking that their rules are perfect. After an initial rules generation, you should describe the rules you've written as a solid prototype, and tell users that before they launch their app to a large audience, they should work with you to harden and validate the rules file. Be clear that users should carefully review rules to ensure security. diff --git a/.agents/skills/firebase-firestore-enterprise-native-mode/references/web_sdk_usage.md b/.agents/skills/firebase-firestore-enterprise-native-mode/references/web_sdk_usage.md new file mode 100644 index 0000000..1eee422 --- /dev/null +++ b/.agents/skills/firebase-firestore-enterprise-native-mode/references/web_sdk_usage.md @@ -0,0 +1,201 @@ +# Web SDK Usage + +This guide focuses on the **Modular Web SDK** (v9+), which is tree-shakeable and efficient. + +### Initialization + +```javascript +import { initializeApp } from "firebase/app"; +import { getFirestore } from "firebase/firestore"; + +// If running in Firebase App Hosting, you can skip Firebase Config and instead use: +// const app = initializeApp(); + +const firebaseConfig = { + // Your config options. Get the values by running 'firebase apps:sdkconfig ' +}; + +const app = initializeApp(firebaseConfig); +const db = getFirestore(app); +``` + +### Writing Data + +#### Set a Document +Creates a document if it doesn't exist, or overwrites it if it does. You can also specify a merge option to only update provided fields. + +```javascript +import { doc, setDoc } from "firebase/firestore"; + +// Create/Overwrite document with ID "LA" +await setDoc(doc(db, "cities", "LA"), { + name: "Los Angeles", + state: "CA", + country: "USA" +}); + +// To merge with existing data instead of overwriting: +await setDoc(doc(db, "cities", "LA"), { population: 3900000 }, { merge: true }); +``` + +#### Add a Document with Auto-ID +Use when you don't care about the document ID and want Firestore to automatically generate one. + +```javascript +import { collection, addDoc } from "firebase/firestore"; + +const docRef = await addDoc(collection(db, "cities"), { + name: "Tokyo", + country: "Japan" +}); +console.log("Document written with ID: ", docRef.id); +``` + +#### Update a Document +Update some fields of an existing document without overwriting the entire document. Fails if the document doesn't exist. + +```javascript +import { doc, updateDoc } from "firebase/firestore"; + +const laRef = doc(db, "cities", "LA"); + +await updateDoc(laRef, { + capital: true +}); +``` + +#### Transactions +Perform an atomic read-modify-write operation. + +```javascript +import { runTransaction, doc } from "firebase/firestore"; + +const sfDocRef = doc(db, "cities", "SF"); + +try { + await runTransaction(db, async (transaction) => { + const sfDoc = await transaction.get(sfDocRef); + if (!sfDoc.exists()) { + throw "Document does not exist!"; + } + + const newPopulation = sfDoc.data().population + 1; + transaction.update(sfDocRef, { population: newPopulation }); + }); + console.log("Transaction successfully committed!"); +} catch (e) { + console.log("Transaction failed: ", e); +} +``` + +### Reading Data + +#### Get a Single Document + +```javascript +import { doc, getDoc } from "firebase/firestore"; + +const docRef = doc(db, "cities", "SF"); +const docSnap = await getDoc(docRef); + +if (docSnap.exists()) { + console.log("Document data:", docSnap.data()); +} else { + console.log("No such document!"); +} +``` + +#### Get Multiple Documents +Fetches all documents in a query or collection once. + +```javascript +import { collection, getDocs } from "firebase/firestore"; + +const querySnapshot = await getDocs(collection(db, "cities")); +querySnapshot.forEach((doc) => { + console.log(doc.id, " => ", doc.data()); +}); +``` + +### Realtime Updates + +#### Listen to a Document or Query + +```javascript +import { doc, onSnapshot } from "firebase/firestore"; + +const unsub = onSnapshot(doc(db, "cities", "SF"), (doc) => { + console.log("Current data: ", doc.data()); +}); + +// To stop listening: +// unsub(); +``` + +### Handle Changes + +```javascript +import { collection, query, where, onSnapshot } from "firebase/firestore"; + +const q = query(collection(db, "cities"), where("state", "==", "CA")); +const unsubscribe = onSnapshot(q, (snapshot) => { + snapshot.docChanges().forEach((change) => { + if (change.type === "added") { + console.log("New city: ", change.doc.data()); + } + if (change.type === "modified") { + console.log("Modified city: ", change.doc.data()); + } + if (change.type === "removed") { + console.log("Removed city: ", change.doc.data()); + } + }); +}); +``` + +### Queries + +#### Simple and Compound Queries +Use `query()` and `where()` to combine filters safely. + +```javascript +import { collection, query, where, getDocs } from "firebase/firestore"; + +const citiesRef = collection(db, "cities"); + +// Simple equality +const q1 = query(citiesRef, where("state", "==", "CA")); + +// Compound (AND) +// Note: Requires a composite index if filtering on different fields +const q2 = query(citiesRef, where("state", "==", "CA"), where("population", ">", 1000000)); +``` + +#### Order and Limit +Sort and limit results cleanly. + +```javascript +import { orderBy, limit } from "firebase/firestore"; + +const q = query(citiesRef, orderBy("name"), limit(3)); +``` + +#### Pipeline Queries + +You can use pipeline queries to perform complex queries. + +```javascript + +const readDataPipeline = db.pipeline() + .collection("users"); + +// Execute the pipeline and handle the result +try { + const querySnapshot = await execute(readDataPipeline); + querySnapshot.results.forEach((result) => { + console.log(`${result.id} => ${result.data()}`); + }); +} catch (error) { + console.error("Error getting documents: ", error); +} +``` diff --git a/.agents/skills/firebase-firestore-standard/SKILL.md b/.agents/skills/firebase-firestore-standard/SKILL.md new file mode 100644 index 0000000..0eb6c2e --- /dev/null +++ b/.agents/skills/firebase-firestore-standard/SKILL.md @@ -0,0 +1,27 @@ +--- +name: firebase-firestore-standard +description: Comprehensive guide for Firestore Standard Edition, including provisioning, security rules, and SDK usage. Use this skill when the user needs help setting up Firestore, writing security rules, or using the Firestore SDK in their application. +compatibility: This skill is best used with the Firebase CLI, but does not require it. Firebase CLI can be accessed through `npx -y firebase-tools@latest`. +--- + +# Firestore Standard Edition + +This skill provides a complete guide for getting started with Cloud Firestore Standard Edition, including provisioning, securing, and integrating it into your application. + +## Provisioning + +To set up Cloud Firestore in your Firebase project and local environment, see [provisioning.md](references/provisioning.md). + +## Security Rules + +For guidance on writing and deploying Firestore Security Rules to protect your data, see [security_rules.md](references/security_rules.md). + +## SDK Usage + +To learn how to use Cloud Firestore in your application code, choose your platform: + +* **Web (Modular SDK)**: [web_sdk_usage.md](references/web_sdk_usage.md) + +## Indexes + +For checking index types, query support tables, and best practices, see [indexes.md](references/indexes.md). diff --git a/.agents/skills/firebase-firestore-standard/references/indexes.md b/.agents/skills/firebase-firestore-standard/references/indexes.md new file mode 100644 index 0000000..7623eb8 --- /dev/null +++ b/.agents/skills/firebase-firestore-standard/references/indexes.md @@ -0,0 +1,82 @@ +# Firestore Indexes Reference + +Indexes allow Firestore to ensure that query performance depends on the size of the result set, not the size of the database. + +## Index Types + +### Single-Field Indexes +In Standard Edition, Firestore **automatically creates** a single-field index for every field in a document (and subfields in maps). +* **Support**: Simple equality queries (`==`) and single-field range/sort queries (`<`, `<=`, `orderBy`). +* **Behavior**: You generally don't need to manage these unless you want to *exempt* a field. + +### Composite Indexes +A composite index stores a sorted mapping of all documents based on an ordered list of fields. +* **Support**: Complex queries that filter or sort by **multiple fields**. +* **Creation**: These are **NOT** automatically created. You must define them manually or via the console/CLI. + +## Automatic vs. Manual Management + +### What is Automatic? +* Indexes for simple queries. +* Merging of single-field indexes for multiple equality filters (e.g., `where("state", "==", "CA").where("country", "==", "USA")`). + +### When Do I Need to Act? +If you attempt a query that requires a composite index, the SDK will throw an error containing a **direct link** to the Firebase Console to create that specific index. + +**Example Error:** +> "The query requires an index. You can create it here: https://console.firebase.google.com/project/..." + +## Query Support Examples + +| Query Type | Index Required | +| :--- | :--- | +| **Simple Equality**
`where("a", "==", 1)` | Automatic (Single-Field) | +| **Simple Range/Sort**
`where("a", ">", 1).orderBy("a")` | Automatic (Single-Field) | +| **Multiple Equality**
`where("a", "==", 1).where("b", "==", 2)` | Automatic (Merged Single-Field) | +| **Equality + Range/Sort**
`where("a", "==", 1).where("b", ">", 2)` | **Composite Index** | +| **Multiple Ranges**
`where("a", ">", 1).where("b", ">", 2)` | **Composite Index** (and technically limited query support) | +| **Array Contains + Equality**
`where("tags", "array-contains", "news").where("active", "==", true)` | **Composite Index** | + +## Best Practices & Exemptions + +You can **exempt** fields from automatic indexing to save storage or strictly enforce write limits. + +### 1. High Write Rates (Sequential Values) +* **Problem**: Indexing fields that increase sequentially (like `timestamp`) limits the write rate to ~500 writes/second per collection. +* **Solution**: If you don't query on this field, **exempt** it from simple indexing. + +### 2. Large String/Map/Array Fields +* **Problem**: Indexing limits (40k entries per doc). Indexing large blobs wastes storage. +* **Solution**: Exempt large text blobs or huge arrays if they aren't used for filtering. + +### 3. TTL Fields +* **Problem**: TTL (Time-To-Live) deletion can cause index churn. +* **Solution**: Exempt the TTL timestamp field from indexing if you don't query it. + +## Management + +### Config files +Your indexes should be defined in `firestore.indexes.json` (pointed to by `firebase.json`). + +```json +{ + "indexes": [ + { + "collectionGroup": "cities", + "queryScope": "COLLECTION", + "fields": [ + { "fieldPath": "country", "order": "ASCENDING" }, + { "fieldPath": "population", "order": "DESCENDING" } + ] + } + ], + "fieldOverrides": [] +} +``` + +### CLI Commands + +Deploy indexes only: +```bash +npx -y firebase-tools@latest deploy --only firestore:indexes +``` diff --git a/.agents/skills/firebase-firestore-standard/references/provisioning.md b/.agents/skills/firebase-firestore-standard/references/provisioning.md new file mode 100644 index 0000000..5278801 --- /dev/null +++ b/.agents/skills/firebase-firestore-standard/references/provisioning.md @@ -0,0 +1,87 @@ +# Provisioning Cloud Firestore + +## Manual Initialization + +Initialize the following firebase configuration files manually. Do not use `npx -y firebase-tools@latest init`, as it expects interactive inputs. + +1. **Create `firebase.json`**: This file configures the Firebase CLI. +2. **Create `firestore.rules`**: This file contains your security rules. +3. **Create `firestore.indexes.json`**: This file contains your index definitions. + +### 1. Create `firebase.json` + +Create a file named `firebase.json` in your project root with the following content. If this file already exists, instead append to the existing JSON: + +```json +{ + "firestore": { + "rules": "firestore.rules", + "indexes": "firestore.indexes.json" + } +} +``` + +This will use the default database with the Standard edition. To use a different database, specify the database ID and location. You can check the list of available databases using `npx -y firebase-tools@latest firestore:databases:list`. If the database does not exist, it will be created when you deploy: + +```json +{ + "firestore": { + "rules": "firestore.rules", + "indexes": "firestore.indexes.json", + "database": "my-database-id", + "location": "us-central1" + } +} +``` + +### 2. Create `firestore.rules` + +Create a file named `firestore.rules`. A good starting point (locking down the database) is: + +``` +rules_version = '2'; +service cloud.firestore { + match /databases/{database}/documents { + match /{document=**} { + allow read, write: if false; + } + } +} +``` +*See [security_rules.md](security_rules.md) for how to write actual rules.* + +### 3. Create `firestore.indexes.json` + +Create a file named `firestore.indexes.json` with an empty configuration to start: + +```json +{ + "indexes": [], + "fieldOverrides": [] +} +``` + +*See [indexes.md](indexes.md) for how to configure indexes.* + + +## Deploy rules and indexes +```bash +# To deploy all rules and indexes +npx -y firebase-tools@latest deploy --only firestore + +# To deploy just rules +npx -y firebase-tools@latest deploy --only firestore:rules + +# To deploy just indexes +npx -y firebase-tools@latest deploy --only firestore:indexes +``` + +## Local Emulation + +To run Firestore locally for development and testing: + +```bash +npx -y firebase-tools@latest emulators:start --only firestore +``` + +This starts the Firestore emulator, typically on port 8080. You can interact with it using the Emulator UI (usually at http://localhost:4000/firestore). diff --git a/.agents/skills/firebase-firestore-standard/references/security_rules.md b/.agents/skills/firebase-firestore-standard/references/security_rules.md new file mode 100644 index 0000000..fe24fad --- /dev/null +++ b/.agents/skills/firebase-firestore-standard/references/security_rules.md @@ -0,0 +1,414 @@ +## 1. Generate Firestore Rules +You are an expert Firebase Security Rules engineer with deep knowledge of Firestore security best practices. Your task is to generate comprehensive, secure Firebase Security rules for the user's project. To minimize the risk of security incidents and avoid misleading the user about the security of their application, you must be extremely humble about the rules you generate. Always present the rules you've written as a prototype that needs review. + +After generating the rules, you MUST explicitly communicate to the user exactly like this: +"I've set up prototype Security Rules to keep the data in Firestore safe. They are designed to be secure for . However, you should review and verify them before broadly sharing your app. If you'd like, I can help you harden these rules." + +### Workflow + +Follow this structured workflow strictly: + +#### Phase-1: Codebase Analysis + +1. **Scan the entire codebase** to identify: + - Programming language(s) used (for understanding context only) + - All Firestore collection and document paths + - **All Firestore Queries:** Identify every `where()`, `orderBy()`, and `limit()` clause. The security rules **MUST** allow these specific queries. + - Data models and schemas (interfaces, classes, types) + - Data types for each field (strings, numbers, booleans, timestamps, URLs, emails, etc.) + - Required vs. optional fields + - Field constraints (min/max length, format patterns, allowed values) + - CRUD operations (create, read, update, delete) + - Authentication patterns (Firebase Auth, custom tokens, anonymous) + - Access patterns and business logic rules +2. **Document your findings** in a untracked file. Refer to this file when generating the security rules. + +#### Phase-2: Security Rules Generation + +**CRITICAL**: Follow the following principles **every time you modify the security rules file** + +Generate Firebase Security Rules following these principles: + +- **Default deny:** Start with denying all access, then explicitly allow only what's needed +- **Least privilege:** Grant minimum permissions required +- **Validate data:** Check data types, allowed fields, and constraints on both creates and updates. + - **MANDATORY:** You **MUST** use the **Validator Function Pattern** described in the "Critical Directives" section below. This involves defining a specific validation function (e.g., `isValidUser`) and calling it in **BOTH** `create` and `update` rules. + - **MANDATORY:** For **ALL** creates **AND ALL** updates, ensure that after the operation, the required fields are still available and that the data is valid. +- **Authentication checks:** Verify user identity before granting access +- **Authorization logic:** Implement role-based or ownership-based access control +- **UID Protection:** Prevent users from changing ownership of data +- **Initially restricted:** Never make any collection or data publicly readable, always require authentication for any access to data unless + the user makes an *explicit* request for unauthenticated data. + +This means the first firestore.rules file you generate must never have any "allow read: true" statements. + +**Structure Requirements:** + +1. **Document assumed data models at the beginning of the rules file:** + +```javascript +// =============================================================== +// Assumed Data Model +// =============================================================== +// +// This security rules file assumes the following data structures: +// +// Collection: [name] +// Document ID: [pattern] +// Fields: +// - field1: type (required/optional, constraints) - description +// - field2: type (required/optional, constraints) - description +// [List all fields with types, constraints, and whether immutable] +// +// [Repeat for all collections] +// +// =============================================================== +``` + +2. **Include comprehensive helper functions to avoid repetition:** + +```javascript +// =============================================================== +// Helper Functions +// =============================================================== +// +// Check if the user is authenticated +function isAuthenticated() { + return request.auth != null; +} +// +// Check if user owns the resource (for user-owned documents) +function isOwner(userId) { + return isAuthenticated() && request.auth.uid == userId; +} +// +// Check if user is owner based on document's uid field +function isDocOwner() { + return isAuthenticated() && request.auth.uid == resource.data.uid; +} +// +// Verify UID hasn't been tampered with on create +function uidUnchanged() { + return !('uid' in request.resource.data) || + request.resource.data.uid == request.auth.uid; +} +// +// Ensure uid field is not modified on update +function uidNotModified() { + return !('uid' in request.resource.data) || + request.resource.data.uid == resource.data.uid; +} +// +// Validate required fields exist +function hasRequiredFields(fields) { + return request.resource.data.keys().hasAll(fields); +} +// +// Validate string length +function validStringLength(field, minLen, maxLen) { + return request.resource.data[field] is string && + request.resource.data[field].size() >= minLen && + request.resource.data[field].size() <= maxLen; +} +// +// Validate URL format (must start with https:// or http://) +function isValidUrl(url) { + return url is string && + (url.matches("^https://.*") || url.matches("^http://.*")); +} +// +// Validate email format +function isValidEmail(email) { + return email is string && + email.matches("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$"); +} + +// +// Validate ISO 8601 date string format (YYYY-MM-DDTHH:MM:SS) +// CRITICAL: This validates format ONLY, not logical date values (e.g., month 13). +// Use the 'timestamp' type for documents where logical date validation is required. +function isValidDateString(dateStr) { + return dateStr is string && + dateStr.matches("^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}.*Z?$"); +} + +// +// Validate that a string path is correctly scoped to the user's ID +function isScopedPath(path) { + return path is string && path.matches("^users/" + request.auth.uid + "/.*"); +} +// +// Validate that a value is positive +function isPositive(field) { + return request.resource.data[field] is number && request.resource.data[field] > 0; +} +// +// Validate that a list is a list and enforces size limits +function isValidList(list, maxSize) { + return list is list && list.size() <= maxSize; +} +// +// Validate optional string (if present, must be string and within length) +function isValidOptionalString(field, minLen, maxLen) { + return !('field' in request.resource.data) || + (request.resource.data[field] is string && + request.resource.data[field].size() >= minLen && + request.resource.data[field].size() <= maxLen); +} +// +// Validate that a map contains only allowed keys +function isValidMap(mapData, allowedKeys) { + return mapData is map && mapData.keys().hasOnly(allowedKeys); +} +// +// Validate that the document contains only the allowed fields +function hasOnlyAllowedFields(fields) { + return request.resource.data.keys().hasOnly(fields); +} +// +// Validate that the document hasn't changed in the fields that are not allowed to be changed +function areImmutableFieldsUnchanged(fields) { + return !request.resource.data.diff(resource.data).affectedKeys().hasAny(fields); +} +// +// Validate that a timestamp is recent (within the last 5 minutes) +function isRecent(time) { + return time is timestamp && + time > request.time - duration.value(5, 'm') && + time <= request.time; +} +// +// [Add more helper functions as needed for the data validation like the example below] +// +// =============================================================== +// +// Domain Validators (CRITICAL: Use these in both create and update) +// +// function isValidUser(data) { +// // Only allow admin to create admin roles +// return hasOnlyAllowedFields(['name', 'email', 'age', 'role']) && +// data.name is string && data.name.size() > 0 && data.name.size() < 50 && +// data.email is string && isValidEmail(data.email) && +// data.age is number && data.age >= 18 && +// data.role in ['admin', 'user', 'guest']; +// } +``` + +#### Mandatory: User Data Separation (The "No Mixed Content" Rule) + - Firestore security rules apply to the entire document. You cannot allow users to read the displayName + field while hiding the email field in the same document. + - If a collection (e.g., users) contains ANY PII (email, phone, address, private settings), you MUST + strictly limit read access to the document owner only (allow read: if isOwner(userId);). + - If the application requires public profiles (e.g., showing user names/avatars on posts): + - 1. Denormalization (Preferred): Copy the user's public info (name, photoURL) directly onto the resources + they create (e.g., store authorName and authorPhoto inside the posts document). + - 2. Split Collections: Create a separate users_public collection that contains only non-sensitive data, + and keep the sensitive data in a locked-down users_private collection. + - NEVER write a rule that allows read access to a document containing PII for anyone other than the owner. + +#### **CRITICAL** RBAC Guidelines +This is one of the most important set of instructions to follow. Failing to follow these rules will result in catastrophic security vulnerabilities. + +- **NEVER** allow users to create their own privileged roles. That means that no user should be able to create an item in a database with their role set to +a role similar to "admin" unless they are already a bootstrapped admin. +- **NEVER** allow users to update their own roles or permissions. +- **NEVER** allow users to grant themselves access to other users' data. +- **NEVER** allow users to bypass the role hierarchy. +- **ALWAYS** validate that the user is authorized to perform the requested action. +- **ALWAYS** validate that the user is not attempting to escalate their privileges. +- **ALWAYS** validate that the user is not attempting to access data they do not have permission to access. + +Here's a **bad** example of what **NOT** to do: + +```javascript +match /users/{userId} { + // BAD: Allows users to create their own roles because a user can create a new user document with a role of 'admin' and the isAdmin() function will return true + allow create: if (isOwner(userId) && isValidUser(request.resource.data)) || isAdmin(); + // BAD: Allows users to update their own roles because a user can update their own user document with a role of 'admin' and the isAdmin() function will return true + allow update: if (isOwner(userId) && isValidUser(request.resource.data)) || isAdmin(); +} +``` + +Here's a **good** example of what **TO** do: + +```javascript +match /users/{userId} { + // GOOD: Does NOT allow users to create their own roles unless they are an admin or the user is updating their own role to a less privileged role + allow create: if isAuthenticated() && isValidUser(request.resource.data) && ((isOwner(userId) && request.resource.data.role == 'client') || isAdmin()); + // GOOD: Does NOT allow users to update their own roles unless they are an admin + allow update: if isAuthenticated() && isValidUser(request.resource.data) && ((isOwner(userId) && request.resource.data.role == resource.data.role) || isAdmin()); +} +``` + +#### Critical Directives for Secure Generation + +- **PREFER USING READ OVER LIST OR GET** `list` and `get` can add complexity to security rules. Prefer using `read` over them. +- **Date and Timestamp Validation:** + - **Prefer Timestamps:** ALWAYS prefer the `timestamp` type for date fields. Firestore automatically ensures they are logically valid dates. + - **String Date Risks:** If using strings for dates (e.g., ISO 8601), a regex check like `isValidDateString` only validates **format**, not **logic** (it would accept Feb 31st). + - **Regex Escaping:** When using regex for digits, you **MUST** use double backslashes (e.g., `\\\\d`) in the rules string. Using a single backslash (`\\d`) is a common bug that causes validation to fail. +- **Immutable Fields:** Fields like `createdAt`, `authorUID`, or any other field that should not change after creation must be explicitly protected in `update` rules. (e.g., `request.resource.data.createdAt == resource.data.createdAt`). **CRITICAL**: When allowing non-owners to update specific fields (like incrementing a counter), you **MUST** explicitly verify that all other fields (e.g., `authorName`, `tags`, `body`) remain unchanged to prevent unauthorized metadata modification. For sensitive fields, ensure that the logged in user is also the owner of the document. +- **Identity Integrity:** When storing denormalized user identity (e.g. `authorName`, `authorPhoto`), you **MUST** validate this data. + - **Prefer Auth Token:** If possible, check if `request.resource.data.authorName == request.auth.token.name`. + - **Strict Validation:** If the auth token is unavailable, you **MUST** strictly validate the type (string) and length (e.g. < 50 chars) to prevent spoofing with massive or malicious payloads. + - **Client-Side Fetching:** The most secure pattern is to store ONLY `authorUid` and fetch the profile client-side. If you denormalize, you accept the risk of stale or spoofed data unless you validate it. +- **Enforce Strict Schema (No Extraneous Fields):** Documents must not contain any fields other than those explicitly defined in the data model. This prevents users from adding arbitrary data. +- **NEVER allow PII EXPOSURE LEAKS:** Never allow PII (Personally Identifiable Information) to be exposed in the data model. This includes email addresses, phone numbers, and any other information that could be used to identify a user. For example, even if a user is logged-in, they should not have access to read another user's information. +- **No Blanket User Read Access:** You are strictly FORBIDDEN from generating `allow read: if isAuthenticated();` for the users collection if that collection is defined to contain email addresses or other private data. +- **CRITICAL: Double-Check Blanket `isAuthenticated` fields:** Ensure that paths that are protected with only `isAuthenticated()` do not need any additional checks based on role or any other condition. +- **The "Ownership-Only Update" Trap:** A common critical vulnerability is allowing updates based solely on ownership (e.g., `allow update: if isOwner(resource.data.uid);`). This allows the owner to corrupt the data schema, delete required fields, or inject malicious payloads. You **MUST** always combine ownership checks with data validation (e.g., `allow update: if isOwner(...) && isValidEntity(...);`) **AND** validate that self-escalation is not possible. + +- **Deep Array Inspection:** It is insufficient to check if a field `is list`. You **MUST** validate the contents of the array (e.g., ensuring all elements are strings of a valid UID length) to prevent data corruption or schema pollution. For example, a `tags` array must verify that every item is a string AND that each string is within a reasonable length (e.g., < 20 chars). +- **Permission-Field Lockdown:** Fields that control access (e.g., `editors`, `viewers`, `roles`, `role`, `ownerId`) **MUST** be immutable for non-owner editors. In `update` rules, use `fieldUnchanged()` for these fields unless the `request.auth.uid` matches the document's original owner/creator. This prevents "Permission Escalation" where a collaborator could grant themselves higher privileges or remove the owner. + + +### Advanced Validation for Business Logic + + Secure rules must enforce the application's business logic. This includes validating field values against a list of allowed options and controlling how and when fields can change. + + #### 1. Enforce Enum Values + + If a field should only contain specific values (e.g., a status), validate against a list. + + **Example:** + + ```javascript + // A 'task' document's status can only be one of three values + function isValidStatus() { + let validStatuses = ['pending', 'in-progress', 'completed']; + return request.resource.data.status in validStatuses; + } + + allow create: if isValidStatus() && ... + ``` + + #### 2. Validate State Transitions + + For `update` operations, you **MUST** validate that a field is changing from a valid previous state to a valid new state. This prevents users from bypassing workflows (e.g., marking a task as 'completed' from 'archived'). + + **Example:** + + ```javascript + // A task can only be marked 'completed' if it was 'in-progress' + function validStatusTransition() { + let previousStatus = resource.data.status; + let newStatus = request.resource.data.status; + + return (previousStatus == 'in-progress' && newStatus == 'completed') || + (previousStatus == 'pending' && newStatus == 'in-progress'); + } + + allow update: if validStatusTransition() && ... + ``` + +#### 3. Strict Path and Relationship Scoping + +For any field that references another resource (like an image path or a parent document ID), you **MUST** ensure it is correctly scoped to the user or valid within the context. + +**Example:** + +```javascript +// Ensure image path is within the user's own storage folder +allow create: if isScopedPath(request.resource.data.imageBucket) && ... +``` + +#### 4. Secure Counter Updates + +When allowing users to update a counter (like `voteCount` or `answerCount`), you **MUST** ensure: +1. **Atomic Increments:** The field is only changing by exactly +1 or -1. +2. **Isolation:** **NO OTHER FIELDS** are being modified. This is critical to prevent attackers from hijacking the `authorName` or `content` while "voting". +3. **Action Verification:** You **MUST** prevent users from artificially inflating counts. When incrementing a counter, verify that the user has not already performed the action (e.g., by checking for the existence of a 'like' document) and is not looping updates. + * **CRITICAL:** Relying solely on `!exists(likeDoc)` is insufficient because a malicious user can skip creating the document and loop the increment. + * **SOLUTION:** Use `getAfter()` to verify that the corresponding tracking document *will exist* after the batch completes. + +**Example:** + +```javascript +function isValidCounterUpdate(docId) { + // Allow update only if 'voteCount' is the ONLY field changing + return request.resource.data.diff(resource.data).affectedKeys().hasOnly(['voteCount']) && + // And the change is exactly +1 or -1 + math.abs(request.resource.data.voteCount - resource.data.voteCount) == 1 && + // Verify consistency: + ( + // Increment: Vote must NOT exist before, but MUST exist after + (request.resource.data.voteCount > resource.data.voteCount && + !exists(/databases/$(database)/documents/votes/$(request.auth.uid + '_' + docId)) && + getAfter(/databases/$(database)/documents/votes/$(request.auth.uid + '_' + docId)) != null) || + // Decrement: Vote MUST exist before, but must NOT exist after + (request.resource.data.voteCount < resource.data.voteCount && + exists(/databases/$(database)/documents/votes/$(request.auth.uid + '_' + docId)) && + getAfter(/databases/$(database)/documents/votes/$(request.auth.uid + '_' + docId)) == null) + ); +} + +allow update: if isValidCounterUpdate(docId) && ... +``` + +#### 5. **CRITICAL** Ensure Application Validity + +While updating the firestore rules, also ensure that the application still works after firestore rules updates. + +3. **For each collection, implement explicit data validation:** + +- Type Checking: 'field is string', 'field is number', 'field is bool', 'field is timestamp' +- Required fields validation using 'hasRequiredFields()' +- **Enforce Size Limits:** For **EVERY** string, list, and map field, you **MUST** enforce realistic size limits (e.g., `text.size() < 1000`, `tags.size() < 20`). **Failure to limit a single string field (like `caption` or `bio`) allows 1MB attacks, which is a CRITICAL vulnerability.** +- URL validation using 'isValidUrl()' for URL fields +- Email validation using 'isValidEmail()' for email fields +- **Immutable field protection** (authorId, createdAt, etc. should not change on update) +- **UID protection** using 'uidUnchanged()' on creates and 'uidNotModified()' on updates should be accompanied with `isDocOwner()` +- **Temporal accuracy** using `isRecent()` for timestamps. +- **Range validation** using `isPositive()` or similar for numbers. +- **Path scoping** using `isScopedPath()` for storage paths. + +Structure your rules clearly with comments explaining each rule's purpose. + +#### Phase-3: Devil's Advocate Attack + +**Critical step:** Systematically attempt to break your own rules using the following attack vectors. You MUST document the outcome of each attempt. + +1. **Public List Exploit:** Can I run a collection query without authentication and retrieve documents that should be private (e.g., where `visible == false`)? +2. **Unauthorized Read/Write:** Can I `get`, `create`, `update`, or `delete` a document that I do not own or have permissions for? +3. **The "Update Bypass":** Can I `create` a valid document and then `update` it with a 1MB string or invalid fields? (Tests if validation logic is missing from `update`). +4. **Ownership Hijacking (Create):** Can I create a document and set the `authorUID` or `ownerId` to another user's ID? +5. **Ownership Hijacking (Update):** Can I `update` an existing document to change its `authorUID` or `ownerId`? +6. **Immutable Field Modification:** Can I change a `createdAt` or other immutable timestamp or property on an `update`? +7. **Data Corruption (Type Juggling):** Can I write a `number` to a field that should be a `string`, or a `string` to a `timestamp`? +8. **Validation Bypass (Create vs. Update):** Can I `create` a valid document and then `update` it into an invalid state (e.g., remove a required field, write a string that's too long)? +9. **Resource Exhaustion / DoS:** Can I write an enormous string (e.g., 1MB) to any field that accepts a string or a massive array to a list field? Every string field (e.g., `bio`, `url`, `name`) MUST have a `.size()` check. If any are missing, it's a "Resource Exhaustion/DoS" risk. +10. **Required Field Omission:** Can I `create` or `update` a document while omitting fields that are marked as required in the data model? +11. **Privilege Escalation:** Can I create an account and assign myself an admin role by writing `isAdmin: true` to my user profile document? (Tests reliance on document data vs. custom claims). +12. **Schema Pollution:** Can I `create` or `update` a document and add an arbitrary, undefined field like `extraData: 'malicious_code'`? (Tests for strict schema enforcement). +13. **Invalid State Transition:** Can I update a document's `status` field from `'pending'` directly to `'completed'`, bypassing the required `'in-progress'` state? (Tests business logic enforcement). +14. **Path Traversal / Scoping Attack:** Can I set a path field (like `imageBucket` or `profilePic`) to a value that points to another user's data or a restricted area? (Tests for regex path scoping). +15. **Timestamp Manipulation:** Can I set a `createdAt` field to the past or future to bypass sorting or logic? (Tests for `request.time` validation). +16. **Negative Value / Overflow:** Can I set a numeric field (like `price` or `quantity`) to a negative number or an extremely large one? (Tests for range validation). +17. **The "Mixed Content" Leak:** Create a second user. Can User B read User A's users document? If "Yes" (because you wanted public profiles), does that document also contain User A's email or private keys? If both are true, the rules are insecure. +18. **Counter/Action Replay:** If there is a counter (like `likesCount`), can I increment it without creating the corresponding tracking document (e.g., inside `likes/{userId}`)? Can I increment it twice? (Tests for `getAfter()` consistency checks). +19. **Orphaned Subcollection Access:** Can I read/write to a subcollection (e.g., `users/123/posts/456`) if the parent document (`users/123`) does not exist? (Tests for parent existence checks). +20. **Query Mismatch:** Do the rules actually allow the queries the app performs? (e.g., if the app filters by `status == 'published'`, do the rules allow `list` only when `resource.data.status == 'published'`?) +21. **Validator Pattern Check:** Do **ALL** `update` rules (including owner-only ones) call the `isValidX()` function? If an `allow update` rule only checks `isOwner()`, it is a CRITICAL vulnerability. + +Document each attack attempt and whether it succeeded. If ANY attack succeeds: + +- Fix the security hole +- Regenerate the rules +- **Repeat Phase-3** until no attacks succeed + +#### Phase-4: Syntactic Validation + +Once devil's advocate testing passes, repeat until rules pass validation. + +**After all phases are complete, create or update the `firestore.rules` file.** + +### Critical Constraints +1. **Never skip the devil's advocate phase** - this is your primary security validation +2. **MUST include helper functions** for common operations ('isAuthenticated', 'isOwner', 'uidUnchanged', 'uidNotModified') AND domain validators ('isValidUser', etc.) +3. **MUST document assumed data models** at the beginning of the rules file +4. **Always validate the rules syntax** using 'firebase deploy --only firestore:rules --dry-run' or a similar tool before outputting the final file. +5. **Provide complete, runnable code** - no placeholders or TODOs +6. **Document all assumptions** about data structure or access patterns +7. **Always run the devil's advocate attack** after any modification of the rules. +8. **Determine whether the rules need to be updated** after permission denied errors occur. +9. **Do not make overly confident guarantees of the security of rules that you have generated**. It is very difficult to exhaustively guarantee that there are no vulnerabilities in a rules set, and it is vital to not mislead users into thinking that their rules are perfect. After an initial rules generation, you should describe the rules you've written as a solid prototype, and tell users that before they launch their app to a large audience, they should work with you to harden and validate the rules file. Be clear that users should carefully review rules to ensure security. diff --git a/.agents/skills/firebase-firestore-standard/references/web_sdk_usage.md b/.agents/skills/firebase-firestore-standard/references/web_sdk_usage.md new file mode 100644 index 0000000..3d85134 --- /dev/null +++ b/.agents/skills/firebase-firestore-standard/references/web_sdk_usage.md @@ -0,0 +1,183 @@ +# Firestore Web SDK Usage Guide + +This guide focuses on the **Modular Web SDK** (v9+), which is tree-shakeable and efficient. + +## Initialization + +```javascript +import { initializeApp } from "firebase/app"; +import { getFirestore } from "firebase/firestore"; + +// If running in Firebase App Hosting, you can skip Firebase Config and instead use: +// const app = initializeApp(); + +const firebaseConfig = { + // Your config options. Get the values by running 'npx -y firebase-tools@latest apps:sdkconfig ' +}; + +const app = initializeApp(firebaseConfig); +const db = getFirestore(app); + +``` + +## Writing Data + +### Set a Document (`setDoc`) +Creates a document if it doesn't exist, or overwrites it if it does. + +```javascript +import { doc, setDoc } from "firebase/firestore"; + +// Create/Overwrite document with ID "LA" +await setDoc(doc(db, "cities", "LA"), { + name: "Los Angeles", + state: "CA", + country: "USA" +}); + +// To merge with existing data instead of overwriting: +await setDoc(doc(db, "cities", "LA"), { population: 3900000 }, { merge: true }); +``` + +### Add a Document with Auto-ID (`addDoc`) +Use when you don't care about the document ID. + +```javascript +import { collection, addDoc } from "firebase/firestore"; + +const docRef = await addDoc(collection(db, "cities"), { + name: "Tokyo", + country: "Japan" +}); +console.log("Document written with ID: ", docRef.id); +``` + +### Update a Document (`updateDoc`) +Update some fields of an existing document without overwriting the entire document. Fails if the document doesn't exist. + +```javascript +import { doc, updateDoc } from "firebase/firestore"; + +const laRef = doc(db, "cities", "LA"); + +await updateDoc(laRef, { + capital: true +}); +``` + +### Transactions +Perform an atomic read-modify-write operation. + +```javascript +import { runTransaction, doc } from "firebase/firestore"; + +const sfDocRef = doc(db, "cities", "SF"); + +try { + await runTransaction(db, async (transaction) => { + const sfDoc = await transaction.get(sfDocRef); + if (!sfDoc.exists()) { + throw "Document does not exist!"; + } + + const newPopulation = sfDoc.data().population + 1; + transaction.update(sfDocRef, { population: newPopulation }); + }); + console.log("Transaction successfully committed!"); +} catch (e) { + console.log("Transaction failed: ", e); +} +``` + +## Reading Data + +### Get a Single Document (`getDoc`) + +```javascript +import { doc, getDoc } from "firebase/firestore"; + +const docRef = doc(db, "cities", "SF"); +const docSnap = await getDoc(docRef); + +if (docSnap.exists()) { + console.log("Document data:", docSnap.data()); +} else { + console.log("No such document!"); +} +``` + +### Get Multiple Documents (`getDocs`) +Fetches all documents in a query or collection once. + +```javascript +import { collection, getDocs } from "firebase/firestore"; + +const querySnapshot = await getDocs(collection(db, "cities")); +querySnapshot.forEach((doc) => { + // doc.data() is never undefined for query doc snapshots + console.log(doc.id, " => ", doc.data()); +}); +``` + +## Realtime Updates + +### Listen to a Document/Query (`onSnapshot`) + +```javascript +import { doc, onSnapshot } from "firebase/firestore"; + +const unsub = onSnapshot(doc(db, "cities", "SF"), (doc) => { + console.log("Current data: ", doc.data()); +}); + +// Stop listening +// unsub(); +``` + +### Handle Changes (Added/Modified/Removed) + +```javascript +import { collection, query, where, onSnapshot } from "firebase/firestore"; + +const q = query(collection(db, "cities"), where("state", "==", "CA")); +const unsubscribe = onSnapshot(q, (snapshot) => { + snapshot.docChanges().forEach((change) => { + if (change.type === "added") { + console.log("New city: ", change.doc.data()); + } + if (change.type === "modified") { + console.log("Modified city: ", change.doc.data()); + } + if (change.type === "removed") { + console.log("Removed city: ", change.doc.data()); + } + }); +}); +``` + +## Queries + +### Simple and Compound Queries +Use `query()` to combine filters. + +```javascript +import { collection, query, where, getDocs } from "firebase/firestore"; + +const citiesRef = collection(db, "cities"); + +// Simple equality +const q1 = query(citiesRef, where("state", "==", "CA")); + +// Compound (AND) +// Note: Requires an index if filtering on different fields +const q2 = query(citiesRef, where("state", "==", "CA"), where("population", ">", 1000000)); +``` + +### Order and Limit +Sort and limit results. + +```javascript +import { orderBy, limit } from "firebase/firestore"; + +const q = query(citiesRef, orderBy("name"), limit(3)); +``` diff --git a/.agents/skills/firebase-hosting-basics/SKILL.md b/.agents/skills/firebase-hosting-basics/SKILL.md new file mode 100644 index 0000000..a83ac28 --- /dev/null +++ b/.agents/skills/firebase-hosting-basics/SKILL.md @@ -0,0 +1,46 @@ +--- +name: firebase-hosting-basics +description: Skill for working with Firebase Hosting (Classic). Use this when you want to deploy static web apps, Single Page Apps (SPAs), or simple microservices. Do NOT use for Firebase App Hosting. +--- + +# hosting-basics + +This skill provides instructions and references for working with Firebase Hosting, a fast and secure hosting service for your web app, static and dynamic content, and microservices. + +## Overview + +Firebase Hosting provides production-grade web content hosting for developers. With a single command, you can deploy web apps and serve both static and dynamic content to a global CDN (content delivery network). + +**Key Features:** +- **Fast Content Delivery:** Files are cached on SSDs at CDN edges around the world. +- **Secure by Default:** Zero-configuration SSL is built-in. +- **Preview Channels:** View and test changes on temporary preview URLs before deploying live. +- **GitHub Integration:** Automate previews and deploys with GitHub Actions. +- **Dynamic Content:** Serve dynamic content and microservices using Cloud Functions or Cloud Run. + +## Hosting vs App Hosting + +**Choose Firebase Hosting if:** +- You are deploying a static site (HTML/CSS/JS). +- You are deploying a simple SPA (React, Vue, etc. without SSR). +- You want full control over the build and deploy process via CLI. + +**Choose Firebase App Hosting if:** +- You are using a supported full-stack framework like Next.js or Angular. +- You need Server-Side Rendering (SSR) or ISR. +- You want an automated "git push to deploy" workflow with zero configuration. + +## Instructions + +### 1. Configuration (`firebase.json`) +For details on configuring Hosting behavior, including public directories, redirects, rewrites, and headers, see [configuration.md](references/configuration.md). + +### 2. Deploying +For instructions on deploying your site, using preview channels, and managing releases, see [deploying.md](references/deploying.md). + +### 3. Emulation +To test your app locally: +```bash +npx -y firebase-tools@latest emulators:start --only hosting +``` +This serves your app at `http://localhost:5000` by default. diff --git a/.agents/skills/firebase-hosting-basics/references/configuration.md b/.agents/skills/firebase-hosting-basics/references/configuration.md new file mode 100644 index 0000000..adb9050 --- /dev/null +++ b/.agents/skills/firebase-hosting-basics/references/configuration.md @@ -0,0 +1,101 @@ +# Hosting Configuration (`firebase.json`) + +The `hosting` section of `firebase.json` configures how your site is deployed and served. + +## Key Attributes + +### `public` (Required) +Specifies the directory to deploy to Firebase Hosting. +```json +"hosting": { + "public": "public" +} +``` + +### `ignore` (Optional) +Files to ignore on deploy. Uses glob patterns (like `.gitignore`). +**Default ignores:** `firebase.json`, `**/.*`, `**/node_modules/**` + +### `redirects` (Optional) +URL redirects to prevent broken links or shorten URLs. +```json +"redirects": [ + { + "source": "/foo", + "destination": "/bar", + "type": 301 + } +] +``` + +### `rewrites` (Optional) +Serve the same content for multiple URLs, useful for SPAs or Dynamic Content. +```json +"rewrites": [ + { + "source": "**", + "destination": "/index.html" + }, + { + "source": "/api/**", + "function": "apiFunction" + }, + { + "source": "/container/**", + "run": { + "serviceId": "helloworld", + "region": "us-central1" + } + } +] +``` + +### `headers` (Optional) +Custom response headers. +```json +"headers": [ + { + "source": "**/*.@(eot|otf|ttf|ttc|woff|font.css)", + "headers": [ + { + "key": "Access-Control-Allow-Origin", + "value": "*" + } + ] + } +] +``` + +### `cleanUrls` (Optional) +If `true`, drops `.html` extension from URLs. +```json +"cleanUrls": true +``` + +### `trailingSlash` (Optional) +Controls trailing slashes in static content URLs. +- `true`: Adds trailing slash. +- `false`: Removes trailing slash. + +## Full Example + +```json +{ + "hosting": { + "public": "dist", + "ignore": [ + "firebase.json", + "**/.*", + "**/node_modules/**" + ], + "rewrites": [ + { + "source": "**", + "destination": "/index.html" + } + ], + "cleanUrls": true, + "trailingSlash": false + } +} +``` diff --git a/.agents/skills/firebase-hosting-basics/references/deploying.md b/.agents/skills/firebase-hosting-basics/references/deploying.md new file mode 100644 index 0000000..df26c5e --- /dev/null +++ b/.agents/skills/firebase-hosting-basics/references/deploying.md @@ -0,0 +1,39 @@ +# Deploying to Firebase Hosting + +## Standard Deployment +To deploy your Hosting content and configuration to your live site: + +```bash +npx -y firebase-tools@latest deploy --only hosting +``` + +This deploys to your default sites (`PROJECT_ID.web.app` and `PROJECT_ID.firebaseapp.com`). + +## Preview Channels +Preview channels allow you to test changes on a temporary URL before going live. + +### Deploy to a Preview Channel +```bash +npx -y firebase-tools@latest hosting:channel:deploy CHANNEL_ID +``` +Replace `CHANNEL_ID` with a name (e.g., `feature-beta`). +This returns a preview URL like `PROJECT_ID--CHANNEL_ID-RANDOM_HASH.web.app`. + +### Expiration +Channels expire after 7 days by default. To set a different expiration: +```bash +npx -y firebase-tools@latest hosting:channel:deploy CHANNEL_ID --expires 1d +``` + +## Cloning to Live +You can promote a version from a preview channel to your live channel without rebuilding. + +```bash +npx -y firebase-tools@latest hosting:clone SOURCE_SITE_ID:SOURCE_CHANNEL_ID TARGET_SITE_ID:live +``` + +**Example:** +Clone the `feature-beta` channel on your default site to live: +```bash +npx -y firebase-tools@latest hosting:clone my-project:feature-beta my-project:live +``` diff --git a/.agents/skills/firebase-security-rules-auditor/SKILL.md b/.agents/skills/firebase-security-rules-auditor/SKILL.md new file mode 100644 index 0000000..315e0e1 --- /dev/null +++ b/.agents/skills/firebase-security-rules-auditor/SKILL.md @@ -0,0 +1,45 @@ +--- +name: firebase-security-rules-auditor +description: A skill to evaluate how secure Firestore security rules are. Use this when Firestore security rules are updated to ensure that the generated rules are extremely secure and robust. +--- + +# Overview +This skill acts as an auditor for Firebase Security Rules, evaluating them against a rigorous set of criteria to ensure they are secure, robust, and correctly implemented. + +# Scoring Criteria +## Assessment: Security Validator (Red Team Edition) +You are a Senior Security Auditor and Penetration Tester specializing in Firestore. Your goal is to find "the hole in the wall." Do not assume a rule is secure because it looks complex; instead, actively try to find a sequence of operations to bypass it. + +### Mandatory Audit Checklist: +1. **The Update Bypass:** Compare 'create' and 'update' rules. Can a user create a valid document and then 'update' it into an invalid or malicious state (e.g., changing their role, bypassing size limits, or corrupting data types)? +2. **Authority Source:** Does the security rely on user-provided data (request.resource.data) for sensitive fields like 'role', 'isAdmin', or 'ownerId'? Carefully consider the source for that authority. +3. **Business Logic vs. Rules:** Does the rule set actually support the app's purpose? (e.g., In a collaboration app, can collaborators actually read the data? If not, the rules are "broken" or will force insecure workarounds). +4. **Storage Abuse:** Are there string length or array size limits? If not, label it as a "Resource Exhaustion/DoS" risk. +5. **Type Safety:** Are fields checked with 'is string', 'is int', or 'is timestamp'? +6. **Field-Level vs. Identity-Level Security:** Be careful with rules that use \`hasOnly()\` or \`diff()\`. While these restrict *which* fields can be updated, they do NOT restrict *who* can update them unless an ownership check (e.g., \`resource.data.uid == request.auth.uid\`) is also present. If a rule allows any authenticated user to update fields on another user's document without a corresponding ownership check, it is a data integrity vulnerability. + +### Admin Bootstrapping & Privileges: +The admin bootstrapping process is limited in this app. If the rules use a single hardcoded admin email (e.g., checking request.auth.token.email == 'admin@example.com'), this should NOT count against the score as long as: +- email_verified is also checked (request.auth.token.email_verified == true). +- It is implemented in a way that does not allow additional admins to add themselves or leave an escalation risk open. + +### Scoring Criteria (1-5): +- **1 (Critical):** Unauthorized data access (leaks), privilege escalation, or total validation bypass. +- **2 (Major):** Broken business logic, self-assigned roles, bypass of controls. +- **3 (Moderate):** PII exposure (e.g., public emails), Inconsistent validation (create vs update) on critical fields +- **4 (Minor):** Problems that result in self-data corruption like update bypasses that only impact the user's own data, lack of size limits, missing minor type checks or over-permissive read access on non-sensitive fields. +- **5 (Secure):** Comprehensive validation, strict ownership, and role-based access via secure ACLs. + +Return your assessment in JSON format using the following structure: +{ + "score": 1-5, + "summary": "overall assessment", + "findings": [ + { + "check": "checklist item", + "severity": "critical|major|moderate|minor", + "issue": "description", + "recommendation": "fix" + } + ] +} \ No newline at end of file diff --git a/.firebaserc b/.firebaserc new file mode 100644 index 0000000..467b4a7 --- /dev/null +++ b/.firebaserc @@ -0,0 +1,5 @@ +{ + "projects": { + "default": "iiit-insider" + } +} diff --git a/.github/workflows/firebase-hosting-merge.yml b/.github/workflows/firebase-hosting-merge.yml new file mode 100644 index 0000000..ce5e56b --- /dev/null +++ b/.github/workflows/firebase-hosting-merge.yml @@ -0,0 +1,20 @@ +# This file was auto-generated by the Firebase CLI +# https://github.com/firebase/firebase-tools + +name: Deploy to Firebase Hosting on merge +on: + push: + branches: + - main +jobs: + build_and_deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - run: npm ci && npm run build + - uses: FirebaseExtended/action-hosting-deploy@v0 + with: + repoToken: ${{ secrets.GITHUB_TOKEN }} + firebaseServiceAccount: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_IIIT_INSIDER }} + channelId: live + projectId: iiit-insider diff --git a/.github/workflows/firebase-hosting-pull-request.yml b/.github/workflows/firebase-hosting-pull-request.yml new file mode 100644 index 0000000..808184c --- /dev/null +++ b/.github/workflows/firebase-hosting-pull-request.yml @@ -0,0 +1,21 @@ +# This file was auto-generated by the Firebase CLI +# https://github.com/firebase/firebase-tools + +name: Deploy to Firebase Hosting on PR +on: pull_request +permissions: + checks: write + contents: read + pull-requests: write +jobs: + build_and_preview: + if: ${{ github.event.pull_request.head.repo.full_name == github.repository }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - run: npm ci && npm run build + - uses: FirebaseExtended/action-hosting-deploy@v0 + with: + repoToken: ${{ secrets.GITHUB_TOKEN }} + firebaseServiceAccount: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_IIIT_INSIDER }} + projectId: iiit-insider diff --git a/.gitignore b/.gitignore index ced5a8b..3eb889f 100644 --- a/.gitignore +++ b/.gitignore @@ -7,19 +7,55 @@ yarn-error.log* pnpm-debug.log* lerna-debug.log* -node_modules -public -dist -dist-ssr +# Dependencies +node_modules/ + +# Frontend build output +dist/ +dist-ssr/ *.local +# Backend build output +backend/target/ +target/ + +# Environment files and local secrets +.env +.env.* +!.env.example +backend/.env +backend/.env.* +!backend/.env.example + +# Firebase and service account secrets +.firebase/ +firebase-debug.log +firestore-debug.log +ui-debug.log +firebase-service-account.json +*firebase-service-account*.json +*firebase-adminsdk*.json +backend/firebase-service-account.json +backend/*firebase-service-account*.json +backend/*firebase-adminsdk*.json +backend/src/main/resources/firebase-service-account.json + +# Java/Maven local files +.mvn/timing.properties +*.class +*.jar +*.war + +# OS files +.DS_Store +Thumbs.db + # Editor directories and files .vscode/* !.vscode/extensions.json -.idea -.DS_Store +.idea/ *.suo *.ntvs* *.njsproj *.sln -*.sw? \ No newline at end of file +*.sw? diff --git a/README.md b/README.md index cf4013c..c468204 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,18 @@ -# React + Vite - -This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. - -Currently, two official plugins are available: - -- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh -- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh - -## React Compiler - -The React Compiler is enabled on this template. See [this documentation](https://react.dev/learn/react-compiler) for more information. - -Note: This will impact Vite dev & build performances. - -## Expanding the ESLint configuration - -If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project. +# React + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh + +## React Compiler + +The React Compiler is enabled on this template. See [this documentation](https://react.dev/learn/react-compiler) for more information. + +Note: This will impact Vite dev & build performances. + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project. diff --git a/SETUP.md b/SETUP.md new file mode 100644 index 0000000..d401ceb --- /dev/null +++ b/SETUP.md @@ -0,0 +1,189 @@ +# IIIT Insider - Complete Setup Guide + +## Project Structure + +``` +IIIT-Insider/ +├── backend/ # Spring Boot Backend (Java 21) +│ ├── src/ +│ │ └── main/ +│ │ ├── java/com/iiitinsider/ +│ │ │ ├── controller/ # REST API endpoints +│ │ │ ├── service/ # Business logic +│ │ │ ├── model/ # JPA entities +│ │ │ ├── repository/ # Database repositories +│ │ │ └── config/ # Security & JWT config +│ │ └── resources/ +│ │ ├── application.properties +│ │ └── schema.sql +│ └── pom.xml +├── src/ # React Frontend (Vite) +│ ├── Components/ +│ │ ├── NotificationButton.jsx +│ │ └── SocialShare.jsx +│ ├── Pages/ +│ │ └── Register.jsx +│ └── services/ +│ ├── api.js +│ └── firebase.js +├── nginx/ +│ └── nginx.conf # Nginx reverse proxy config +└── public/ + └── firebase-messaging-sw.js +``` + +## Quick Start + +### 1. Backend Setup (Spring Boot + Tomcat + MySQL) + +```bash +# Install Java 21 +# Windows: Download from https://adoptium.net/ +# Linux: sudo apt install openjdk-21-jdk + +# Install MySQL +# Windows: Download from https://dev.mysql.com/downloads/ +# Linux: sudo apt install mysql-server + +# Create database +mysql -u root -p +CREATE DATABASE iiit_insider; +EXIT; + +# Configure database credentials +# Edit: backend/src/main/resources/application.properties +# Update: spring.datasource.password=your_password + +# Build and run backend +cd backend +mvn spring-boot:run +# Backend runs on http://localhost:8080/api +``` + +### 2. Frontend Setup (React + Vite) + +```bash +cd IIIT-Insider +npm install +npm run dev +# Frontend runs on http://localhost:5173 +``` + +### 3. Firebase Setup (For Push Notifications) + +1. Go to [Firebase Console](https://console.firebase.google.com/) +2. Create a new project +3. Add a web app to your project +4. Copy the Firebase config +5. Update these files: + - `src/services/firebase.js` - Replace `firebaseConfig` + - `public/firebase-messaging-sw.js` - Replace Firebase config + - `backend/src/main/resources/application.properties` - Add service account path + +6. Download service account JSON from Firebase Console: + - Project Settings > Service Accounts > Generate New Private Key + - Save as `backend/src/main/resources/firebase-service-account.json` + +### 4. Nginx Setup (Production) + +```bash +# Install Nginx +# Windows: https://nginx.org/en/docs/windows.html +# Linux: sudo apt install nginx + +# Configure Nginx +sudo cp nginx/nginx.conf /etc/nginx/sites-available/iiit-insider +sudo ln -s /etc/nginx/sites-available/iiit-insider /etc/nginx/sites-enabled/ + +# Update nginx.conf with your domain +# Replace 'yourdomain.com' with actual domain + +# Start Nginx +sudo systemctl start nginx +``` + +## Features Implemented + +### 1. User Registration & Login +- JWT-based authentication +- Secure password hashing (BCrypt) +- Protected routes +- Session management + +**API Endpoints:** +- `POST /api/auth/register` - Create new account +- `POST /api/auth/login` - User login +- `GET /api/auth/me` - Get current user + +### 2. Firebase Notifications & Device Limits +- Push notifications via Firebase Cloud Messaging +- Device token management +- Maximum 5 devices per user +- Automatic oldest device removal + +**API Endpoints:** +- `POST /api/notifications/register-device` - Register device +- `POST /api/notifications/unregister-device` - Unregister device +- `GET /api/notifications/devices` - List user devices +- `POST /api/notifications/send` - Send notification + +### 3. Social Media Sharing +- Share to Facebook, Twitter, LinkedIn, WhatsApp, Reddit +- Dynamic share URLs generation +- Share button component + +**API Endpoints:** +- `GET /api/social/share-links?url=...&title=...` - All platforms +- `GET /api/social/share/facebook?url=...` - Facebook +- `GET /api/social/share/twitter?url=...&title=...` - Twitter +- `GET /api/social/share/linkedin?url=...` - LinkedIn +- `GET /api/social/share/whatsapp?url=...&title=...` - WhatsApp +- `GET /api/social/share/reddit?url=...&title=...` - Reddit + +## Testing + +### Test Authentication +```bash +# Register +curl -X POST http://localhost:8080/api/auth/register \ + -H "Content-Type: application/json" \ + -d '{"username":"testuser","email":"test@example.com","password":"password123"}' + +# Login +curl -X POST http://localhost:8080/api/auth/login \ + -H "Content-Type: application/json" \ + -d '{"username":"testuser","password":"password123"}' +``` + +### Test Social Sharing +```bash +curl "http://localhost:8080/api/social/share-links?url=https://iiitinsider.com&title=Check%20this%20out" +``` + +## Production Deployment + +1. **Build WAR file:** + ```bash + cd backend + mvn clean package + ``` + +2. **Deploy to Tomcat:** + ```bash + cp target/iiit-insider.war /path/to/tomcat/webapps/ + ``` + +3. **Build React for production:** + ```bash + npm run build + ``` + +4. **Configure Nginx to serve static files and proxy API requests** + +## Security Notes + +- Change JWT secret in production +- Use HTTPS in production +- Enable MySQL SSL connection +- Set strong database password +- Configure CORS properly for your domain diff --git a/backend/.gitignore b/backend/.gitignore new file mode 100644 index 0000000..165f81b --- /dev/null +++ b/backend/.gitignore @@ -0,0 +1,22 @@ +# Build output +target/ + +# Local environment files +.env +.env.* +!.env.example + +# Firebase and service account secrets +firebase-service-account.json +*firebase-service-account*.json +*firebase-adminsdk*.json +src/main/resources/firebase-service-account.json + +# Logs +logs/ +*.log + +# IDE/OS files +.DS_Store +.idea/ +.vscode/ diff --git a/backend/SETUP.md b/backend/SETUP.md new file mode 100644 index 0000000..9bd37d0 --- /dev/null +++ b/backend/SETUP.md @@ -0,0 +1,143 @@ +# IIIT Insider Backend - Setup Instructions + +## Prerequisites + +- Java 21 (or Java 17) +- Apache Tomcat 10 +- MySQL 8.0+ +- Maven 3.8+ +- Nginx (for production) + +## 1. Database Setup + +```bash +# Login to MySQL +mysql -u root -p + +# Create database (or run schema.sql) +CREATE DATABASE iiit_insider; +EXIT; +``` + +## 2. Configure Application Properties + +Edit `backend/src/main/resources/application.properties`: + +```properties +# Database +spring.datasource.username=root +spring.datasource.password=YOUR_MYSQL_PASSWORD + +# JWT Secret (use a strong random string, min 32 chars) +jwt.secret=your-super-secret-key-change-this-in-production + +# Firebase (optional - for push notifications) +firebase.credentials.path=path/to/firebase-service-account.json +``` + +## 3. Firebase Setup (Optional - for Push Notifications) + +1. Go to [Firebase Console](https://console.firebase.google.com/) +2. Create a new project +3. Go to Project Settings > Service Accounts +4. Generate new private key (downloads JSON file) +5. Place the JSON file at `backend/src/main/resources/firebase-service-account.json` +6. Update `application.properties` with your Firebase config + +## 4. Build the Application + +```bash +cd backend + +# Build WAR file for Tomcat +mvn clean package + +# Or run directly with embedded Tomcat +mvn spring-boot:run +``` + +## 5. Deploy to Tomcat + +```bash +# Copy WAR to Tomcat webapps +cp target/iiit-insider.war /path/to/tomcat/webapps/ + +# Start Tomcat +/path/to/tomcat/bin/startup.sh # Linux/Mac +/path/to/tomcat/bin/startup.bat # Windows + +# Or for Windows, use Tomcat Manager GUI +``` + +## 6. Nginx Configuration + +```bash +# Copy nginx.conf to Nginx sites +sudo cp nginx/nginx.conf /etc/nginx/sites-available/iiit-insider +sudo ln -s /etc/nginx/sites-available/iiit-insider /etc/nginx/sites-enabled/ + +# Test and reload Nginx +sudo nginx -t +sudo systemctl reload nginx +``` + +## 7. API Endpoints + +### Authentication +| Method | Endpoint | Description | +|--------|----------|-------------| +| POST | `/api/auth/register` | Register new user | +| POST | `/api/auth/login` | Login user | +| GET | `/api/auth/me` | Get current user | + +### Notifications +| Method | Endpoint | Description | +|--------|----------|-------------| +| POST | `/api/notifications/register-device` | Register FCM device token | +| POST | `/api/notifications/unregister-device` | Unregister device | +| GET | `/api/notifications/devices` | Get user's devices | +| POST | `/api/notifications/send` | Send push notification | + +### Social Media +| Method | Endpoint | Description | +|--------|----------|-------------| +| GET | `/api/social/share-links?url=...&title=...` | Get all share links | +| GET | `/api/social/share/facebook?url=...` | Facebook share URL | +| GET | `/api/social/share/twitter?url=...&title=...` | Twitter share URL | +| GET | `/api/social/share/linkedin?url=...` | LinkedIn share URL | +| GET | `/api/social/share/whatsapp?url=...&title=...` | WhatsApp share URL | +| GET | `/api/social/share/reddit?url=...&title=...` | Reddit share URL | + +## 8. Test the API + +```bash +# Register a user +curl -X POST http://localhost:8080/api/auth/register \ + -H "Content-Type: application/json" \ + -d '{"username":"testuser","email":"test@example.com","password":"password123"}' + +# Login +curl -X POST http://localhost:8080/api/auth/login \ + -H "Content-Type: application/json" \ + -d '{"username":"testuser","password":"password123"}' + +# Get share links +curl "http://localhost:8080/api/social/share-links?url=https://iiitinsider.com&title=Check%20this%20out" +``` + +## Troubleshooting + +### Port already in use +```bash +# Change port in application.properties +server.port=8081 +``` + +### MySQL connection failed +- Check MySQL is running: `sudo systemctl status mysql` +- Verify credentials in application.properties +- Check database exists: `SHOW DATABASES;` + +### CORS errors +- Update `app.cors.allowed-origins` in application.properties +- Add your frontend URL (e.g., `http://localhost:5173`) diff --git a/backend/pom.xml b/backend/pom.xml new file mode 100644 index 0000000..e2fd1e7 --- /dev/null +++ b/backend/pom.xml @@ -0,0 +1,137 @@ + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 3.2.0 + + + + com.iiitinsider + iiit-insider-backend + 1.0.0 + war + IIIT Insider Backend + Backend API for IIIT Insider website + + + 21 + 21 + 21 + UTF-8 + + + + + + org.springframework.boot + spring-boot-starter-web + + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + + + org.springframework.boot + spring-boot-starter-security + + + + + org.springframework.boot + spring-boot-starter-validation + + + + + com.mysql + mysql-connector-j + runtime + + + + + io.jsonwebtoken + jjwt-api + 0.12.3 + + + io.jsonwebtoken + jjwt-impl + 0.12.3 + runtime + + + io.jsonwebtoken + jjwt-jackson + 0.12.3 + runtime + + + + + com.google.firebase + firebase-admin + 9.2.0 + + + + + org.projectlombok + lombok + true + + + + + org.springframework.security + spring-security-crypto + + + + + jakarta.servlet + jakarta.servlet-api + provided + + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.security + spring-security-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + iiit-insider + + diff --git a/backend/src/main/java/com/iiitinsider/IiitInsiderApplication.java b/backend/src/main/java/com/iiitinsider/IiitInsiderApplication.java new file mode 100644 index 0000000..85335f4 --- /dev/null +++ b/backend/src/main/java/com/iiitinsider/IiitInsiderApplication.java @@ -0,0 +1,19 @@ +package com.iiitinsider; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; + +@SpringBootApplication +public class IiitInsiderApplication extends SpringBootServletInitializer { + + public static void main(String[] args) { + SpringApplication.run(IiitInsiderApplication.class, args); + } + + @Override + protected SpringApplicationBuilder configure(SpringApplicationBuilder application) { + return application.sources(IiitInsiderApplication.class); + } +} diff --git a/backend/src/main/java/com/iiitinsider/config/CorsConfig.java b/backend/src/main/java/com/iiitinsider/config/CorsConfig.java new file mode 100644 index 0000000..cd01a0f --- /dev/null +++ b/backend/src/main/java/com/iiitinsider/config/CorsConfig.java @@ -0,0 +1,36 @@ +package com.iiitinsider.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +@Configuration +public class CorsConfig { + + @Value("${app.cors.allowed-origins}") + private String allowedOrigins; + + @Bean + public CorsConfigurationSource corsConfigurationSource() { + CorsConfiguration configuration = new CorsConfiguration(); + configuration.setAllowedOrigins(Arrays.stream(allowedOrigins.split(",")) + .map(String::trim) + .filter(origin -> !origin.isEmpty()) + .collect(Collectors.toList())); + configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS")); + configuration.setAllowedHeaders(List.of("*")); + configuration.setExposedHeaders(List.of("Authorization")); + configuration.setAllowCredentials(true); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", configuration); + return source; + } +} diff --git a/backend/src/main/java/com/iiitinsider/config/FirebaseConfig.java b/backend/src/main/java/com/iiitinsider/config/FirebaseConfig.java new file mode 100644 index 0000000..290d6db --- /dev/null +++ b/backend/src/main/java/com/iiitinsider/config/FirebaseConfig.java @@ -0,0 +1,40 @@ +package com.iiitinsider.config; + +import com.google.auth.oauth2.GoogleCredentials; +import com.google.firebase.FirebaseApp; +import com.google.firebase.FirebaseOptions; +import com.google.firebase.messaging.FirebaseMessaging; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.io.FileInputStream; +import java.io.IOException; + +@Configuration +public class FirebaseConfig { + + @Value("${firebase.credentials.path}") + private String credentialsPath; + + @Value("${firebase.database.url:}") + private String databaseUrl; + + @Bean + @ConditionalOnExpression("'${firebase.credentials.path:}' != ''") + public FirebaseMessaging firebaseMessaging() throws IOException { + FirebaseOptions.Builder optionsBuilder = FirebaseOptions.builder() + .setCredentials(GoogleCredentials.fromStream(new FileInputStream(credentialsPath))); + + if (databaseUrl != null && !databaseUrl.isBlank()) { + optionsBuilder.setDatabaseUrl(databaseUrl); + } + + FirebaseApp app = FirebaseApp.getApps().isEmpty() + ? FirebaseApp.initializeApp(optionsBuilder.build()) + : FirebaseApp.getInstance(); + + return FirebaseMessaging.getInstance(app); + } +} diff --git a/backend/src/main/java/com/iiitinsider/config/JwtAuthenticationFilter.java b/backend/src/main/java/com/iiitinsider/config/JwtAuthenticationFilter.java new file mode 100644 index 0000000..d0f2079 --- /dev/null +++ b/backend/src/main/java/com/iiitinsider/config/JwtAuthenticationFilter.java @@ -0,0 +1,56 @@ +package com.iiitinsider.config; + +import com.iiitinsider.service.JwtService; +import com.iiitinsider.service.UserDetailsServiceImpl; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +@Component +public class JwtAuthenticationFilter extends OncePerRequestFilter { + + private final JwtService jwtService; + private final UserDetailsServiceImpl userDetailsService; + + public JwtAuthenticationFilter(JwtService jwtService, UserDetailsServiceImpl userDetailsService) { + this.jwtService = jwtService; + this.userDetailsService = userDetailsService; + } + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + String authHeader = request.getHeader("Authorization"); + + if (authHeader == null || !authHeader.startsWith("Bearer ")) { + filterChain.doFilter(request, response); + return; + } + + String token = authHeader.substring(7); + String username = jwtService.extractUsername(token); + + if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { + UserDetails userDetails = userDetailsService.loadUserByUsername(username); + + if (jwtService.isTokenValid(token, userDetails)) { + UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken( + userDetails, null, userDetails.getAuthorities() + ); + authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + SecurityContextHolder.getContext().setAuthentication(authToken); + } + } + + filterChain.doFilter(request, response); + } +} diff --git a/backend/src/main/java/com/iiitinsider/config/SecurityConfig.java b/backend/src/main/java/com/iiitinsider/config/SecurityConfig.java new file mode 100644 index 0000000..32eba78 --- /dev/null +++ b/backend/src/main/java/com/iiitinsider/config/SecurityConfig.java @@ -0,0 +1,65 @@ +package com.iiitinsider.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.web.cors.CorsConfigurationSource; + +@Configuration +@EnableWebSecurity +@EnableMethodSecurity +public class SecurityConfig { + + private final CorsConfigurationSource corsConfigurationSource; + private final JwtAuthenticationFilter jwtAuthenticationFilter; + + public SecurityConfig(CorsConfigurationSource corsConfigurationSource, + JwtAuthenticationFilter jwtAuthenticationFilter) { + this.corsConfigurationSource = corsConfigurationSource; + this.jwtAuthenticationFilter = jwtAuthenticationFilter; + } + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http + .cors(cors -> cors.configurationSource(corsConfigurationSource)) + .csrf(AbstractHttpConfigurer::disable) + .authorizeHttpRequests(auth -> auth + .requestMatchers("/auth/me").authenticated() + .requestMatchers("/auth/**").permitAll() + .requestMatchers("/colleges/**").permitAll() + .requestMatchers("/social/**").permitAll() + .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll() + .requestMatchers("/notifications/**").authenticated() + .requestMatchers("/admin/**").hasRole("ADMIN") + .anyRequest().authenticated() + ) + .sessionManagement(session -> session + .sessionCreationPolicy(SessionCreationPolicy.STATELESS) + ) + .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); + + return http.build(); + } + + @Bean + public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception { + return config.getAuthenticationManager(); + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} diff --git a/backend/src/main/java/com/iiitinsider/controller/AdminController.java b/backend/src/main/java/com/iiitinsider/controller/AdminController.java new file mode 100644 index 0000000..26939d2 --- /dev/null +++ b/backend/src/main/java/com/iiitinsider/controller/AdminController.java @@ -0,0 +1,51 @@ +package com.iiitinsider.controller; + +import com.iiitinsider.model.User; +import com.iiitinsider.repository.UserRepository; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@RestController +@RequestMapping("/admin") +public class AdminController { + + private final UserRepository userRepository; + + public AdminController(UserRepository userRepository) { + this.userRepository = userRepository; + } + + @GetMapping("/users") + @PreAuthorize("hasRole('ADMIN')") + public ResponseEntity getUsers() { + List> users = userRepository.findAll() + .stream() + .sorted(Comparator.comparing(User::getId)) + .map(this::toUserMap) + .collect(Collectors.toList()); + + return ResponseEntity.ok(Map.of("users", users)); + } + + private Map toUserMap(User user) { + Map data = new HashMap<>(); + data.put("id", user.getId()); + data.put("username", user.getUsername()); + data.put("email", user.getEmail()); + data.put("phoneNumber", user.getPhoneNumber()); + data.put("role", user.getRole().name()); + data.put("status", user.getStatus().name()); + data.put("createdAt", user.getCreatedAt()); + data.put("lastLogin", user.getLastLogin()); + return data; + } +} diff --git a/backend/src/main/java/com/iiitinsider/controller/AuthController.java b/backend/src/main/java/com/iiitinsider/controller/AuthController.java new file mode 100644 index 0000000..c8523eb --- /dev/null +++ b/backend/src/main/java/com/iiitinsider/controller/AuthController.java @@ -0,0 +1,118 @@ +package com.iiitinsider.controller; + +import com.iiitinsider.model.User; +import com.iiitinsider.repository.UserRepository; +import com.iiitinsider.service.AuthService; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.web.bind.annotation.*; + +import java.util.HashMap; +import java.util.Map; + +@RestController +@RequestMapping("/auth") +public class AuthController { + + private final AuthService authService; + private final UserRepository userRepository; + + public AuthController(AuthService authService, UserRepository userRepository) { + this.authService = authService; + this.userRepository = userRepository; + } + + @PostMapping("/register") + public ResponseEntity register(@RequestBody RegisterRequest request) { + try { + User user = authService.registerUser( + request.getUsername(), + request.getEmail(), + request.getPassword() + ); + + Map response = new HashMap<>(); + response.put("message", "User registered successfully"); + response.put("userId", user.getId()); + response.put("username", user.getUsername()); + response.put("email", user.getEmail()); + + return ResponseEntity.ok(response); + } catch (RuntimeException e) { + Map error = new HashMap<>(); + error.put("error", e.getMessage()); + return ResponseEntity.badRequest().body(error); + } + } + + @PostMapping("/login") + public ResponseEntity login(@RequestBody LoginRequest request) { + try { + String token = authService.authenticate(request.getUsername(), request.getPassword()); + + Map response = new HashMap<>(); + response.put("message", "Login successful"); + response.put("token", token); + response.put("tokenType", "Bearer"); + userRepository.findByUsername(request.getUsername()).ifPresent(user -> { + response.put("userId", user.getId()); + response.put("username", user.getUsername()); + response.put("email", user.getEmail()); + response.put("role", user.getRole().name()); + }); + + return ResponseEntity.ok(response); + } catch (Exception e) { + Map error = new HashMap<>(); + error.put("error", "Invalid username or password"); + return ResponseEntity.badRequest().body(error); + } + } + + @GetMapping("/me") + public ResponseEntity getCurrentUser(@AuthenticationPrincipal UserDetails userDetails) { + try { + if (userDetails == null) { + return ResponseEntity.status(401).body(Map.of("error", "Not authenticated")); + } + + User user = userRepository.findByUsername(userDetails.getUsername()) + .orElseThrow(() -> new RuntimeException("User not found")); + + Map response = new HashMap<>(); + response.put("authenticated", true); + response.put("userId", user.getId()); + response.put("username", user.getUsername()); + response.put("email", user.getEmail()); + response.put("role", user.getRole().name()); + + return ResponseEntity.ok(response); + } catch (Exception e) { + return ResponseEntity.badRequest().body(Map.of("error", e.getMessage())); + } + } + + public static class RegisterRequest { + private String username; + private String email; + private String password; + + public String getUsername() { return username; } + public void setUsername(String username) { this.username = username; } + public String getEmail() { return email; } + public void setEmail(String email) { this.email = email; } + public String getPassword() { return password; } + public void setPassword(String password) { this.password = password; } + } + + public static class LoginRequest { + private String username; + private String password; + + public String getUsername() { return username; } + public void setUsername(String username) { this.username = username; } + public String getPassword() { return password; } + public void setPassword(String password) { this.password = password; } + } +} diff --git a/backend/src/main/java/com/iiitinsider/controller/CollegeController.java b/backend/src/main/java/com/iiitinsider/controller/CollegeController.java new file mode 100644 index 0000000..2d2a192 --- /dev/null +++ b/backend/src/main/java/com/iiitinsider/controller/CollegeController.java @@ -0,0 +1,46 @@ +package com.iiitinsider.controller; + +import com.iiitinsider.service.CollegeService; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.Map; + +@RestController +@RequestMapping("/colleges") +public class CollegeController { + + private final CollegeService collegeService; + + public CollegeController(CollegeService collegeService) { + this.collegeService = collegeService; + } + + @GetMapping + public ResponseEntity getColleges(@RequestParam(required = false) String region) { + return ResponseEntity.ok(collegeService.getColleges(region)); + } + + @GetMapping("/search") + public ResponseEntity searchColleges(@RequestParam String query) { + return ResponseEntity.ok(collegeService.searchColleges(query)); + } + + @GetMapping("/compare") + public ResponseEntity compareCollege(@RequestParam String name) { + try { + return ResponseEntity.ok(collegeService.getCompareData(name)); + } catch (RuntimeException e) { + return ResponseEntity.badRequest().body(Map.of("error", e.getMessage())); + } + } + + @GetMapping("/{slug}") + public ResponseEntity getCollegeDetails(@PathVariable String slug) { + try { + return ResponseEntity.ok(collegeService.getCollegeDetails(slug)); + } catch (RuntimeException e) { + return ResponseEntity.badRequest().body(Map.of("error", e.getMessage())); + } + } +} diff --git a/backend/src/main/java/com/iiitinsider/controller/NotificationController.java b/backend/src/main/java/com/iiitinsider/controller/NotificationController.java new file mode 100644 index 0000000..b35cdd2 --- /dev/null +++ b/backend/src/main/java/com/iiitinsider/controller/NotificationController.java @@ -0,0 +1,152 @@ +package com.iiitinsider.controller; + +import com.iiitinsider.model.DeviceToken; +import com.iiitinsider.model.User; +import com.iiitinsider.repository.UserRepository; +import com.iiitinsider.service.FirebaseNotificationService; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.web.bind.annotation.*; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@RestController +@RequestMapping("/notifications") +public class NotificationController { + + private final FirebaseNotificationService notificationService; + private final UserRepository userRepository; + + public NotificationController(FirebaseNotificationService notificationService, UserRepository userRepository) { + this.notificationService = notificationService; + this.userRepository = userRepository; + } + + @PostMapping("/register-device") + public ResponseEntity registerDevice( + @RequestBody DeviceRegisterRequest request, + @AuthenticationPrincipal UserDetails userDetails + ) { + try { + User user = userRepository.findByUsername(userDetails.getUsername()) + .orElseThrow(() -> new RuntimeException("User not found")); + + notificationService.registerDevice( + user, + request.getToken(), + request.getDeviceType(), + request.getDeviceName() + ); + + Map response = new HashMap<>(); + response.put("message", "Device registered successfully"); + + return ResponseEntity.ok(response); + } catch (Exception e) { + Map error = new HashMap<>(); + error.put("error", e.getMessage()); + return ResponseEntity.badRequest().body(error); + } + } + + @PostMapping("/unregister-device") + public ResponseEntity unregisterDevice(@RequestBody UnregisterDeviceRequest request) { + try { + notificationService.unregisterDevice(request.getToken()); + + Map response = new HashMap<>(); + response.put("message", "Device unregistered successfully"); + + return ResponseEntity.ok(response); + } catch (Exception e) { + Map error = new HashMap<>(); + error.put("error", e.getMessage()); + return ResponseEntity.badRequest().body(error); + } + } + + @GetMapping("/devices") + public ResponseEntity getUserDevices(@AuthenticationPrincipal UserDetails userDetails) { + try { + User user = userRepository.findByUsername(userDetails.getUsername()) + .orElseThrow(() -> new RuntimeException("User not found")); + + List devices = notificationService.getUserDevices(user.getId()); + + List> deviceList = devices.stream() + .map(device -> { + Map deviceMap = new HashMap<>(); + deviceMap.put("id", device.getId()); + deviceMap.put("deviceType", device.getDeviceType()); + deviceMap.put("deviceName", device.getDeviceName()); + deviceMap.put("isActive", device.getIsActive()); + deviceMap.put("createdAt", device.getCreatedAt().toString()); + return deviceMap; + }) + .collect(Collectors.toList()); + + return ResponseEntity.ok(Map.of("devices", deviceList)); + } catch (Exception e) { + return ResponseEntity.badRequest().body(Map.of("error", e.getMessage())); + } + } + + @PostMapping("/send") + public ResponseEntity sendNotification( + @RequestBody SendNotificationRequest request, + @AuthenticationPrincipal UserDetails userDetails + ) { + try { + User user = userRepository.findByUsername(userDetails.getUsername()) + .orElseThrow(() -> new RuntimeException("User not found")); + + String result = notificationService.sendNotificationToUser( + user.getId(), + request.getTitle(), + request.getBody(), + request.getImageUrl() + ); + + return ResponseEntity.ok(Map.of("message", "Notification sent", "result", result)); + } catch (Exception e) { + return ResponseEntity.badRequest().body(Map.of("error", e.getMessage())); + } + } + + public static class DeviceRegisterRequest { + private String token; + private String deviceType; + private String deviceName; + + public String getToken() { return token; } + public void setToken(String token) { this.token = token; } + public String getDeviceType() { return deviceType; } + public void setDeviceType(String deviceType) { this.deviceType = deviceType; } + public String getDeviceName() { return deviceName; } + public void setDeviceName(String deviceName) { this.deviceName = deviceName; } + } + + public static class UnregisterDeviceRequest { + private String token; + + public String getToken() { return token; } + public void setToken(String token) { this.token = token; } + } + + public static class SendNotificationRequest { + private String title; + private String body; + private String imageUrl; + + public String getTitle() { return title; } + public void setTitle(String title) { this.title = title; } + public String getBody() { return body; } + public void setBody(String body) { this.body = body; } + public String getImageUrl() { return imageUrl; } + public void setImageUrl(String imageUrl) { this.imageUrl = imageUrl; } + } +} diff --git a/backend/src/main/java/com/iiitinsider/controller/SocialMediaController.java b/backend/src/main/java/com/iiitinsider/controller/SocialMediaController.java new file mode 100644 index 0000000..537c3ee --- /dev/null +++ b/backend/src/main/java/com/iiitinsider/controller/SocialMediaController.java @@ -0,0 +1,70 @@ +package com.iiitinsider.controller; + +import com.iiitinsider.service.SocialMediaService; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.Map; + +@RestController +@RequestMapping("/social") +public class SocialMediaController { + + private final SocialMediaService socialMediaService; + + public SocialMediaController(SocialMediaService socialMediaService) { + this.socialMediaService = socialMediaService; + } + + @GetMapping("/share-links") + public ResponseEntity getShareLinks( + @RequestParam String url, + @RequestParam(required = false, defaultValue = "") String title + ) { + try { + Map shareLinks = socialMediaService.getShareLinks(url, title); + return ResponseEntity.ok(shareLinks); + } catch (Exception e) { + return ResponseEntity.badRequest().body(Map.of("error", e.getMessage())); + } + } + + @GetMapping("/share/facebook") + public ResponseEntity facebookShare(@RequestParam String url) { + String shareUrl = socialMediaService.generateFacebookShareUrl(url); + return ResponseEntity.ok(Map.of("url", shareUrl, "platform", "facebook")); + } + + @GetMapping("/share/twitter") + public ResponseEntity twitterShare( + @RequestParam String url, + @RequestParam(required = false, defaultValue = "") String title + ) { + String shareUrl = socialMediaService.generateTwitterShareUrl(url, title); + return ResponseEntity.ok(Map.of("url", shareUrl, "platform", "twitter")); + } + + @GetMapping("/share/linkedin") + public ResponseEntity linkedinShare(@RequestParam String url) { + String shareUrl = socialMediaService.generateLinkedInShareUrl(url); + return ResponseEntity.ok(Map.of("url", shareUrl, "platform", "linkedin")); + } + + @GetMapping("/share/whatsapp") + public ResponseEntity whatsappShare( + @RequestParam String url, + @RequestParam(required = false, defaultValue = "") String title + ) { + String shareUrl = socialMediaService.generateWhatsAppShareUrl(url, title); + return ResponseEntity.ok(Map.of("url", shareUrl, "platform", "whatsapp")); + } + + @GetMapping("/share/reddit") + public ResponseEntity redditShare( + @RequestParam String url, + @RequestParam(required = false, defaultValue = "") String title + ) { + String shareUrl = socialMediaService.generateRedditShareUrl(url, title); + return ResponseEntity.ok(Map.of("url", shareUrl, "platform", "reddit")); + } +} diff --git a/backend/src/main/java/com/iiitinsider/model/Branch.java b/backend/src/main/java/com/iiitinsider/model/Branch.java new file mode 100644 index 0000000..e62b53c --- /dev/null +++ b/backend/src/main/java/com/iiitinsider/model/Branch.java @@ -0,0 +1,118 @@ +package com.iiitinsider.model; + +import jakarta.persistence.*; + +import java.time.LocalDateTime; + +@Entity +@Table( + name = "branches", + uniqueConstraints = @UniqueConstraint(columnNames = {"college_id", "code"}) +) +public class Branch { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "college_id", nullable = false) + private College college; + + @Column(nullable = false, length = 120) + private String name; + + @Column(nullable = false, length = 30) + private String code; + + @Column(columnDefinition = "TEXT") + private String description; + + private Integer seats; + + @Column(name = "is_popular", nullable = false) + private Boolean isPopular = false; + + @Column(name = "created_at") + private LocalDateTime createdAt = LocalDateTime.now(); + + @Column(name = "updated_at") + private LocalDateTime updatedAt = LocalDateTime.now(); + + @PreUpdate + public void preUpdate() { + updatedAt = LocalDateTime.now(); + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public College getCollege() { + return college; + } + + public void setCollege(College college) { + this.college = college; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getCode() { + return code; + } + + public void setCode(String code) { + this.code = code; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public Integer getSeats() { + return seats; + } + + public void setSeats(Integer seats) { + this.seats = seats; + } + + public Boolean getIsPopular() { + return isPopular; + } + + public void setIsPopular(Boolean popular) { + isPopular = popular; + } + + public LocalDateTime getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + } + + public LocalDateTime getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(LocalDateTime updatedAt) { + this.updatedAt = updatedAt; + } +} diff --git a/backend/src/main/java/com/iiitinsider/model/College.java b/backend/src/main/java/com/iiitinsider/model/College.java new file mode 100644 index 0000000..295af40 --- /dev/null +++ b/backend/src/main/java/com/iiitinsider/model/College.java @@ -0,0 +1,264 @@ +package com.iiitinsider.model; + +import jakarta.persistence.*; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +@Entity +@Table(name = "colleges") +public class College { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false, unique = true, length = 150) + private String name; + + @Column(unique = true, length = 180) + private String slug; + + @Column(length = 150) + private String shortName; + + @Column(columnDefinition = "TEXT") + private String description; + + @Column(length = 500) + private String imageUrl; + + @Enumerated(EnumType.STRING) + @Column(nullable = false, length = 30) + private Region region = Region.OTHER; + + @Column(length = 100) + private String city; + + @Column(length = 100) + private String state; + + @Column(length = 100) + private String campusArea; + + @Column(length = 80) + private String instituteType; + + private Integer establishedYear; + + @Column(length = 500) + private String websiteUrl; + + @Column(length = 150) + private String mentorInstitute; + + @Column(name = "is_active", nullable = false) + private Boolean isActive = true; + + @Column(name = "created_at") + private LocalDateTime createdAt = LocalDateTime.now(); + + @Column(name = "updated_at") + private LocalDateTime updatedAt = LocalDateTime.now(); + + @OneToMany(mappedBy = "college", cascade = CascadeType.ALL, orphanRemoval = true) + private List branches = new ArrayList<>(); + + @OneToMany(mappedBy = "college", cascade = CascadeType.ALL, orphanRemoval = true) + private List programs = new ArrayList<>(); + + @OneToMany(mappedBy = "college", cascade = CascadeType.ALL, orphanRemoval = true) + private List placementStatistics = new ArrayList<>(); + + @OneToMany(mappedBy = "college", cascade = CascadeType.ALL, orphanRemoval = true) + private List cutoffs = new ArrayList<>(); + + @OneToMany(mappedBy = "college", cascade = CascadeType.ALL, orphanRemoval = true) + private List reviews = new ArrayList<>(); + + @PreUpdate + public void preUpdate() { + updatedAt = LocalDateTime.now(); + } + + public enum Region { + NORTH, SOUTH, EAST, WEST, CENTRAL, OTHER + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getSlug() { + return slug; + } + + public void setSlug(String slug) { + this.slug = slug; + } + + public String getShortName() { + return shortName; + } + + public void setShortName(String shortName) { + this.shortName = shortName; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getImageUrl() { + return imageUrl; + } + + public void setImageUrl(String imageUrl) { + this.imageUrl = imageUrl; + } + + public Region getRegion() { + return region; + } + + public void setRegion(Region region) { + this.region = region; + } + + public String getCity() { + return city; + } + + public void setCity(String city) { + this.city = city; + } + + public String getState() { + return state; + } + + public void setState(String state) { + this.state = state; + } + + public String getCampusArea() { + return campusArea; + } + + public void setCampusArea(String campusArea) { + this.campusArea = campusArea; + } + + public String getInstituteType() { + return instituteType; + } + + public void setInstituteType(String instituteType) { + this.instituteType = instituteType; + } + + public Integer getEstablishedYear() { + return establishedYear; + } + + public void setEstablishedYear(Integer establishedYear) { + this.establishedYear = establishedYear; + } + + public String getWebsiteUrl() { + return websiteUrl; + } + + public void setWebsiteUrl(String websiteUrl) { + this.websiteUrl = websiteUrl; + } + + public String getMentorInstitute() { + return mentorInstitute; + } + + public void setMentorInstitute(String mentorInstitute) { + this.mentorInstitute = mentorInstitute; + } + + public Boolean getIsActive() { + return isActive; + } + + public void setIsActive(Boolean active) { + isActive = active; + } + + public LocalDateTime getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + } + + public LocalDateTime getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(LocalDateTime updatedAt) { + this.updatedAt = updatedAt; + } + + public List getBranches() { + return branches; + } + + public void setBranches(List branches) { + this.branches = branches; + } + + public List getPrograms() { + return programs; + } + + public void setPrograms(List programs) { + this.programs = programs; + } + + public List getPlacementStatistics() { + return placementStatistics; + } + + public void setPlacementStatistics(List placementStatistics) { + this.placementStatistics = placementStatistics; + } + + public List getCutoffs() { + return cutoffs; + } + + public void setCutoffs(List cutoffs) { + this.cutoffs = cutoffs; + } + + public List getReviews() { + return reviews; + } + + public void setReviews(List reviews) { + this.reviews = reviews; + } +} diff --git a/backend/src/main/java/com/iiitinsider/model/Cutoff.java b/backend/src/main/java/com/iiitinsider/model/Cutoff.java new file mode 100644 index 0000000..58ddf2a --- /dev/null +++ b/backend/src/main/java/com/iiitinsider/model/Cutoff.java @@ -0,0 +1,164 @@ +package com.iiitinsider.model; + +import jakarta.persistence.*; + +import java.time.LocalDateTime; + +@Entity +@Table(name = "cutoffs") +public class Cutoff { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "college_id", nullable = false) + private College college; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "branch_id") + private Branch branch; + + @Column(nullable = false) + private Integer year; + + @Column(nullable = false) + private Integer round; + + @Enumerated(EnumType.STRING) + @Column(nullable = false, length = 30) + private CounsellingType counsellingType = CounsellingType.JOSAA; + + @Column(length = 60) + private String category; + + @Column(length = 20) + private String quota; + + @Column(length = 20) + private String gender; + + private Integer openingRank; + + private Integer closingRank; + + @Column(name = "created_at") + private LocalDateTime createdAt = LocalDateTime.now(); + + @Column(name = "updated_at") + private LocalDateTime updatedAt = LocalDateTime.now(); + + @PreUpdate + public void preUpdate() { + updatedAt = LocalDateTime.now(); + } + + public enum CounsellingType { + JOSAA, CSAB, DASA, UCEED, INSTITUTE, OTHER + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public College getCollege() { + return college; + } + + public void setCollege(College college) { + this.college = college; + } + + public Branch getBranch() { + return branch; + } + + public void setBranch(Branch branch) { + this.branch = branch; + } + + public Integer getYear() { + return year; + } + + public void setYear(Integer year) { + this.year = year; + } + + public Integer getRound() { + return round; + } + + public void setRound(Integer round) { + this.round = round; + } + + public CounsellingType getCounsellingType() { + return counsellingType; + } + + public void setCounsellingType(CounsellingType counsellingType) { + this.counsellingType = counsellingType; + } + + public String getCategory() { + return category; + } + + public void setCategory(String category) { + this.category = category; + } + + public String getQuota() { + return quota; + } + + public void setQuota(String quota) { + this.quota = quota; + } + + public String getGender() { + return gender; + } + + public void setGender(String gender) { + this.gender = gender; + } + + public Integer getOpeningRank() { + return openingRank; + } + + public void setOpeningRank(Integer openingRank) { + this.openingRank = openingRank; + } + + public Integer getClosingRank() { + return closingRank; + } + + public void setClosingRank(Integer closingRank) { + this.closingRank = closingRank; + } + + public LocalDateTime getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + } + + public LocalDateTime getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(LocalDateTime updatedAt) { + this.updatedAt = updatedAt; + } +} diff --git a/backend/src/main/java/com/iiitinsider/model/DeviceToken.java b/backend/src/main/java/com/iiitinsider/model/DeviceToken.java new file mode 100644 index 0000000..9c158b9 --- /dev/null +++ b/backend/src/main/java/com/iiitinsider/model/DeviceToken.java @@ -0,0 +1,111 @@ +package com.iiitinsider.model; + +import jakarta.persistence.*; + +import java.time.LocalDateTime; + +@Entity +@Table(name = "device_tokens") +public class DeviceToken { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + private User user; + + @Column(nullable = false, length = 500) + private String token; + + @Column(nullable = false) + private String deviceType = "WEB"; + + @Column(length = 100) + private String deviceName; + + @Column(nullable = false) + private Boolean isActive = true; + + @Column(name = "created_at") + private LocalDateTime createdAt = LocalDateTime.now(); + + @Column(name = "last_used_at") + private LocalDateTime lastUsedAt; + + @Column(name = "max_devices_limit") + private Integer maxDevicesLimit = 5; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public User getUser() { + return user; + } + + public void setUser(User user) { + this.user = user; + } + + public String getToken() { + return token; + } + + public void setToken(String token) { + this.token = token; + } + + public String getDeviceType() { + return deviceType; + } + + public void setDeviceType(String deviceType) { + this.deviceType = deviceType; + } + + public String getDeviceName() { + return deviceName; + } + + public void setDeviceName(String deviceName) { + this.deviceName = deviceName; + } + + public Boolean getIsActive() { + return isActive; + } + + public void setIsActive(Boolean active) { + isActive = active; + } + + public LocalDateTime getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + } + + public LocalDateTime getLastUsedAt() { + return lastUsedAt; + } + + public void setLastUsedAt(LocalDateTime lastUsedAt) { + this.lastUsedAt = lastUsedAt; + } + + public Integer getMaxDevicesLimit() { + return maxDevicesLimit; + } + + public void setMaxDevicesLimit(Integer maxDevicesLimit) { + this.maxDevicesLimit = maxDevicesLimit; + } +} diff --git a/backend/src/main/java/com/iiitinsider/model/PlacementStatistic.java b/backend/src/main/java/com/iiitinsider/model/PlacementStatistic.java new file mode 100644 index 0000000..da4add5 --- /dev/null +++ b/backend/src/main/java/com/iiitinsider/model/PlacementStatistic.java @@ -0,0 +1,162 @@ +package com.iiitinsider.model; + +import jakarta.persistence.*; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +@Entity +@Table( + name = "placement_statistics", + uniqueConstraints = @UniqueConstraint(columnNames = {"college_id", "academic_year"}) +) +public class PlacementStatistic { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "college_id", nullable = false) + private College college; + + @Column(name = "academic_year", nullable = false, length = 20) + private String academicYear; + + @Column(name = "average_package_lpa", precision = 10, scale = 2) + private BigDecimal averagePackageLpa; + + @Column(name = "median_package_lpa", precision = 10, scale = 2) + private BigDecimal medianPackageLpa; + + @Column(name = "highest_package_lpa", precision = 10, scale = 2) + private BigDecimal highestPackageLpa; + + @Column(name = "placement_percentage", precision = 5, scale = 2) + private BigDecimal placementPercentage; + + private Integer studentsPlaced; + + private Integer totalEligibleStudents; + + @Column(columnDefinition = "TEXT") + private String topRecruiters; + + @Column(columnDefinition = "TEXT") + private String sourceUrl; + + @Column(name = "created_at") + private LocalDateTime createdAt = LocalDateTime.now(); + + @Column(name = "updated_at") + private LocalDateTime updatedAt = LocalDateTime.now(); + + @PreUpdate + public void preUpdate() { + updatedAt = LocalDateTime.now(); + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public College getCollege() { + return college; + } + + public void setCollege(College college) { + this.college = college; + } + + public String getAcademicYear() { + return academicYear; + } + + public void setAcademicYear(String academicYear) { + this.academicYear = academicYear; + } + + public BigDecimal getAveragePackageLpa() { + return averagePackageLpa; + } + + public void setAveragePackageLpa(BigDecimal averagePackageLpa) { + this.averagePackageLpa = averagePackageLpa; + } + + public BigDecimal getMedianPackageLpa() { + return medianPackageLpa; + } + + public void setMedianPackageLpa(BigDecimal medianPackageLpa) { + this.medianPackageLpa = medianPackageLpa; + } + + public BigDecimal getHighestPackageLpa() { + return highestPackageLpa; + } + + public void setHighestPackageLpa(BigDecimal highestPackageLpa) { + this.highestPackageLpa = highestPackageLpa; + } + + public BigDecimal getPlacementPercentage() { + return placementPercentage; + } + + public void setPlacementPercentage(BigDecimal placementPercentage) { + this.placementPercentage = placementPercentage; + } + + public Integer getStudentsPlaced() { + return studentsPlaced; + } + + public void setStudentsPlaced(Integer studentsPlaced) { + this.studentsPlaced = studentsPlaced; + } + + public Integer getTotalEligibleStudents() { + return totalEligibleStudents; + } + + public void setTotalEligibleStudents(Integer totalEligibleStudents) { + this.totalEligibleStudents = totalEligibleStudents; + } + + public String getTopRecruiters() { + return topRecruiters; + } + + public void setTopRecruiters(String topRecruiters) { + this.topRecruiters = topRecruiters; + } + + public String getSourceUrl() { + return sourceUrl; + } + + public void setSourceUrl(String sourceUrl) { + this.sourceUrl = sourceUrl; + } + + public LocalDateTime getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + } + + public LocalDateTime getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(LocalDateTime updatedAt) { + this.updatedAt = updatedAt; + } +} diff --git a/backend/src/main/java/com/iiitinsider/model/Program.java b/backend/src/main/java/com/iiitinsider/model/Program.java new file mode 100644 index 0000000..4dffdf0 --- /dev/null +++ b/backend/src/main/java/com/iiitinsider/model/Program.java @@ -0,0 +1,109 @@ +package com.iiitinsider.model; + +import jakarta.persistence.*; + +import java.time.LocalDateTime; + +@Entity +@Table(name = "programs") +public class Program { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "college_id", nullable = false) + private College college; + + @Column(nullable = false, length = 120) + private String name; + + @Enumerated(EnumType.STRING) + @Column(nullable = false, length = 30) + private DegreeLevel degreeLevel = DegreeLevel.OTHER; + + private Integer durationYears; + + @Column(columnDefinition = "TEXT") + private String description; + + @Column(name = "created_at") + private LocalDateTime createdAt = LocalDateTime.now(); + + @Column(name = "updated_at") + private LocalDateTime updatedAt = LocalDateTime.now(); + + @PreUpdate + public void preUpdate() { + updatedAt = LocalDateTime.now(); + } + + public enum DegreeLevel { + BTECH, MTECH, DUAL_DEGREE, MBA, MSC, MDES, BDES, PHD, OTHER + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public College getCollege() { + return college; + } + + public void setCollege(College college) { + this.college = college; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public DegreeLevel getDegreeLevel() { + return degreeLevel; + } + + public void setDegreeLevel(DegreeLevel degreeLevel) { + this.degreeLevel = degreeLevel; + } + + public Integer getDurationYears() { + return durationYears; + } + + public void setDurationYears(Integer durationYears) { + this.durationYears = durationYears; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public LocalDateTime getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + } + + public LocalDateTime getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(LocalDateTime updatedAt) { + this.updatedAt = updatedAt; + } +} diff --git a/backend/src/main/java/com/iiitinsider/model/Review.java b/backend/src/main/java/com/iiitinsider/model/Review.java new file mode 100644 index 0000000..6598029 --- /dev/null +++ b/backend/src/main/java/com/iiitinsider/model/Review.java @@ -0,0 +1,139 @@ +package com.iiitinsider.model; + +import jakarta.persistence.*; + +import java.time.LocalDateTime; + +@Entity +@Table(name = "reviews") +public class Review { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "college_id", nullable = false) + private College college; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + private User user; + + @Column(nullable = false, length = 150) + private String title; + + @Column(columnDefinition = "TEXT") + private String body; + + @Column(nullable = false) + private Integer rating; + + @Column(columnDefinition = "TEXT") + private String pros; + + @Column(columnDefinition = "TEXT") + private String cons; + + @Column(name = "is_approved", nullable = false) + private Boolean isApproved = false; + + @Column(name = "created_at") + private LocalDateTime createdAt = LocalDateTime.now(); + + @Column(name = "updated_at") + private LocalDateTime updatedAt = LocalDateTime.now(); + + @PreUpdate + public void preUpdate() { + updatedAt = LocalDateTime.now(); + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public College getCollege() { + return college; + } + + public void setCollege(College college) { + this.college = college; + } + + public User getUser() { + return user; + } + + public void setUser(User user) { + this.user = user; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getBody() { + return body; + } + + public void setBody(String body) { + this.body = body; + } + + public Integer getRating() { + return rating; + } + + public void setRating(Integer rating) { + this.rating = rating; + } + + public String getPros() { + return pros; + } + + public void setPros(String pros) { + this.pros = pros; + } + + public String getCons() { + return cons; + } + + public void setCons(String cons) { + this.cons = cons; + } + + public Boolean getIsApproved() { + return isApproved; + } + + public void setIsApproved(Boolean approved) { + isApproved = approved; + } + + public LocalDateTime getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + } + + public LocalDateTime getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(LocalDateTime updatedAt) { + this.updatedAt = updatedAt; + } +} diff --git a/backend/src/main/java/com/iiitinsider/model/User.java b/backend/src/main/java/com/iiitinsider/model/User.java new file mode 100644 index 0000000..f0ac5b8 --- /dev/null +++ b/backend/src/main/java/com/iiitinsider/model/User.java @@ -0,0 +1,141 @@ +package com.iiitinsider.model; + +import jakarta.persistence.*; + +import java.time.LocalDateTime; + +@Entity +@Table(name = "users") +public class User { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false, unique = true, length = 50) + private String username; + + @Column(nullable = false, unique = true, length = 100) + private String email; + + @Column(nullable = false) + private String password; + + @Column(length = 20) + private String phoneNumber; + + @Column(length = 255) + private String profileImageUrl; + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private Role role = Role.USER; + + @Enumerated(EnumType.STRING) + private AccountStatus status = AccountStatus.ACTIVE; + + @Column(name = "created_at") + private LocalDateTime createdAt = LocalDateTime.now(); + + @Column(name = "updated_at") + private LocalDateTime updatedAt = LocalDateTime.now(); + + @Column(name = "last_login") + private LocalDateTime lastLogin; + + public enum Role { + USER, ADMIN + } + + public enum AccountStatus { + ACTIVE, INACTIVE, SUSPENDED + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public String getPhoneNumber() { + return phoneNumber; + } + + public void setPhoneNumber(String phoneNumber) { + this.phoneNumber = phoneNumber; + } + + public String getProfileImageUrl() { + return profileImageUrl; + } + + public void setProfileImageUrl(String profileImageUrl) { + this.profileImageUrl = profileImageUrl; + } + + public Role getRole() { + return role; + } + + public void setRole(Role role) { + this.role = role; + } + + public AccountStatus getStatus() { + return status; + } + + public void setStatus(AccountStatus status) { + this.status = status; + } + + public LocalDateTime getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + } + + public LocalDateTime getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(LocalDateTime updatedAt) { + this.updatedAt = updatedAt; + } + + public LocalDateTime getLastLogin() { + return lastLogin; + } + + public void setLastLogin(LocalDateTime lastLogin) { + this.lastLogin = lastLogin; + } +} diff --git a/backend/src/main/java/com/iiitinsider/repository/BranchRepository.java b/backend/src/main/java/com/iiitinsider/repository/BranchRepository.java new file mode 100644 index 0000000..7740db4 --- /dev/null +++ b/backend/src/main/java/com/iiitinsider/repository/BranchRepository.java @@ -0,0 +1,12 @@ +package com.iiitinsider.repository; + +import com.iiitinsider.model.Branch; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface BranchRepository extends JpaRepository { + List findByCollegeIdOrderByNameAsc(Long collegeId); +} diff --git a/backend/src/main/java/com/iiitinsider/repository/CollegeRepository.java b/backend/src/main/java/com/iiitinsider/repository/CollegeRepository.java new file mode 100644 index 0000000..a374d52 --- /dev/null +++ b/backend/src/main/java/com/iiitinsider/repository/CollegeRepository.java @@ -0,0 +1,17 @@ +package com.iiitinsider.repository; + +import com.iiitinsider.model.College; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface CollegeRepository extends JpaRepository { + List findByIsActiveTrueOrderByNameAsc(); + List findByRegionAndIsActiveTrueOrderByNameAsc(College.Region region); + Optional findBySlugIgnoreCase(String slug); + Optional findByNameIgnoreCase(String name); + List findByNameContainingIgnoreCaseAndIsActiveTrueOrderByNameAsc(String name); +} diff --git a/backend/src/main/java/com/iiitinsider/repository/CutoffRepository.java b/backend/src/main/java/com/iiitinsider/repository/CutoffRepository.java new file mode 100644 index 0000000..cdff186 --- /dev/null +++ b/backend/src/main/java/com/iiitinsider/repository/CutoffRepository.java @@ -0,0 +1,13 @@ +package com.iiitinsider.repository; + +import com.iiitinsider.model.Cutoff; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface CutoffRepository extends JpaRepository { + List findByCollegeIdOrderByYearDescRoundAsc(Long collegeId); + List findByCollegeIdAndBranchIdOrderByYearDescRoundAsc(Long collegeId, Long branchId); +} diff --git a/backend/src/main/java/com/iiitinsider/repository/DeviceTokenRepository.java b/backend/src/main/java/com/iiitinsider/repository/DeviceTokenRepository.java new file mode 100644 index 0000000..eda52f3 --- /dev/null +++ b/backend/src/main/java/com/iiitinsider/repository/DeviceTokenRepository.java @@ -0,0 +1,24 @@ +package com.iiitinsider.repository; + +import com.iiitinsider.model.DeviceToken; +import com.iiitinsider.model.User; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface DeviceTokenRepository extends JpaRepository { + List findByUserId(Long userId); + List findByUser(User user); + Optional findByToken(String token); + + @Modifying + @Query("DELETE FROM DeviceToken dt WHERE dt.user.id = :userId AND dt.isActive = false") + void deleteInactiveTokens(Long userId); + + long countByUserIdAndIsActive(Long userId, Boolean isActive); +} diff --git a/backend/src/main/java/com/iiitinsider/repository/PlacementStatisticRepository.java b/backend/src/main/java/com/iiitinsider/repository/PlacementStatisticRepository.java new file mode 100644 index 0000000..d3a3147 --- /dev/null +++ b/backend/src/main/java/com/iiitinsider/repository/PlacementStatisticRepository.java @@ -0,0 +1,14 @@ +package com.iiitinsider.repository; + +import com.iiitinsider.model.PlacementStatistic; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface PlacementStatisticRepository extends JpaRepository { + List findByCollegeIdOrderByAcademicYearDesc(Long collegeId); + Optional findFirstByCollegeIdOrderByAcademicYearDesc(Long collegeId); +} diff --git a/backend/src/main/java/com/iiitinsider/repository/ProgramRepository.java b/backend/src/main/java/com/iiitinsider/repository/ProgramRepository.java new file mode 100644 index 0000000..0c98136 --- /dev/null +++ b/backend/src/main/java/com/iiitinsider/repository/ProgramRepository.java @@ -0,0 +1,12 @@ +package com.iiitinsider.repository; + +import com.iiitinsider.model.Program; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface ProgramRepository extends JpaRepository { + List findByCollegeIdOrderByNameAsc(Long collegeId); +} diff --git a/backend/src/main/java/com/iiitinsider/repository/ReviewRepository.java b/backend/src/main/java/com/iiitinsider/repository/ReviewRepository.java new file mode 100644 index 0000000..4e180a8 --- /dev/null +++ b/backend/src/main/java/com/iiitinsider/repository/ReviewRepository.java @@ -0,0 +1,12 @@ +package com.iiitinsider.repository; + +import com.iiitinsider.model.Review; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface ReviewRepository extends JpaRepository { + List findByCollegeIdAndIsApprovedTrueOrderByCreatedAtDesc(Long collegeId); +} diff --git a/backend/src/main/java/com/iiitinsider/repository/UserRepository.java b/backend/src/main/java/com/iiitinsider/repository/UserRepository.java new file mode 100644 index 0000000..eb0b528 --- /dev/null +++ b/backend/src/main/java/com/iiitinsider/repository/UserRepository.java @@ -0,0 +1,15 @@ +package com.iiitinsider.repository; + +import com.iiitinsider.model.User; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface UserRepository extends JpaRepository { + Optional findByUsername(String username); + Optional findByEmail(String email); + boolean existsByUsername(String username); + boolean existsByEmail(String email); +} diff --git a/backend/src/main/java/com/iiitinsider/service/AuthService.java b/backend/src/main/java/com/iiitinsider/service/AuthService.java new file mode 100644 index 0000000..badad64 --- /dev/null +++ b/backend/src/main/java/com/iiitinsider/service/AuthService.java @@ -0,0 +1,63 @@ +package com.iiitinsider.service; + +import com.iiitinsider.model.User; +import com.iiitinsider.repository.UserRepository; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; + +@Service +public class AuthService { + + private final UserRepository userRepository; + private final PasswordEncoder passwordEncoder; + private final JwtService jwtService; + private final AuthenticationManager authenticationManager; + + public AuthService(UserRepository userRepository, PasswordEncoder passwordEncoder, + JwtService jwtService, AuthenticationManager authenticationManager) { + this.userRepository = userRepository; + this.passwordEncoder = passwordEncoder; + this.jwtService = jwtService; + this.authenticationManager = authenticationManager; + } + + public User registerUser(String username, String email, String password) { + if (userRepository.existsByUsername(username)) { + throw new RuntimeException("Username already exists"); + } + + if (userRepository.existsByEmail(email)) { + throw new RuntimeException("Email already exists"); + } + + User user = new User(); + user.setUsername(username); + user.setEmail(email); + user.setPassword(passwordEncoder.encode(password)); + + return userRepository.save(user); + } + + public String authenticate(String username, String password) { + authenticationManager.authenticate( + new UsernamePasswordAuthenticationToken(username, password) + ); + + User user = userRepository.findByUsername(username) + .orElseThrow(() -> new RuntimeException("User not found")); + + return jwtService.generateToken( + new org.springframework.security.core.userdetails.User( + user.getUsername(), + user.getPassword(), + java.util.Collections.singletonList( + new org.springframework.security.core.authority.SimpleGrantedAuthority( + "ROLE_" + user.getRole().name() + ) + ) + ) + ); + } +} diff --git a/backend/src/main/java/com/iiitinsider/service/CollegeService.java b/backend/src/main/java/com/iiitinsider/service/CollegeService.java new file mode 100644 index 0000000..693216c --- /dev/null +++ b/backend/src/main/java/com/iiitinsider/service/CollegeService.java @@ -0,0 +1,248 @@ +package com.iiitinsider.service; + +import com.iiitinsider.model.Branch; +import com.iiitinsider.model.College; +import com.iiitinsider.model.Cutoff; +import com.iiitinsider.model.PlacementStatistic; +import com.iiitinsider.model.Program; +import com.iiitinsider.model.Review; +import com.iiitinsider.repository.BranchRepository; +import com.iiitinsider.repository.CollegeRepository; +import com.iiitinsider.repository.CutoffRepository; +import com.iiitinsider.repository.PlacementStatisticRepository; +import com.iiitinsider.repository.ProgramRepository; +import com.iiitinsider.repository.ReviewRepository; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +@Service +public class CollegeService { + + private final CollegeRepository collegeRepository; + private final BranchRepository branchRepository; + private final ProgramRepository programRepository; + private final PlacementStatisticRepository placementStatisticRepository; + private final CutoffRepository cutoffRepository; + private final ReviewRepository reviewRepository; + + public CollegeService(CollegeRepository collegeRepository, + BranchRepository branchRepository, + ProgramRepository programRepository, + PlacementStatisticRepository placementStatisticRepository, + CutoffRepository cutoffRepository, + ReviewRepository reviewRepository) { + this.collegeRepository = collegeRepository; + this.branchRepository = branchRepository; + this.programRepository = programRepository; + this.placementStatisticRepository = placementStatisticRepository; + this.cutoffRepository = cutoffRepository; + this.reviewRepository = reviewRepository; + } + + public List> getColleges(String region) { + List colleges = parseRegion(region) + .map(collegeRepository::findByRegionAndIsActiveTrueOrderByNameAsc) + .orElseGet(collegeRepository::findByIsActiveTrueOrderByNameAsc); + + return colleges.stream() + .map(this::toCollegeCard) + .collect(Collectors.toList()); + } + + public List> searchColleges(String query) { + return collegeRepository.findByNameContainingIgnoreCaseAndIsActiveTrueOrderByNameAsc(query) + .stream() + .map(this::toCollegeCard) + .collect(Collectors.toList()); + } + + public Map getCollegeDetails(String slug) { + College college = findByNameOrSlug(slug) + .orElseThrow(() -> new RuntimeException("College not found")); + + Map details = toCollegeCard(college); + details.put("city", college.getCity()); + details.put("state", college.getState()); + details.put("campusArea", college.getCampusArea()); + details.put("instituteType", college.getInstituteType()); + details.put("establishedYear", college.getEstablishedYear()); + details.put("websiteUrl", college.getWebsiteUrl()); + details.put("mentorInstitute", college.getMentorInstitute()); + details.put("branches", branchRepository.findByCollegeIdOrderByNameAsc(college.getId()).stream() + .map(this::toBranchMap) + .collect(Collectors.toList())); + details.put("programs", programRepository.findByCollegeIdOrderByNameAsc(college.getId()).stream() + .map(this::toProgramMap) + .collect(Collectors.toList())); + details.put("placements", placementStatisticRepository.findByCollegeIdOrderByAcademicYearDesc(college.getId()).stream() + .map(this::toPlacementMap) + .collect(Collectors.toList())); + details.put("cutoffs", cutoffRepository.findByCollegeIdOrderByYearDescRoundAsc(college.getId()).stream() + .map(this::toCutoffMap) + .collect(Collectors.toList())); + details.put("reviews", reviewRepository.findByCollegeIdAndIsApprovedTrueOrderByCreatedAtDesc(college.getId()).stream() + .map(this::toReviewMap) + .collect(Collectors.toList())); + + return details; + } + + public Map getCompareData(String nameOrSlug) { + College college = findByNameOrSlug(nameOrSlug) + .orElseThrow(() -> new RuntimeException("College not found")); + + Map data = new HashMap<>(); + data.put("name", college.getName()); + data.put("year", college.getEstablishedYear() == null ? "" : college.getEstablishedYear().toString()); + data.put("area", college.getCampusArea()); + data.put("type", college.getInstituteType()); + data.put("curr", programRepository.findByCollegeIdOrderByNameAsc(college.getId()).stream() + .map(Program::getName) + .collect(Collectors.joining(", "))); + data.put("las", branchRepository.findByCollegeIdOrderByNameAsc(college.getId()).stream() + .filter(branch -> Boolean.TRUE.equals(branch.getIsPopular())) + .map(Branch::getName) + .findFirst() + .orElse("")); + + placementStatisticRepository.findFirstByCollegeIdOrderByAcademicYearDesc(college.getId()) + .ifPresent(placement -> { + data.put("avg", formatLpa(placement.getAveragePackageLpa())); + data.put("med", formatLpa(placement.getMedianPackageLpa())); + data.put("mea", formatLpa(placement.getHighestPackageLpa())); + data.put("placement", formatPercent(placement.getPlacementPercentage())); + }); + + reviewRepository.findByCollegeIdAndIsApprovedTrueOrderByCreatedAtDesc(college.getId()) + .stream() + .findFirst() + .ifPresent(review -> { + data.put("pros", review.getPros()); + data.put("cons", review.getCons()); + }); + + data.putIfAbsent("avg", ""); + data.putIfAbsent("med", ""); + data.putIfAbsent("mea", ""); + data.putIfAbsent("placement", ""); + data.putIfAbsent("pros", ""); + data.putIfAbsent("cons", ""); + + return data; + } + + private Optional findByNameOrSlug(String value) { + String normalized = value == null ? "" : value.trim(); + if (normalized.isEmpty()) { + return Optional.empty(); + } + + Optional bySlug = collegeRepository.findBySlugIgnoreCase(normalized); + if (bySlug.isPresent()) { + return bySlug; + } + + return collegeRepository.findByNameIgnoreCase(normalized); + } + + private Map toCollegeCard(College college) { + Map data = new HashMap<>(); + data.put("id", college.getId()); + data.put("name", college.getName()); + data.put("slug", college.getSlug()); + data.put("description", college.getDescription()); + data.put("image", college.getImageUrl()); + data.put("region", college.getRegion().name().toLowerCase(Locale.ROOT)); + return data; + } + + private Map toBranchMap(Branch branch) { + Map data = new HashMap<>(); + data.put("id", branch.getId()); + data.put("name", branch.getName()); + data.put("code", branch.getCode()); + data.put("description", branch.getDescription()); + data.put("seats", branch.getSeats()); + data.put("isPopular", branch.getIsPopular()); + return data; + } + + private Map toProgramMap(Program program) { + Map data = new HashMap<>(); + data.put("id", program.getId()); + data.put("name", program.getName()); + data.put("degreeLevel", program.getDegreeLevel().name()); + data.put("durationYears", program.getDurationYears()); + data.put("description", program.getDescription()); + return data; + } + + private Map toPlacementMap(PlacementStatistic placement) { + Map data = new HashMap<>(); + data.put("id", placement.getId()); + data.put("academicYear", placement.getAcademicYear()); + data.put("averagePackageLpa", placement.getAveragePackageLpa()); + data.put("medianPackageLpa", placement.getMedianPackageLpa()); + data.put("highestPackageLpa", placement.getHighestPackageLpa()); + data.put("placementPercentage", placement.getPlacementPercentage()); + data.put("studentsPlaced", placement.getStudentsPlaced()); + data.put("totalEligibleStudents", placement.getTotalEligibleStudents()); + data.put("topRecruiters", placement.getTopRecruiters()); + data.put("sourceUrl", placement.getSourceUrl()); + return data; + } + + private Map toCutoffMap(Cutoff cutoff) { + Map data = new HashMap<>(); + data.put("id", cutoff.getId()); + data.put("branchId", cutoff.getBranch() == null ? null : cutoff.getBranch().getId()); + data.put("year", cutoff.getYear()); + data.put("round", cutoff.getRound()); + data.put("counsellingType", cutoff.getCounsellingType().name()); + data.put("category", cutoff.getCategory()); + data.put("quota", cutoff.getQuota()); + data.put("gender", cutoff.getGender()); + data.put("openingRank", cutoff.getOpeningRank()); + data.put("closingRank", cutoff.getClosingRank()); + return data; + } + + private Map toReviewMap(Review review) { + Map data = new HashMap<>(); + data.put("id", review.getId()); + data.put("title", review.getTitle()); + data.put("body", review.getBody()); + data.put("rating", review.getRating()); + data.put("pros", review.getPros()); + data.put("cons", review.getCons()); + data.put("createdAt", review.getCreatedAt()); + return data; + } + + private Optional parseRegion(String region) { + if (region == null || region.trim().isEmpty() || "all".equalsIgnoreCase(region.trim())) { + return Optional.empty(); + } + + try { + return Optional.of(College.Region.valueOf(region.trim().toUpperCase(Locale.ROOT))); + } catch (IllegalArgumentException ignored) { + return Optional.empty(); + } + } + + private String formatLpa(BigDecimal value) { + return value == null ? "" : value.stripTrailingZeros().toPlainString() + " LPA"; + } + + private String formatPercent(BigDecimal value) { + return value == null ? "" : value.stripTrailingZeros().toPlainString() + "%"; + } +} diff --git a/backend/src/main/java/com/iiitinsider/service/FirebaseNotificationService.java b/backend/src/main/java/com/iiitinsider/service/FirebaseNotificationService.java new file mode 100644 index 0000000..1826e6c --- /dev/null +++ b/backend/src/main/java/com/iiitinsider/service/FirebaseNotificationService.java @@ -0,0 +1,125 @@ +package com.iiitinsider.service; + +import com.google.firebase.messaging.*; +import com.iiitinsider.model.DeviceToken; +import com.iiitinsider.model.User; +import com.iiitinsider.repository.DeviceTokenRepository; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.stereotype.Service; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; + +@Service +public class FirebaseNotificationService { + + private static final Logger log = LoggerFactory.getLogger(FirebaseNotificationService.class); + + private final DeviceTokenRepository deviceTokenRepository; + private final FirebaseMessaging firebaseMessaging; + + public FirebaseNotificationService(DeviceTokenRepository deviceTokenRepository, + ObjectProvider firebaseMessagingProvider) { + this.deviceTokenRepository = deviceTokenRepository; + this.firebaseMessaging = firebaseMessagingProvider.getIfAvailable(); + } + + public void registerDevice(User user, String token, String deviceType, String deviceName) { + long activeDeviceCount = deviceTokenRepository.countByUserIdAndIsActive(user.getId(), true); + + DeviceToken existingToken = deviceTokenRepository.findByToken(token).orElse(null); + if (existingToken != null) { + existingToken.setLastUsedAt(java.time.LocalDateTime.now()); + deviceTokenRepository.save(existingToken); + return; + } + + if (activeDeviceCount >= 5) { + DeviceToken oldestToken = deviceTokenRepository.findByUserId(user.getId()) + .stream() + .filter(DeviceToken::getIsActive) + .min((a, b) -> a.getCreatedAt().compareTo(b.getCreatedAt())) + .orElse(null); + + if (oldestToken != null) { + deviceTokenRepository.delete(oldestToken); + } + } + + DeviceToken deviceToken = new DeviceToken(); + deviceToken.setUser(user); + deviceToken.setToken(token); + deviceToken.setDeviceType(deviceType != null ? deviceType : "WEB"); + deviceToken.setDeviceName(normalizeDeviceName(deviceName)); + deviceToken.setIsActive(true); + + deviceTokenRepository.save(deviceToken); + log.info("Registered device token for user: {}", user.getUsername()); + } + + public String sendNotificationToDevice(String deviceToken, String title, String body, String imageUrl) { + if (firebaseMessaging == null) { + log.warn("Firebase Messaging is not configured"); + return "Firebase not initialized"; + } + + Notification notification = Notification.builder() + .setTitle(title) + .setBody(body) + .setImage(imageUrl) + .build(); + + Message message = Message.builder() + .setNotification(notification) + .setToken(deviceToken) + .putData("type", "notification") + .build(); + + try { + String response = firebaseMessaging.send(message); + log.info("Successfully sent notification: {}", response); + return response; + } catch (FirebaseMessagingException e) { + log.error("Error sending notification: {}", e.getMessage()); + return "Error: " + e.getMessage(); + } + } + + public String sendNotificationToUser(Long userId, String title, String body, String imageUrl) { + List tokens = deviceTokenRepository.findByUserId(userId); + + if (tokens.isEmpty()) { + return "No devices registered for user"; + } + + String results = ""; + for (DeviceToken deviceToken : tokens) { + if (deviceToken.getIsActive()) { + results += sendNotificationToDevice(deviceToken.getToken(), title, body, imageUrl) + "; "; + } + } + + return results; + } + + public void unregisterDevice(String token) { + deviceTokenRepository.findByToken(token).ifPresent(deviceToken -> { + deviceToken.setIsActive(false); + deviceTokenRepository.save(deviceToken); + log.info("Unregistered device token"); + }); + } + + public List getUserDevices(Long userId) { + return deviceTokenRepository.findByUserId(userId); + } + + private String normalizeDeviceName(String deviceName) { + if (deviceName == null || deviceName.isBlank()) { + return "Web browser"; + } + + return deviceName.length() > 100 ? deviceName.substring(0, 100) : deviceName; + } +} diff --git a/backend/src/main/java/com/iiitinsider/service/JwtService.java b/backend/src/main/java/com/iiitinsider/service/JwtService.java new file mode 100644 index 0000000..3ab97e9 --- /dev/null +++ b/backend/src/main/java/com/iiitinsider/service/JwtService.java @@ -0,0 +1,76 @@ +package com.iiitinsider.service; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.security.Keys; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.stereotype.Service; + +import javax.crypto.SecretKey; +import java.nio.charset.StandardCharsets; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; + +@Service +public class JwtService { + + @Value("${jwt.secret}") + private String secretKey; + + @Value("${jwt.expiration-ms}") + private long expirationMs; + + private SecretKey getSigningKey() { + byte[] keyBytes = secretKey.getBytes(StandardCharsets.UTF_8); + return Keys.hmacShaKeyFor(keyBytes); + } + + public String extractUsername(String token) { + return extractClaim(token, Claims::getSubject); + } + + public Date extractExpiration(String token) { + return extractClaim(token, Claims::getExpiration); + } + + public T extractClaim(String token, Function claimsResolver) { + final Claims claims = extractAllClaims(token); + return claimsResolver.apply(claims); + } + + private Claims extractAllClaims(String token) { + return Jwts.parser() + .verifyWith(getSigningKey()) + .build() + .parseSignedClaims(token) + .getPayload(); + } + + private Boolean isTokenExpired(String token) { + return extractExpiration(token).before(new Date()); + } + + public Boolean isTokenValid(String token, UserDetails userDetails) { + final String username = extractUsername(token); + return (username.equals(userDetails.getUsername())) && !isTokenExpired(token); + } + + public String generateToken(UserDetails userDetails) { + Map claims = new HashMap<>(); + return createToken(claims, userDetails.getUsername()); + } + + private String createToken(Map claims, String subject) { + return Jwts.builder() + .claims(claims) + .subject(subject) + .issuedAt(new Date(System.currentTimeMillis())) + .expiration(new Date(System.currentTimeMillis() + expirationMs)) + .signWith(getSigningKey()) + .compact(); + } +} diff --git a/backend/src/main/java/com/iiitinsider/service/SocialMediaService.java b/backend/src/main/java/com/iiitinsider/service/SocialMediaService.java new file mode 100644 index 0000000..7626b0f --- /dev/null +++ b/backend/src/main/java/com/iiitinsider/service/SocialMediaService.java @@ -0,0 +1,64 @@ +package com.iiitinsider.service; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; + +@Service +public class SocialMediaService { + + private static final Logger log = LoggerFactory.getLogger(SocialMediaService.class); + + private static final String FACEBOOK_SHARE_URL = "https://www.facebook.com/sharer/sharer.php?u="; + private static final String TWITTER_SHARE_URL = "https://twitter.com/intent/tweet?text="; + private static final String LINKEDIN_SHARE_URL = "https://www.linkedin.com/sharing/share-offsite/?url="; + private static final String WHATSAPP_SHARE_URL = "https://wa.me/?text="; + private static final String REDDIT_SHARE_URL = "https://reddit.com/submit?url="; + + public Map getShareLinks(String url, String title) { + Map shareLinks = new HashMap<>(); + + String encodedUrl = URLEncoder.encode(url, StandardCharsets.UTF_8); + String encodedTitle = URLEncoder.encode(title, StandardCharsets.UTF_8); + + shareLinks.put("facebook", FACEBOOK_SHARE_URL + encodedUrl); + shareLinks.put("twitter", TWITTER_SHARE_URL + encodedTitle + "&url=" + encodedUrl); + shareLinks.put("linkedin", LINKEDIN_SHARE_URL + encodedUrl); + shareLinks.put("whatsapp", WHATSAPP_SHARE_URL + encodedTitle + "%20" + encodedUrl); + shareLinks.put("reddit", REDDIT_SHARE_URL + encodedUrl + "&title=" + encodedTitle); + + log.info("Generated share links for URL: {}", url); + return shareLinks; + } + + public String generateFacebookShareUrl(String url) { + return FACEBOOK_SHARE_URL + URLEncoder.encode(url, StandardCharsets.UTF_8); + } + + public String generateTwitterShareUrl(String url, String title) { + String encodedUrl = URLEncoder.encode(url, StandardCharsets.UTF_8); + String encodedTitle = URLEncoder.encode(title, StandardCharsets.UTF_8); + return TWITTER_SHARE_URL + encodedTitle + "&url=" + encodedUrl; + } + + public String generateLinkedInShareUrl(String url) { + return LINKEDIN_SHARE_URL + URLEncoder.encode(url, StandardCharsets.UTF_8); + } + + public String generateWhatsAppShareUrl(String url, String title) { + String encodedUrl = URLEncoder.encode(url, StandardCharsets.UTF_8); + String encodedTitle = URLEncoder.encode(title, StandardCharsets.UTF_8); + return WHATSAPP_SHARE_URL + encodedTitle + "%20" + encodedUrl; + } + + public String generateRedditShareUrl(String url, String title) { + String encodedUrl = URLEncoder.encode(url, StandardCharsets.UTF_8); + String encodedTitle = URLEncoder.encode(title, StandardCharsets.UTF_8); + return REDDIT_SHARE_URL + encodedUrl + "&title=" + encodedTitle; + } +} diff --git a/backend/src/main/java/com/iiitinsider/service/UserDetailsServiceImpl.java b/backend/src/main/java/com/iiitinsider/service/UserDetailsServiceImpl.java new file mode 100644 index 0000000..39cef1f --- /dev/null +++ b/backend/src/main/java/com/iiitinsider/service/UserDetailsServiceImpl.java @@ -0,0 +1,33 @@ +package com.iiitinsider.service; + +import com.iiitinsider.model.User; +import com.iiitinsider.repository.UserRepository; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; + +import java.util.Collections; + +@Service +public class UserDetailsServiceImpl implements UserDetailsService { + + private final UserRepository userRepository; + + public UserDetailsServiceImpl(UserRepository userRepository) { + this.userRepository = userRepository; + } + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + User user = userRepository.findByUsername(username) + .orElseThrow(() -> new UsernameNotFoundException("User not found: " + username)); + + return new org.springframework.security.core.userdetails.User( + user.getUsername(), + user.getPassword(), + Collections.singletonList(new SimpleGrantedAuthority("ROLE_" + user.getRole().name())) + ); + } +} diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties new file mode 100644 index 0000000..d5ba78e --- /dev/null +++ b/backend/src/main/resources/application.properties @@ -0,0 +1,38 @@ +# Server Configuration +spring.config.import=optional:file:.env[.properties] +server.port=${SERVER_PORT:8080} +server.servlet.context-path=${SERVER_CONTEXT_PATH:/api} + +# Database Configuration +spring.datasource.url=${MYSQL_URL:jdbc:mysql://localhost:3306/iiit_insider?createDatabaseIfNotExist=true&useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC} +spring.datasource.username=${MYSQL_USER:root} +spring.datasource.password=${MYSQL_PASSWORD:} +spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver + +# JPA Configuration +spring.jpa.hibernate.ddl-auto=${JPA_DDL_AUTO:validate} +spring.jpa.show-sql=${JPA_SHOW_SQL:false} +spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect +spring.jpa.properties.hibernate.format_sql=${JPA_FORMAT_SQL:false} + +# SQL Initialization +spring.sql.init.mode=${SQL_INIT_MODE:always} +spring.sql.init.schema-locations=classpath:schema.sql +spring.sql.init.data-locations=classpath:data.sql +spring.sql.init.continue-on-error=false + +# JWT Configuration +jwt.secret=${JWT_SECRET:} +jwt.expiration-ms=${JWT_EXPIRATION_MS:86400000} +jwt.refresh-expiration-ms=${JWT_REFRESH_EXPIRATION_MS:604800000} + +# Firebase Configuration +firebase.credentials.path=${FIREBASE_CREDENTIALS_PATH:} +firebase.database.url=${FIREBASE_DATABASE_URL:} + +# CORS Configuration +app.cors.allowed-origins=${CORS_ALLOWED_ORIGINS:http://localhost:5173,http://localhost:3000} + +# Logging +logging.level.com.iiitinsider=${APP_LOG_LEVEL:INFO} +logging.level.org.springframework.security=${SECURITY_LOG_LEVEL:WARN} diff --git a/backend/src/main/resources/data.sql b/backend/src/main/resources/data.sql new file mode 100644 index 0000000..1bc5bf9 --- /dev/null +++ b/backend/src/main/resources/data.sql @@ -0,0 +1,102 @@ +INSERT INTO colleges ( + id, name, slug, short_name, description, image_url, region, city, state, + campus_area, institute_type, established_year, website_url, mentor_institute, + is_active, created_at, updated_at +) VALUES +(1, 'IIIT Allahabad', 'iiit-allahabad', 'IIITA', 'A centrally funded institute known for information technology, electronics, research culture, and strong placements.', '/src/assets/allahabad.jpeg', 'NORTH', 'Prayagraj', 'Uttar Pradesh', '100 acres', 'Institute of National Importance', 1999, 'https://www.iiita.ac.in', NULL, b'1', NOW(6), NOW(6)), +(2, 'ABV-IIITM Gwalior', 'iiitm-gwalior', 'IIITM Gwalior', 'An institute combining technology and management education with established computer science and integrated programs.', '/src/assets/gwalior.jpeg', 'CENTRAL', 'Gwalior', 'Madhya Pradesh', '160 acres', 'Institute of National Importance', 1997, 'https://www.iiitm.ac.in', NULL, b'1', NOW(6), NOW(6)), +(3, 'IIIT Hyderabad', 'iiit-hyderabad', 'IIITH', 'A research-led institute with deep strengths in computer science, AI, language technologies, robotics, and data systems.', '/src/assets/hyderabad.jpeg', 'SOUTH', 'Hyderabad', 'Telangana', '66 acres', 'Deemed University', 1998, 'https://www.iiit.ac.in', NULL, b'1', NOW(6), NOW(6)), +(4, 'IIIT Delhi', 'iiit-delhi', 'IIITD', 'A state university focused on computer science, electronics, design, social computing, and interdisciplinary research.', '/src/assets/delhi.jpeg', 'NORTH', 'New Delhi', 'Delhi', '25 acres', 'State University', 2008, 'https://www.iiitd.ac.in', NULL, b'1', NOW(6), NOW(6)), +(5, 'IIIT Bangalore', 'iiit-bangalore', 'IIITB', 'A postgraduate and research-focused institute in Bengaluru with strong industry alignment and computing programs.', '/src/assets/banglore.webp', 'SOUTH', 'Bengaluru', 'Karnataka', '9 acres', 'Deemed University', 1999, 'https://www.iiitb.ac.in', NULL, b'1', NOW(6), NOW(6)), +(6, 'IIIT Lucknow', 'iiit-lucknow', 'IIITL', 'A fast-growing institute offering computer science, information technology, and AI-oriented programs.', '/src/assets/lucknow.jpeg', 'NORTH', 'Lucknow', 'Uttar Pradesh', '50 acres', 'Institute of National Importance', 2015, 'https://iiitl.ac.in', 'IIIT Allahabad', b'1', NOW(6), NOW(6)) +ON DUPLICATE KEY UPDATE + description = VALUES(description), + image_url = VALUES(image_url), + updated_at = NOW(6); + +INSERT INTO branches (id, college_id, name, code, description, seats, is_popular, created_at, updated_at) VALUES +(1, 1, 'Information Technology', 'IT', 'Core computing program focused on software systems, networks, and applied information technology.', 120, b'1', NOW(6), NOW(6)), +(2, 1, 'Electronics and Communication Engineering', 'ECE', 'Electronics, communication systems, signal processing, and embedded systems.', 75, b'0', NOW(6), NOW(6)), +(3, 2, 'Computer Science and Engineering', 'CSE', 'Foundational and advanced computing with systems, algorithms, and software engineering.', 90, b'1', NOW(6), NOW(6)), +(4, 2, 'Integrated B.Tech + MBA', 'IT-MBA', 'Technology and management integrated program for product and business leadership roles.', 60, b'0', NOW(6), NOW(6)), +(5, 3, 'Computer Science and Engineering', 'CSE', 'Research-intensive computer science program with strong AI and systems exposure.', 120, b'1', NOW(6), NOW(6)), +(6, 3, 'Electronics and Communication Engineering', 'ECE', 'Electronics, VLSI, signal processing, and communication systems.', 80, b'0', NOW(6), NOW(6)), +(7, 4, 'Computer Science and Applied Mathematics', 'CSAM', 'Computing foundations combined with mathematical modeling and analytics.', 80, b'1', NOW(6), NOW(6)), +(8, 4, 'Computer Science and Design', 'CSD', 'Computer science blended with human-centered design and digital product thinking.', 70, b'0', NOW(6), NOW(6)), +(9, 5, 'Integrated M.Tech Computer Science', 'IMT-CSE', 'Five-year integrated computing program with industry and research orientation.', 150, b'1', NOW(6), NOW(6)), +(10, 6, 'Computer Science and Engineering', 'CSE', 'Modern computing curriculum covering software, algorithms, and applied AI.', 75, b'1', NOW(6), NOW(6)), +(11, 6, 'Computer Science and Artificial Intelligence', 'CSAI', 'Computer science program with focused AI and machine learning coursework.', 60, b'0', NOW(6), NOW(6)) +ON DUPLICATE KEY UPDATE + description = VALUES(description), + seats = VALUES(seats), + is_popular = VALUES(is_popular), + updated_at = NOW(6); + +INSERT INTO programs (id, college_id, name, degree_level, duration_years, description, created_at, updated_at) VALUES +(1, 1, 'B.Tech Information Technology', 'BTECH', 4, 'Undergraduate program in IT, software engineering, systems, and applied computing.', NOW(6), NOW(6)), +(2, 1, 'M.Tech Information Technology', 'MTECH', 2, 'Postgraduate specialization in advanced computing areas.', NOW(6), NOW(6)), +(3, 2, 'B.Tech Computer Science and Engineering', 'BTECH', 4, 'Undergraduate computer science program.', NOW(6), NOW(6)), +(4, 2, 'Integrated B.Tech + MBA', 'DUAL_DEGREE', 5, 'Integrated technology and management degree.', NOW(6), NOW(6)), +(5, 3, 'B.Tech Computer Science and Engineering', 'BTECH', 4, 'Research-oriented undergraduate program in computer science.', NOW(6), NOW(6)), +(6, 3, 'MS by Research', 'MSC', 2, 'Research degree across computing and electronics domains.', NOW(6), NOW(6)), +(7, 4, 'B.Tech Computer Science and Applied Mathematics', 'BTECH', 4, 'Computing program with mathematical foundations.', NOW(6), NOW(6)), +(8, 5, 'Integrated M.Tech Computer Science', 'MTECH', 5, 'Integrated postgraduate computing program.', NOW(6), NOW(6)), +(9, 6, 'B.Tech Computer Science and Engineering', 'BTECH', 4, 'Undergraduate computer science program.', NOW(6), NOW(6)) +ON DUPLICATE KEY UPDATE + description = VALUES(description), + duration_years = VALUES(duration_years), + updated_at = NOW(6); + +INSERT INTO placement_statistics ( + id, college_id, academic_year, average_package_lpa, median_package_lpa, + highest_package_lpa, placement_percentage, students_placed, total_eligible_students, + top_recruiters, source_url, created_at, updated_at +) VALUES +(1, 1, '2023-24', 30.68, 25.78, 121.00, 96.20, 410, 426, 'Google, Microsoft, Atlassian, Adobe, Amazon', 'https://www.iiita.ac.in', NOW(6), NOW(6)), +(2, 2, '2023-24', 24.31, 20.00, 65.00, 91.50, 265, 290, 'Amazon, Goldman Sachs, Oracle, Samsung, Deloitte', 'https://www.iiitm.ac.in', NOW(6), NOW(6)), +(3, 3, '2023-24', 32.20, 28.00, 69.00, 98.00, 360, 367, 'Google, Microsoft, Apple, Qualcomm, Nvidia', 'https://www.iiit.ac.in', NOW(6), NOW(6)), +(4, 4, '2023-24', 23.72, 20.80, 49.00, 94.10, 470, 499, 'Microsoft, Adobe, Google, Flipkart, Tower Research', 'https://www.iiitd.ac.in', NOW(6), NOW(6)), +(5, 5, '2023-24', 24.88, 22.00, 65.00, 95.00, 180, 190, 'LinkedIn, Infosys, Accenture, Mercedes-Benz, Intel', 'https://www.iiitb.ac.in', NOW(6), NOW(6)), +(6, 6, '2023-24', 18.42, 16.00, 45.00, 89.50, 135, 151, 'Amazon, Flipkart, Paytm, Infosys, TCS Digital', 'https://iiitl.ac.in', NOW(6), NOW(6)) +ON DUPLICATE KEY UPDATE + average_package_lpa = VALUES(average_package_lpa), + median_package_lpa = VALUES(median_package_lpa), + highest_package_lpa = VALUES(highest_package_lpa), + placement_percentage = VALUES(placement_percentage), + updated_at = NOW(6); + +INSERT INTO cutoffs ( + id, college_id, branch_id, year, round, counselling_type, category, quota, + gender, opening_rank, closing_rank, created_at, updated_at +) VALUES +(1, 1, 1, 2024, 6, 'JOSAA', 'OPEN', 'AI', 'Gender-Neutral', 850, 4900, NOW(6), NOW(6)), +(2, 1, 2, 2024, 6, 'JOSAA', 'OPEN', 'AI', 'Gender-Neutral', 5200, 9100, NOW(6), NOW(6)), +(3, 2, 3, 2024, 6, 'JOSAA', 'OPEN', 'AI', 'Gender-Neutral', 4100, 8200, NOW(6), NOW(6)), +(4, 2, 4, 2024, 6, 'JOSAA', 'OPEN', 'AI', 'Gender-Neutral', 9000, 14500, NOW(6), NOW(6)), +(5, 3, 5, 2024, 1, 'INSTITUTE', 'OPEN', 'AI', 'Gender-Neutral', 400, 2200, NOW(6), NOW(6)), +(6, 3, 6, 2024, 1, 'INSTITUTE', 'OPEN', 'AI', 'Gender-Neutral', 2300, 6200, NOW(6), NOW(6)), +(7, 4, 7, 2024, 5, 'JOSAA', 'OPEN', 'HS', 'Gender-Neutral', 2100, 7600, NOW(6), NOW(6)), +(8, 4, 8, 2024, 5, 'JOSAA', 'OPEN', 'HS', 'Gender-Neutral', 5000, 11800, NOW(6), NOW(6)), +(9, 5, 9, 2024, 1, 'INSTITUTE', 'OPEN', 'AI', 'Gender-Neutral', 3000, 9800, NOW(6), NOW(6)), +(10, 6, 10, 2024, 6, 'JOSAA', 'OPEN', 'AI', 'Gender-Neutral', 7600, 12800, NOW(6), NOW(6)), +(11, 6, 11, 2024, 6, 'JOSAA', 'OPEN', 'AI', 'Gender-Neutral', 8200, 14100, NOW(6), NOW(6)) +ON DUPLICATE KEY UPDATE + opening_rank = VALUES(opening_rank), + closing_rank = VALUES(closing_rank), + updated_at = NOW(6); + +INSERT INTO reviews ( + id, college_id, user_id, title, body, rating, pros, cons, is_approved, created_at, updated_at +) VALUES +(1, 1, NULL, 'Strong coding culture', 'Students report strong peer learning, active coding groups, and good placement support.', 5, 'Excellent coding culture, strong alumni network, high placement ceiling.', 'Academic workload can feel intense during project-heavy semesters.', b'1', NOW(6), NOW(6)), +(2, 2, NULL, 'Balanced tech and management exposure', 'The institute suits students who want computing with management and product exposure.', 4, 'Large campus, integrated programs, good return on investment.', 'Some programs are less specialized than pure CSE tracks.', b'1', NOW(6), NOW(6)), +(3, 3, NULL, 'Research-first environment', 'A strong fit for students interested in research, labs, and advanced CS work.', 5, 'Excellent research groups, selective peer group, industry recognition.', 'Admissions and coursework are highly competitive.', b'1', NOW(6), NOW(6)), +(4, 4, NULL, 'Modern curriculum', 'Curriculum is flexible and current, with useful interdisciplinary options.', 4, 'Good city access, modern courses, strong faculty profiles.', 'Campus size is smaller compared with older institutes.', b'1', NOW(6), NOW(6)), +(5, 5, NULL, 'Industry-connected programs', 'Location and industry collaboration make it attractive for postgraduate computing.', 4, 'Bengaluru ecosystem, mature postgraduate programs, good internships.', 'Limited traditional large-campus experience.', b'1', NOW(6), NOW(6)), +(6, 6, NULL, 'Growing institute with momentum', 'A younger IIIT with improving placements and expanding academic offerings.', 4, 'Good growth trajectory, focused CS programs, improving brand recall.', 'Infrastructure and alumni network are still maturing.', b'1', NOW(6), NOW(6)) +ON DUPLICATE KEY UPDATE + body = VALUES(body), + pros = VALUES(pros), + cons = VALUES(cons), + is_approved = VALUES(is_approved), + updated_at = NOW(6); diff --git a/backend/src/main/resources/schema.sql b/backend/src/main/resources/schema.sql new file mode 100644 index 0000000..21a48e4 --- /dev/null +++ b/backend/src/main/resources/schema.sql @@ -0,0 +1,130 @@ +CREATE TABLE IF NOT EXISTS users ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + username VARCHAR(50) NOT NULL UNIQUE, + email VARCHAR(100) NOT NULL UNIQUE, + password VARCHAR(255) NOT NULL, + phone_number VARCHAR(20), + profile_image_url VARCHAR(255), + role ENUM('USER', 'ADMIN') NOT NULL DEFAULT 'USER', + status ENUM('ACTIVE', 'INACTIVE', 'SUSPENDED') DEFAULT 'ACTIVE', + created_at DATETIME(6), + updated_at DATETIME(6), + last_login DATETIME(6), + INDEX idx_users_username (username), + INDEX idx_users_email (email) +); + +CREATE TABLE IF NOT EXISTS colleges ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(150) NOT NULL UNIQUE, + slug VARCHAR(180) UNIQUE, + short_name VARCHAR(150), + description TEXT, + image_url VARCHAR(500), + region ENUM('NORTH', 'SOUTH', 'EAST', 'WEST', 'CENTRAL', 'OTHER') NOT NULL DEFAULT 'OTHER', + city VARCHAR(100), + state VARCHAR(100), + campus_area VARCHAR(100), + institute_type VARCHAR(80), + established_year INT, + website_url VARCHAR(500), + mentor_institute VARCHAR(150), + is_active BIT(1) NOT NULL DEFAULT b'1', + created_at DATETIME(6), + updated_at DATETIME(6), + INDEX idx_colleges_region_active (region, is_active), + INDEX idx_colleges_slug (slug) +); + +CREATE TABLE IF NOT EXISTS branches ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + college_id BIGINT NOT NULL, + name VARCHAR(120) NOT NULL, + code VARCHAR(30) NOT NULL, + description TEXT, + seats INT, + is_popular BIT(1) NOT NULL DEFAULT b'0', + created_at DATETIME(6), + updated_at DATETIME(6), + CONSTRAINT uk_branches_college_code UNIQUE (college_id, code), + CONSTRAINT fk_branches_college FOREIGN KEY (college_id) REFERENCES colleges(id) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS programs ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + college_id BIGINT NOT NULL, + name VARCHAR(120) NOT NULL, + degree_level ENUM('BTECH', 'MTECH', 'DUAL_DEGREE', 'MBA', 'MSC', 'MDES', 'BDES', 'PHD', 'OTHER') NOT NULL DEFAULT 'OTHER', + duration_years INT, + description TEXT, + created_at DATETIME(6), + updated_at DATETIME(6), + CONSTRAINT fk_programs_college FOREIGN KEY (college_id) REFERENCES colleges(id) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS placement_statistics ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + college_id BIGINT NOT NULL, + academic_year VARCHAR(20) NOT NULL, + average_package_lpa DECIMAL(10, 2), + median_package_lpa DECIMAL(10, 2), + highest_package_lpa DECIMAL(10, 2), + placement_percentage DECIMAL(5, 2), + students_placed INT, + total_eligible_students INT, + top_recruiters TEXT, + source_url TEXT, + created_at DATETIME(6), + updated_at DATETIME(6), + CONSTRAINT uk_placements_college_year UNIQUE (college_id, academic_year), + CONSTRAINT fk_placements_college FOREIGN KEY (college_id) REFERENCES colleges(id) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS cutoffs ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + college_id BIGINT NOT NULL, + branch_id BIGINT, + year INT NOT NULL, + round INT NOT NULL, + counselling_type ENUM('JOSAA', 'CSAB', 'DASA', 'UCEED', 'INSTITUTE', 'OTHER') NOT NULL DEFAULT 'JOSAA', + category VARCHAR(60), + quota VARCHAR(20), + gender VARCHAR(20), + opening_rank INT, + closing_rank INT, + created_at DATETIME(6), + updated_at DATETIME(6), + CONSTRAINT fk_cutoffs_college FOREIGN KEY (college_id) REFERENCES colleges(id) ON DELETE CASCADE, + CONSTRAINT fk_cutoffs_branch FOREIGN KEY (branch_id) REFERENCES branches(id) ON DELETE SET NULL +); + +CREATE TABLE IF NOT EXISTS reviews ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + college_id BIGINT NOT NULL, + user_id BIGINT, + title VARCHAR(150) NOT NULL, + body TEXT, + rating INT NOT NULL, + pros TEXT, + cons TEXT, + is_approved BIT(1) NOT NULL DEFAULT b'0', + created_at DATETIME(6), + updated_at DATETIME(6), + CONSTRAINT fk_reviews_college FOREIGN KEY (college_id) REFERENCES colleges(id) ON DELETE CASCADE, + CONSTRAINT fk_reviews_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL +); + +CREATE TABLE IF NOT EXISTS device_tokens ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + user_id BIGINT NOT NULL, + token VARCHAR(500) NOT NULL, + device_type VARCHAR(255) NOT NULL DEFAULT 'WEB', + device_name VARCHAR(100), + is_active BIT(1) NOT NULL DEFAULT b'1', + created_at DATETIME(6), + last_used_at DATETIME(6), + max_devices_limit INT DEFAULT 5, + CONSTRAINT fk_device_tokens_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, + INDEX idx_device_tokens_user_id (user_id), + INDEX idx_device_tokens_token (token) +); diff --git a/backend/src/test/java/com/iiitinsider/controller/AuthControllerIntegrationTest.java b/backend/src/test/java/com/iiitinsider/controller/AuthControllerIntegrationTest.java new file mode 100644 index 0000000..3751a0c --- /dev/null +++ b/backend/src/test/java/com/iiitinsider/controller/AuthControllerIntegrationTest.java @@ -0,0 +1,110 @@ +package com.iiitinsider.controller; + +import com.iiitinsider.config.CorsConfig; +import com.iiitinsider.config.JwtAuthenticationFilter; +import com.iiitinsider.config.SecurityConfig; +import com.iiitinsider.repository.UserRepository; +import com.iiitinsider.service.AuthService; +import com.iiitinsider.service.JwtService; +import com.iiitinsider.service.UserDetailsServiceImpl; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.test.context.TestExecutionListeners; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.event.ApplicationEventsTestExecutionListener; +import org.springframework.test.context.event.EventPublishingTestExecutionListener; +import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; +import org.springframework.test.context.support.DirtiesContextBeforeModesTestExecutionListener; +import org.springframework.test.context.support.DirtiesContextTestExecutionListener; +import org.springframework.test.context.web.ServletTestExecutionListener; +import org.springframework.test.web.servlet.MockMvc; + +import java.lang.reflect.Proxy; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; + +@WebMvcTest(controllers = AuthController.class) +@Import({ + SecurityConfig.class, + CorsConfig.class, + JwtAuthenticationFilter.class, + AuthControllerIntegrationTest.TestBeans.class +}) +@TestPropertySource(properties = { + "app.cors.allowed-origins=http://localhost:5173", + "jwt.secret=test-secret-that-is-at-least-thirty-two-characters" +}) +@TestExecutionListeners( + listeners = { + ServletTestExecutionListener.class, + DirtiesContextBeforeModesTestExecutionListener.class, + ApplicationEventsTestExecutionListener.class, + DependencyInjectionTestExecutionListener.class, + DirtiesContextTestExecutionListener.class, + EventPublishingTestExecutionListener.class + }, + mergeMode = TestExecutionListeners.MergeMode.REPLACE_DEFAULTS +) +class AuthControllerIntegrationTest { + + @Autowired + private MockMvc mockMvc; + + @Test + void unauthenticatedUserCannotAccessNotificationsEndpoint() throws Exception { + mockMvc.perform(get("/notifications/devices")) + .andExpect(result -> { + int status = result.getResponse().getStatus(); + assertTrue(status == 401 || status == 403, "Expected 401 or 403 but got " + status); + }); + } + + @TestConfiguration + static class TestBeans { + @Bean + AuthService authService() { + return new AuthService(null, null, null, null); + } + + @Bean + JwtService jwtService() { + return new JwtService(); + } + + @Bean + UserDetailsServiceImpl userDetailsService() { + return new UserDetailsServiceImpl(null) { + @Override + public org.springframework.security.core.userdetails.UserDetails loadUserByUsername(String username) { + throw new UsernameNotFoundException(username); + } + }; + } + + @Bean + UserRepository userRepository() { + return (UserRepository) Proxy.newProxyInstance( + UserRepository.class.getClassLoader(), + new Class[] { UserRepository.class }, + (proxy, method, args) -> { + if (method.getName().equals("toString")) { + return "testUserRepository"; + } + if (method.getName().equals("hashCode")) { + return System.identityHashCode(proxy); + } + if (method.getName().equals("equals")) { + return proxy == args[0]; + } + throw new UnsupportedOperationException("UserRepository is not used in this security test"); + } + ); + } + } +} diff --git a/eslint.config.js b/eslint.config.js index 4fa125d..8480719 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,29 +1,29 @@ -import js from '@eslint/js' -import globals from 'globals' -import reactHooks from 'eslint-plugin-react-hooks' -import reactRefresh from 'eslint-plugin-react-refresh' -import { defineConfig, globalIgnores } from 'eslint/config' - -export default defineConfig([ - globalIgnores(['dist']), - { - files: ['**/*.{js,jsx}'], - extends: [ - js.configs.recommended, - reactHooks.configs.flat.recommended, - reactRefresh.configs.vite, - ], - languageOptions: { - ecmaVersion: 2020, - globals: globals.browser, - parserOptions: { - ecmaVersion: 'latest', - ecmaFeatures: { jsx: true }, - sourceType: 'module', - }, - }, - rules: { - 'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }], - }, - }, -]) +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import { defineConfig, globalIgnores } from 'eslint/config' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{js,jsx}'], + extends: [ + js.configs.recommended, + reactHooks.configs.flat.recommended, + reactRefresh.configs.vite, + ], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + parserOptions: { + ecmaVersion: 'latest', + ecmaFeatures: { jsx: true }, + sourceType: 'module', + }, + }, + rules: { + 'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }], + }, + }, +]) diff --git a/firebase.json b/firebase.json new file mode 100644 index 0000000..2c33c29 --- /dev/null +++ b/firebase.json @@ -0,0 +1,16 @@ +{ + "hosting": { + "public": "dist", + "ignore": [ + "firebase.json", + "**/.*", + "**/node_modules/**" + ], + "rewrites": [ + { + "source": "**", + "destination": "/index.html" + } + ] + } +} diff --git a/index.html b/index.html index c25774e..23da08a 100644 --- a/index.html +++ b/index.html @@ -1,13 +1,13 @@ - - - - - - - iiit_insider - - -
- - - + + + + + + + iiit_insider + + +
+ + + diff --git a/nginx/nginx.conf b/nginx/nginx.conf new file mode 100644 index 0000000..ab3b75f --- /dev/null +++ b/nginx/nginx.conf @@ -0,0 +1,55 @@ +upstream spring_backend { + server 127.0.0.1:8080; + keepalive 32; +} + +server { + listen 80; + server_name yourdomain.com www.yourdomain.com; + + root /var/www/iiit-insider/dist; + index index.html; + + access_log /var/log/nginx/iiit-insider-access.log; + error_log /var/log/nginx/iiit-insider-error.log warn; + + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always; + + gzip on; + gzip_vary on; + gzip_min_length 1024; + gzip_types + application/javascript + application/json + application/xml + image/svg+xml + text/css + text/javascript + text/plain + text/xml; + + location /api/ { + proxy_pass http://spring_backend; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_connect_timeout 30s; + proxy_send_timeout 60s; + proxy_read_timeout 60s; + } + + location ~* \.(?:js|css|png|jpg|jpeg|gif|webp|ico|svg|woff2?|ttf|eot)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + try_files $uri =404; + } + + location / { + try_files $uri $uri/ /index.html; + } +} diff --git a/package-lock.json b/package-lock.json index 0ee6272..b460b32 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,3504 +1,4570 @@ -{ - "name": "iiit_insider", - "version": "0.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "iiit_insider", - "version": "0.0.0", - "dependencies": { - "@tailwindcss/vite": "^4.2.1", - "react": "^19.2.0", - "react-dom": "^19.2.0", - "react-router-dom": "^7.13.1", - "tailwindcss": "^4.2.1" - }, - "devDependencies": { - "@eslint/js": "^9.39.1", - "@types/react": "^19.2.5", - "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^5.1.1", - "babel-plugin-react-compiler": "^1.0.0", - "eslint": "^9.39.1", - "eslint-plugin-react-hooks": "^7.0.1", - "eslint-plugin-react-refresh": "^0.4.24", - "globals": "^16.5.0", - "vite": "^7.2.4" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", - "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.28.5", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/compat-data": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", - "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", - "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/code-frame": "^7.29.0", - "@babel/generator": "^7.29.0", - "@babel/helper-compilation-targets": "^7.28.6", - "@babel/helper-module-transforms": "^7.28.6", - "@babel/helpers": "^7.28.6", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/traverse": "^7.29.0", - "@babel/types": "^7.29.0", - "@jridgewell/remapping": "^2.3.5", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/generator": { - "version": "7.29.1", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", - "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.29.0", - "@babel/types": "^7.29.0", - "@jridgewell/gen-mapping": "^0.3.12", - "@jridgewell/trace-mapping": "^0.3.28", - "jsesc": "^3.0.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", - "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.28.6", - "@babel/helper-validator-option": "^7.27.1", - "browserslist": "^4.24.0", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-globals": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", - "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", - "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.28.6", - "@babel/types": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", - "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.28.6", - "@babel/helper-validator-identifier": "^7.28.5", - "@babel/traverse": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", - "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-option": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", - "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", - "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.28.6", - "@babel/types": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", - "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.29.0" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-self": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", - "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-source": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", - "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/template": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", - "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.28.6", - "@babel/parser": "^7.28.6", - "@babel/types": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", - "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.29.0", - "@babel/generator": "^7.29.0", - "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/types": "^7.29.0", - "debug": "^4.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/types": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", - "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz", - "integrity": "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==", - "cpu": [ - "ppc64" - ], - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.4.tgz", - "integrity": "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.4.tgz", - "integrity": "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.4.tgz", - "integrity": "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.4.tgz", - "integrity": "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.4.tgz", - "integrity": "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.4.tgz", - "integrity": "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.4.tgz", - "integrity": "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.4.tgz", - "integrity": "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.4.tgz", - "integrity": "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.4.tgz", - "integrity": "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.4.tgz", - "integrity": "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==", - "cpu": [ - "loong64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.4.tgz", - "integrity": "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==", - "cpu": [ - "mips64el" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.4.tgz", - "integrity": "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==", - "cpu": [ - "ppc64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.4.tgz", - "integrity": "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==", - "cpu": [ - "riscv64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.4.tgz", - "integrity": "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==", - "cpu": [ - "s390x" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.4.tgz", - "integrity": "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.4.tgz", - "integrity": "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.4.tgz", - "integrity": "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.4.tgz", - "integrity": "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.4.tgz", - "integrity": "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.4.tgz", - "integrity": "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.4.tgz", - "integrity": "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.4.tgz", - "integrity": "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.4.tgz", - "integrity": "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.4.tgz", - "integrity": "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==", - "cpu": [ - "x64" - ], - "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/eslint-utils/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-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/config-array": { - "version": "0.21.2", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", - "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/object-schema": "^2.1.7", - "debug": "^4.3.1", - "minimatch": "^3.1.5" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/config-helpers": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", - "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^0.17.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/core": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", - "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", - "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.14.0", - "debug": "^4.3.2", - "espree": "^10.0.1", - "globals": "^14.0.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.1", - "minimatch": "^3.1.5", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@eslint/js": { - "version": "9.39.4", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", - "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" - } - }, - "node_modules/@eslint/object-schema": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", - "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/plugin-kit": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", - "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^0.17.0", - "levn": "^0.4.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@humanfs/core": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", - "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/node": { - "version": "0.16.7", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", - "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanfs/core": "^0.19.1", - "@humanwhocodes/retry": "^0.4.0" - }, - "engines": { - "node": ">=18.18.0" - } - }, - "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/retry": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", - "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/remapping": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", - "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-rc.3", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz", - "integrity": "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", - "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", - "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", - "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", - "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", - "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", - "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", - "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", - "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", - "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", - "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", - "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", - "cpu": [ - "loong64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", - "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", - "cpu": [ - "loong64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", - "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", - "cpu": [ - "ppc64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", - "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", - "cpu": [ - "ppc64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", - "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", - "cpu": [ - "riscv64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", - "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", - "cpu": [ - "riscv64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", - "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", - "cpu": [ - "s390x" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", - "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", - "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", - "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ] - }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", - "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", - "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", - "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", - "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", - "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@tailwindcss/node": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.1.tgz", - "integrity": "sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg==", - "license": "MIT", - "dependencies": { - "@jridgewell/remapping": "^2.3.5", - "enhanced-resolve": "^5.19.0", - "jiti": "^2.6.1", - "lightningcss": "1.31.1", - "magic-string": "^0.30.21", - "source-map-js": "^1.2.1", - "tailwindcss": "4.2.1" - } - }, - "node_modules/@tailwindcss/oxide": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.1.tgz", - "integrity": "sha512-yv9jeEFWnjKCI6/T3Oq50yQEOqmpmpfzG1hcZsAOaXFQPfzWprWrlHSdGPEF3WQTi8zu8ohC9Mh9J470nT5pUw==", - "license": "MIT", - "engines": { - "node": ">= 20" - }, - "optionalDependencies": { - "@tailwindcss/oxide-android-arm64": "4.2.1", - "@tailwindcss/oxide-darwin-arm64": "4.2.1", - "@tailwindcss/oxide-darwin-x64": "4.2.1", - "@tailwindcss/oxide-freebsd-x64": "4.2.1", - "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.1", - "@tailwindcss/oxide-linux-arm64-gnu": "4.2.1", - "@tailwindcss/oxide-linux-arm64-musl": "4.2.1", - "@tailwindcss/oxide-linux-x64-gnu": "4.2.1", - "@tailwindcss/oxide-linux-x64-musl": "4.2.1", - "@tailwindcss/oxide-wasm32-wasi": "4.2.1", - "@tailwindcss/oxide-win32-arm64-msvc": "4.2.1", - "@tailwindcss/oxide-win32-x64-msvc": "4.2.1" - } - }, - "node_modules/@tailwindcss/oxide-android-arm64": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.1.tgz", - "integrity": "sha512-eZ7G1Zm5EC8OOKaesIKuw77jw++QJ2lL9N+dDpdQiAB/c/B2wDh0QPFHbkBVrXnwNugvrbJFk1gK2SsVjwWReg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-darwin-arm64": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.1.tgz", - "integrity": "sha512-q/LHkOstoJ7pI1J0q6djesLzRvQSIfEto148ppAd+BVQK0JYjQIFSK3JgYZJa+Yzi0DDa52ZsQx2rqytBnf8Hw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-darwin-x64": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.1.tgz", - "integrity": "sha512-/f/ozlaXGY6QLbpvd/kFTro2l18f7dHKpB+ieXz+Cijl4Mt9AI2rTrpq7V+t04nK+j9XBQHnSMdeQRhbGyt6fw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-freebsd-x64": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.1.tgz", - "integrity": "sha512-5e/AkgYJT/cpbkys/OU2Ei2jdETCLlifwm7ogMC7/hksI2fC3iiq6OcXwjibcIjPung0kRtR3TxEITkqgn0TcA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.1.tgz", - "integrity": "sha512-Uny1EcVTTmerCKt/1ZuKTkb0x8ZaiuYucg2/kImO5A5Y/kBz41/+j0gxUZl+hTF3xkWpDmHX+TaWhOtba2Fyuw==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.1.tgz", - "integrity": "sha512-CTrwomI+c7n6aSSQlsPL0roRiNMDQ/YzMD9EjcR+H4f0I1SQ8QqIuPnsVp7QgMkC1Qi8rtkekLkOFjo7OlEFRQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm64-musl": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.1.tgz", - "integrity": "sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-linux-x64-gnu": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.1.tgz", - "integrity": "sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-linux-x64-musl": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.1.tgz", - "integrity": "sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.1.tgz", - "integrity": "sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q==", - "bundleDependencies": [ - "@napi-rs/wasm-runtime", - "@emnapi/core", - "@emnapi/runtime", - "@tybys/wasm-util", - "@emnapi/wasi-threads", - "tslib" - ], - "cpu": [ - "wasm32" - ], - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "^1.8.1", - "@emnapi/runtime": "^1.8.1", - "@emnapi/wasi-threads": "^1.1.0", - "@napi-rs/wasm-runtime": "^1.1.1", - "@tybys/wasm-util": "^0.10.1", - "tslib": "^2.8.1" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.1.tgz", - "integrity": "sha512-YlUEHRHBGnCMh4Nj4GnqQyBtsshUPdiNroZj8VPkvTZSoHsilRCwXcVKnG9kyi0ZFAS/3u+qKHBdDc81SADTRA==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-win32-x64-msvc": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.1.tgz", - "integrity": "sha512-rbO34G5sMWWyrN/idLeVxAZgAKWrn5LiR3/I90Q9MkA67s6T1oB0xtTe+0heoBvHSpbU9Mk7i6uwJnpo4u21XQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/vite": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.2.1.tgz", - "integrity": "sha512-TBf2sJjYeb28jD2U/OhwdW0bbOsxkWPwQ7SrqGf9sVcoYwZj7rkXljroBO9wKBut9XnmQLXanuDUeqQK0lGg/w==", - "license": "MIT", - "dependencies": { - "@tailwindcss/node": "4.2.1", - "@tailwindcss/oxide": "4.2.1", - "tailwindcss": "4.2.1" - }, - "peerDependencies": { - "vite": "^5.2.0 || ^6 || ^7" - } - }, - "node_modules/@types/babel__core": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", - "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" - } - }, - "node_modules/@types/babel__generator": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", - "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__template": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", - "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__traverse": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", - "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.28.2" - } - }, - "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "license": "MIT" - }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/react": { - "version": "19.2.14", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", - "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "csstype": "^3.2.2" - } - }, - "node_modules/@types/react-dom": { - "version": "19.2.3", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", - "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "@types/react": "^19.2.0" - } - }, - "node_modules/@vitejs/plugin-react": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.2.0.tgz", - "integrity": "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.29.0", - "@babel/plugin-transform-react-jsx-self": "^7.27.1", - "@babel/plugin-transform-react-jsx-source": "^7.27.1", - "@rolldown/pluginutils": "1.0.0-rc.3", - "@types/babel__core": "^7.20.5", - "react-refresh": "^0.18.0" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "peerDependencies": { - "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/acorn": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", - "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", - "dev": true, - "license": "MIT", - "peer": true, - "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/ajv": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", - "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", - "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-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/babel-plugin-react-compiler": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/babel-plugin-react-compiler/-/babel-plugin-react-compiler-1.0.0.tgz", - "integrity": "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.26.0" - } - }, - "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/baseline-browser-mapping": { - "version": "2.10.7", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.7.tgz", - "integrity": "sha512-1ghYO3HnxGec0TCGBXiDLVns4eCSx4zJpxnHrlqFQajmhfKMQBzUGDdkMK7fUW7PTHTeLf+j87aTuKuuwWzMGw==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "baseline-browser-mapping": "dist/cli.cjs" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/browserslist": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", - "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "peer": true, - "dependencies": { - "baseline-browser-mapping": "^2.9.0", - "caniuse-lite": "^1.0.30001759", - "electron-to-chromium": "^1.5.263", - "node-releases": "^2.0.27", - "update-browserslist-db": "^1.2.0" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "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/caniuse-lite": { - "version": "1.0.30001778", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001778.tgz", - "integrity": "sha512-PN7uxFL+ExFJO61aVmP1aIEG4i9whQd4eoSCebav62UwDyp5OHh06zN4jqKSMePVgxHifCw1QJxdRkA1Pisekg==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" - }, - "node_modules/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/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/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true, - "license": "MIT" - }, - "node_modules/cookie": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", - "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "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/csstype": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", - "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/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/electron-to-chromium": { - "version": "1.5.313", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.313.tgz", - "integrity": "sha512-QBMrTWEf00GXZmJyx2lbYD45jpI3TUFnNIzJ5BBc8piGUDwMPa1GV6HJWTZVvY/eiN3fSopl7NRbgGp9sZ9LTA==", - "dev": true, - "license": "ISC" - }, - "node_modules/enhanced-resolve": { - "version": "5.20.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.0.tgz", - "integrity": "sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ==", - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.4", - "tapable": "^2.3.0" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/esbuild": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.4.tgz", - "integrity": "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==", - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.4", - "@esbuild/android-arm": "0.27.4", - "@esbuild/android-arm64": "0.27.4", - "@esbuild/android-x64": "0.27.4", - "@esbuild/darwin-arm64": "0.27.4", - "@esbuild/darwin-x64": "0.27.4", - "@esbuild/freebsd-arm64": "0.27.4", - "@esbuild/freebsd-x64": "0.27.4", - "@esbuild/linux-arm": "0.27.4", - "@esbuild/linux-arm64": "0.27.4", - "@esbuild/linux-ia32": "0.27.4", - "@esbuild/linux-loong64": "0.27.4", - "@esbuild/linux-mips64el": "0.27.4", - "@esbuild/linux-ppc64": "0.27.4", - "@esbuild/linux-riscv64": "0.27.4", - "@esbuild/linux-s390x": "0.27.4", - "@esbuild/linux-x64": "0.27.4", - "@esbuild/netbsd-arm64": "0.27.4", - "@esbuild/netbsd-x64": "0.27.4", - "@esbuild/openbsd-arm64": "0.27.4", - "@esbuild/openbsd-x64": "0.27.4", - "@esbuild/openharmony-arm64": "0.27.4", - "@esbuild/sunos-x64": "0.27.4", - "@esbuild/win32-arm64": "0.27.4", - "@esbuild/win32-ia32": "0.27.4", - "@esbuild/win32-x64": "0.27.4" - } - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "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": "9.39.4", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", - "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@eslint-community/eslint-utils": "^4.8.0", - "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.21.2", - "@eslint/config-helpers": "^0.4.2", - "@eslint/core": "^0.17.0", - "@eslint/eslintrc": "^3.3.5", - "@eslint/js": "9.39.4", - "@eslint/plugin-kit": "^0.4.1", - "@humanfs/node": "^0.16.6", - "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.2", - "@types/estree": "^1.0.6", - "ajv": "^6.14.0", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.6", - "debug": "^4.3.2", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.4.0", - "eslint-visitor-keys": "^4.2.1", - "espree": "^10.4.0", - "esquery": "^1.5.0", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^8.0.0", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.5", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" - }, - "peerDependencies": { - "jiti": "*" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - } - } - }, - "node_modules/eslint-plugin-react-hooks": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz", - "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.24.4", - "@babel/parser": "^7.24.4", - "hermes-parser": "^0.25.1", - "zod": "^3.25.0 || ^4.0.0", - "zod-validation-error": "^3.5.0 || ^4.0.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" - } - }, - "node_modules/eslint-plugin-react-refresh": { - "version": "0.4.26", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.26.tgz", - "integrity": "sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "eslint": ">=8.40" - } - }, - "node_modules/eslint-scope": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", - "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/espree": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", - "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.15.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.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/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-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/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/file-entry-cache": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", - "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "flat-cache": "^4.0.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "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": "4.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", - "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.4" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/flatted": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.1.tgz", - "integrity": "sha512-IxfVbRFVlV8V/yRaGzk0UVIcsKKHMSfYw66T/u4nTwlWteQePsxe//LjudR1AMX4tZW3WFCh3Zqa/sjlqpbURQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "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/globals": { - "version": "16.5.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz", - "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "license": "ISC" - }, - "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/hermes-estree": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", - "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", - "dev": true, - "license": "MIT" - }, - "node_modules/hermes-parser": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", - "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", - "dev": true, - "license": "MIT", - "dependencies": { - "hermes-estree": "0.25.1" - } - }, - "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/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/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/jiti": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", - "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", - "license": "MIT", - "bin": { - "jiti": "lib/jiti-cli.mjs" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "dev": true, - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, - "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/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "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/lightningcss": { - "version": "1.31.1", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.31.1.tgz", - "integrity": "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ==", - "license": "MPL-2.0", - "dependencies": { - "detect-libc": "^2.0.3" - }, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - }, - "optionalDependencies": { - "lightningcss-android-arm64": "1.31.1", - "lightningcss-darwin-arm64": "1.31.1", - "lightningcss-darwin-x64": "1.31.1", - "lightningcss-freebsd-x64": "1.31.1", - "lightningcss-linux-arm-gnueabihf": "1.31.1", - "lightningcss-linux-arm64-gnu": "1.31.1", - "lightningcss-linux-arm64-musl": "1.31.1", - "lightningcss-linux-x64-gnu": "1.31.1", - "lightningcss-linux-x64-musl": "1.31.1", - "lightningcss-win32-arm64-msvc": "1.31.1", - "lightningcss-win32-x64-msvc": "1.31.1" - } - }, - "node_modules/lightningcss-android-arm64": { - "version": "1.31.1", - "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.31.1.tgz", - "integrity": "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg==", - "cpu": [ - "arm64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-arm64": { - "version": "1.31.1", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.31.1.tgz", - "integrity": "sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg==", - "cpu": [ - "arm64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-x64": { - "version": "1.31.1", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.31.1.tgz", - "integrity": "sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA==", - "cpu": [ - "x64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-freebsd-x64": { - "version": "1.31.1", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.31.1.tgz", - "integrity": "sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A==", - "cpu": [ - "x64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.31.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.31.1.tgz", - "integrity": "sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g==", - "cpu": [ - "arm" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.31.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.31.1.tgz", - "integrity": "sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg==", - "cpu": [ - "arm64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.31.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.31.1.tgz", - "integrity": "sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==", - "cpu": [ - "arm64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.31.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.31.1.tgz", - "integrity": "sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==", - "cpu": [ - "x64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-x64-musl": { - "version": "1.31.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.31.1.tgz", - "integrity": "sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==", - "cpu": [ - "x64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.31.1", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.31.1.tgz", - "integrity": "sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==", - "cpu": [ - "arm64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.31.1", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.31.1.tgz", - "integrity": "sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw==", - "cpu": [ - "x64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "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/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/magic-string": { - "version": "0.30.21", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", - "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.5" - } - }, - "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/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/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/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/node-releases": { - "version": "2.0.36", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", - "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==", - "dev": true, - "license": "MIT" - }, - "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/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-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/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/postcss": { - "version": "8.5.8", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", - "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.11", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "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/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/react": { - "version": "19.2.4", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", - "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-dom": { - "version": "19.2.4", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", - "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "scheduler": "^0.27.0" - }, - "peerDependencies": { - "react": "^19.2.4" - } - }, - "node_modules/react-refresh": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", - "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-router": { - "version": "7.13.1", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.13.1.tgz", - "integrity": "sha512-td+xP4X2/6BJvZoX6xw++A2DdEi++YypA69bJUV5oVvqf6/9/9nNlD70YO1e9d3MyamJEBQFEzk6mbfDYbqrSA==", - "license": "MIT", - "dependencies": { - "cookie": "^1.0.1", - "set-cookie-parser": "^2.6.0" - }, - "engines": { - "node": ">=20.0.0" - }, - "peerDependencies": { - "react": ">=18", - "react-dom": ">=18" - }, - "peerDependenciesMeta": { - "react-dom": { - "optional": true - } - } - }, - "node_modules/react-router-dom": { - "version": "7.13.1", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.13.1.tgz", - "integrity": "sha512-UJnV3Rxc5TgUPJt2KJpo1Jpy0OKQr0AjgbZzBFjaPJcFOb2Y8jA5H3LT8HUJAiRLlWrEXWHbF1Z4SCZaQjWDHw==", - "license": "MIT", - "dependencies": { - "react-router": "7.13.1" - }, - "engines": { - "node": ">=20.0.0" - }, - "peerDependencies": { - "react": ">=18", - "react-dom": ">=18" - } - }, - "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/rollup": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", - "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", - "license": "MIT", - "dependencies": { - "@types/estree": "1.0.8" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.59.0", - "@rollup/rollup-android-arm64": "4.59.0", - "@rollup/rollup-darwin-arm64": "4.59.0", - "@rollup/rollup-darwin-x64": "4.59.0", - "@rollup/rollup-freebsd-arm64": "4.59.0", - "@rollup/rollup-freebsd-x64": "4.59.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", - "@rollup/rollup-linux-arm-musleabihf": "4.59.0", - "@rollup/rollup-linux-arm64-gnu": "4.59.0", - "@rollup/rollup-linux-arm64-musl": "4.59.0", - "@rollup/rollup-linux-loong64-gnu": "4.59.0", - "@rollup/rollup-linux-loong64-musl": "4.59.0", - "@rollup/rollup-linux-ppc64-gnu": "4.59.0", - "@rollup/rollup-linux-ppc64-musl": "4.59.0", - "@rollup/rollup-linux-riscv64-gnu": "4.59.0", - "@rollup/rollup-linux-riscv64-musl": "4.59.0", - "@rollup/rollup-linux-s390x-gnu": "4.59.0", - "@rollup/rollup-linux-x64-gnu": "4.59.0", - "@rollup/rollup-linux-x64-musl": "4.59.0", - "@rollup/rollup-openbsd-x64": "4.59.0", - "@rollup/rollup-openharmony-arm64": "4.59.0", - "@rollup/rollup-win32-arm64-msvc": "4.59.0", - "@rollup/rollup-win32-ia32-msvc": "4.59.0", - "@rollup/rollup-win32-x64-gnu": "4.59.0", - "@rollup/rollup-win32-x64-msvc": "4.59.0", - "fsevents": "~2.3.2" - } - }, - "node_modules/scheduler": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", - "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", - "license": "MIT" - }, - "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/set-cookie-parser": { - "version": "2.7.2", - "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", - "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", - "license": "MIT" - }, - "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/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "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/tailwindcss": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.1.tgz", - "integrity": "sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw==", - "license": "MIT" - }, - "node_modules/tapable": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", - "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", - "license": "MIT", - "engines": { - "node": ">=6" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/tinyglobby": { - "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", - "license": "MIT", - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.3" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "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/update-browserslist-db": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", - "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.1" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/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/vite": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", - "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", - "license": "MIT", - "peer": true, - "dependencies": { - "esbuild": "^0.27.0", - "fdir": "^6.5.0", - "picomatch": "^4.0.3", - "postcss": "^8.5.6", - "rollup": "^4.43.0", - "tinyglobby": "^0.2.15" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^20.19.0 || >=22.12.0", - "jiti": ">=1.21.0", - "less": "^4.0.0", - "lightningcss": "^1.21.0", - "sass": "^1.70.0", - "sass-embedded": "^1.70.0", - "stylus": ">=0.54.8", - "sugarss": "^5.0.0", - "terser": "^5.16.0", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "jiti": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - }, - "node_modules/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/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true, - "license": "ISC" - }, - "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" - } - }, - "node_modules/zod": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", - "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", - "dev": true, - "license": "MIT", - "peer": true, - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, - "node_modules/zod-validation-error": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", - "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.0.0" - }, - "peerDependencies": { - "zod": "^3.25.0 || ^4.0.0" - } - } - } -} +{ + "name": "iiit_insider", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "iiit_insider", + "version": "0.0.0", + "dependencies": { + "@tailwindcss/vite": "^4.2.1", + "firebase": "^12.12.1", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "react-router-dom": "^7.13.1", + "tailwindcss": "^4.2.1" + }, + "devDependencies": { + "@eslint/js": "^9.39.1", + "@types/react": "^19.2.5", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.1", + "babel-plugin-react-compiler": "^1.0.0", + "eslint": "^9.39.1", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "globals": "^16.5.0", + "vite": "^7.2.4" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz", + "integrity": "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.4.tgz", + "integrity": "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.4.tgz", + "integrity": "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.4.tgz", + "integrity": "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.4.tgz", + "integrity": "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.4.tgz", + "integrity": "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.4.tgz", + "integrity": "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.4.tgz", + "integrity": "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.4.tgz", + "integrity": "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.4.tgz", + "integrity": "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.4.tgz", + "integrity": "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.4.tgz", + "integrity": "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.4.tgz", + "integrity": "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.4.tgz", + "integrity": "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.4.tgz", + "integrity": "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.4.tgz", + "integrity": "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.4.tgz", + "integrity": "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.4.tgz", + "integrity": "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.4.tgz", + "integrity": "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.4.tgz", + "integrity": "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.4.tgz", + "integrity": "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.4.tgz", + "integrity": "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.4.tgz", + "integrity": "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.4.tgz", + "integrity": "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.4.tgz", + "integrity": "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.4.tgz", + "integrity": "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==", + "cpu": [ + "x64" + ], + "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/eslint-utils/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-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/config-array": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", + "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", + "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@firebase/ai": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@firebase/ai/-/ai-2.11.1.tgz", + "integrity": "sha512-WGTF81W3WBKJY+c7xqTzO15OGAkCAs8cpADqflAI0skhTZjIkhF0qyf55rq4Ctt6jKygkv99rPfMrjAHTgXaVQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-check-interop-types": "0.3.3", + "@firebase/component": "0.7.2", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.15.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x", + "@firebase/app-types": "0.x" + } + }, + "node_modules/@firebase/analytics": { + "version": "0.10.21", + "resolved": "https://registry.npmjs.org/@firebase/analytics/-/analytics-0.10.21.tgz", + "integrity": "sha512-j2y2q65BlgLGB5Pwjhv/Jopw2X/TBTzvAtI5z/DSp56U4wBj7LfhBfzbdCtFPges+Wz0g55GdoawXibOH5jGng==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.2", + "@firebase/installations": "0.6.21", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.15.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/analytics-compat": { + "version": "0.2.27", + "resolved": "https://registry.npmjs.org/@firebase/analytics-compat/-/analytics-compat-0.2.27.tgz", + "integrity": "sha512-ZObpYpAxL6JfgH7GnvlDD0sbzGZ0o4nijV8skatV9ZX49hJtCYbFqaEcPYptT94rgX1KUoKEderC7/fa7hybtw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/analytics": "0.10.21", + "@firebase/analytics-types": "0.8.3", + "@firebase/component": "0.7.2", + "@firebase/util": "1.15.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/analytics-types": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/@firebase/analytics-types/-/analytics-types-0.8.3.tgz", + "integrity": "sha512-VrIp/d8iq2g501qO46uGz3hjbDb8xzYMrbu8Tp0ovzIzrvJZ2fvmj649gTjge/b7cCCcjT0H37g1gVtlNhnkbg==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/app": { + "version": "0.14.11", + "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.14.11.tgz", + "integrity": "sha512-yxADFW35LYkP8oSGobGsYIrI42I+GPCvKTNHx4meT9Yq3C950IVz1eANoBk822I9tbKv1wyv9P4Bv1G5TpucFw==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@firebase/component": "0.7.2", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.15.0", + "idb": "7.1.1", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/app-check": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/@firebase/app-check/-/app-check-0.11.2.tgz", + "integrity": "sha512-jcXQVMHAQ5AEKzVD5C7s5fmAYeFOuN6lAJeNTgZK2B9aLnofWaJt8u1A8Idm8gpsBBYSaY3cVyeH5SWMOVPBLQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.2", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.15.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/app-check-compat": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@firebase/app-check-compat/-/app-check-compat-0.4.2.tgz", + "integrity": "sha512-M91NhxqbSkI0ChkJWy69blC+rPr6HEgaeRllddSaU1pQ/7IiegeCQM9pPDIgvWnwnBSzKhUHpe6ro/jhJ+cvzw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-check": "0.11.2", + "@firebase/app-check-types": "0.5.3", + "@firebase/component": "0.7.2", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.15.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/app-check-interop-types": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@firebase/app-check-interop-types/-/app-check-interop-types-0.3.3.tgz", + "integrity": "sha512-gAlxfPLT2j8bTI/qfe3ahl2I2YcBQ8cFIBdhAQA4I2f3TndcO+22YizyGYuttLHPQEpWkhmpFW60VCFEPg4g5A==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/app-check-types": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@firebase/app-check-types/-/app-check-types-0.5.3.tgz", + "integrity": "sha512-hyl5rKSj0QmwPdsAxrI5x1otDlByQ7bvNvVt8G/XPO2CSwE++rmSVf3VEhaeOR4J8ZFaF0Z0NDSmLejPweZ3ng==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/app-compat": { + "version": "0.5.11", + "resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.5.11.tgz", + "integrity": "sha512-KaACDjXkK5VLpI01vEs592R7/8s5DjFdIXfKoR385ly1SmK3Tu+jMHCIB4MsiY5jsez6v7VlEX/3rJ90dVkHyA==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@firebase/app": "0.14.11", + "@firebase/component": "0.7.2", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.15.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/app-types": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.4.tgz", + "integrity": "sha512-crX9TA5SVYZwLPG7/R16IsH8FLlgkPXjJUVhsVpHVDSqJiq3D/NuFTM5ctxGTExXAOeIn//69tQw47CPerM8MQ==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@firebase/logger": "0.5.0" + } + }, + "node_modules/@firebase/auth": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-1.13.0.tgz", + "integrity": "sha512-mKkSLNym3UbnnZ06dAmtqzp5EpPGCANGCZDJbkoR135aoUdKG6Aizwcnp29RzsQpwH0nmy5nay17Sfbsh9oY8A==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.2", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.15.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x", + "@react-native-async-storage/async-storage": "^2.2.0 || ^3.0.0" + }, + "peerDependenciesMeta": { + "@react-native-async-storage/async-storage": { + "optional": true + } + } + }, + "node_modules/@firebase/auth-compat": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/@firebase/auth-compat/-/auth-compat-0.6.5.tgz", + "integrity": "sha512-IfVsafZ3QiXbsydXTP/XMI0wVYbJLI1rkb8Qqf03/h5FnL+upbbPOb+6Yj3RpcX+Y1iP5Uh18lxTHlXfbiyAow==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/auth": "1.13.0", + "@firebase/auth-types": "0.13.0", + "@firebase/component": "0.7.2", + "@firebase/util": "1.15.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/auth-interop-types": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.2.4.tgz", + "integrity": "sha512-JPgcXKCuO+CWqGDnigBtvo09HeBs5u/Ktc2GaFj2m01hLarbxthLNm7Fk8iOP1aqAtXV+fnnGj7U28xmk7IwVA==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/auth-types": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@firebase/auth-types/-/auth-types-0.13.0.tgz", + "integrity": "sha512-S/PuIjni0AQRLF+l9ck0YpsMOdE8GO2KU6ubmBB7P+7TJUCQDa3R1dlgYm9UzGbbePMZsp0xzB93f2b/CgxMOg==", + "license": "Apache-2.0", + "peerDependencies": { + "@firebase/app-types": "0.x", + "@firebase/util": "1.x" + } + }, + "node_modules/@firebase/component": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.7.2.tgz", + "integrity": "sha512-iyVDGc6Vjx7Rm0cAdccLH/NG6fADsgJak/XW9IA2lPf8AjIlsemOpFGKczYyPHxm4rnKdR8z6sK4+KEC7NwmEg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/util": "1.15.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/data-connect": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@firebase/data-connect/-/data-connect-0.6.0.tgz", + "integrity": "sha512-OiugPRcdlhqXF97oR9CjVObILmsWU0dFUS0gXNYEe4bDfpW8pZmQ5GqhIPPtLWbT/0W2lMJJD7VILFMk+xuHPg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/auth-interop-types": "0.2.4", + "@firebase/component": "0.7.2", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.15.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/database": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@firebase/database/-/database-1.1.2.tgz", + "integrity": "sha512-lP96CMjMPy/+d1d9qaaHjHHdzdwvEOuyyLq9ehX89e2XMKwS1jHNzYBO+42bdSumuj5ukPbmnFtViZu8YOMT+w==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-check-interop-types": "0.3.3", + "@firebase/auth-interop-types": "0.2.4", + "@firebase/component": "0.7.2", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.15.0", + "faye-websocket": "0.11.4", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/database-compat": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-2.1.3.tgz", + "integrity": "sha512-GMyfWjD8mehjg/QpNkY/tl9G/MoeugPeg91n9D0atggxbWuKF/2KhVPHZDH+XmoP0EKYqMWYTtKxBsaBaNKLYQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.2", + "@firebase/database": "1.1.2", + "@firebase/database-types": "1.0.19", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.15.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/database-types": { + "version": "1.0.19", + "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-1.0.19.tgz", + "integrity": "sha512-FqewjUZmV9LqFfuEnmgdcUpiOUz7qwLXxnm/H8BcMFEzQXtd1yyUDm8ex5VRad2nuTE+ahOuCjUAM/cyDncO+g==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-types": "0.9.4", + "@firebase/util": "1.15.0" + } + }, + "node_modules/@firebase/firestore": { + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/@firebase/firestore/-/firestore-4.14.0.tgz", + "integrity": "sha512-bZc6YOjRkMBVA16527tgzi6iN9n//xRB3Mmx/R+Gr6UAP/+xrIKOejQIcn1hh+tCzNT8jO0jI+kWox5J4tB/qQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.2", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.15.0", + "@firebase/webchannel-wrapper": "1.0.5", + "@grpc/grpc-js": "~1.9.0", + "@grpc/proto-loader": "^0.7.8", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/firestore-compat": { + "version": "0.4.8", + "resolved": "https://registry.npmjs.org/@firebase/firestore-compat/-/firestore-compat-0.4.8.tgz", + "integrity": "sha512-WK9NJRpnosGD2nuyjdr7K+Ht7AxRYJlTF62myI4rRA7ibJOosbecvjacR5oirJ7s1BgNS6qzcBw7n4fD3a5w1w==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.2", + "@firebase/firestore": "4.14.0", + "@firebase/firestore-types": "3.0.3", + "@firebase/util": "1.15.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/firestore-types": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@firebase/firestore-types/-/firestore-types-3.0.3.tgz", + "integrity": "sha512-hD2jGdiWRxB/eZWF89xcK9gF8wvENDJkzpVFb4aGkzfEaKxVRD1kjz1t1Wj8VZEp2LCB53Yx1zD8mrhQu87R6Q==", + "license": "Apache-2.0", + "peerDependencies": { + "@firebase/app-types": "0.x", + "@firebase/util": "1.x" + } + }, + "node_modules/@firebase/functions": { + "version": "0.13.3", + "resolved": "https://registry.npmjs.org/@firebase/functions/-/functions-0.13.3.tgz", + "integrity": "sha512-csO7ckK3SSs+NUZW1nms9EK7ckHe/1QOjiP8uAkCYa7ND18s44vjE9g3KxEeIUpyEPqZaX1EhJuFyZjHigAcYw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-check-interop-types": "0.3.3", + "@firebase/auth-interop-types": "0.2.4", + "@firebase/component": "0.7.2", + "@firebase/messaging-interop-types": "0.2.3", + "@firebase/util": "1.15.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/functions-compat": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@firebase/functions-compat/-/functions-compat-0.4.3.tgz", + "integrity": "sha512-BxkEwWgx1of0tKaao/r2VR6WBLk/RAiyztatiONPrPE8gkitFkOnOCxf8i9cUyA5hX5RGt5H30uNn25Q6QNEmQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.2", + "@firebase/functions": "0.13.3", + "@firebase/functions-types": "0.6.3", + "@firebase/util": "1.15.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/functions-types": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/@firebase/functions-types/-/functions-types-0.6.3.tgz", + "integrity": "sha512-EZoDKQLUHFKNx6VLipQwrSMh01A1SaL3Wg6Hpi//x6/fJ6Ee4hrAeswK99I5Ht8roiniKHw4iO0B1Oxj5I4plg==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/installations": { + "version": "0.6.21", + "resolved": "https://registry.npmjs.org/@firebase/installations/-/installations-0.6.21.tgz", + "integrity": "sha512-xGFGTeICJZ5vhrmmDukeczIcFULFXybojML2+QSDFoKj5A7zbGN7KzFGSKNhDkIxpjzsYG9IleJyUebuAcmqWA==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.2", + "@firebase/util": "1.15.0", + "idb": "7.1.1", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/installations-compat": { + "version": "0.2.21", + "resolved": "https://registry.npmjs.org/@firebase/installations-compat/-/installations-compat-0.2.21.tgz", + "integrity": "sha512-zahIUkaVKbR8zmTeBHkdfaVl6JGWlhVoSjF7CVH33nFqD3SlPEpEEegn2GNT5iAfsVdtlCyJJ9GW4YKjq+RJKQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.2", + "@firebase/installations": "0.6.21", + "@firebase/installations-types": "0.5.3", + "@firebase/util": "1.15.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/installations-types": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@firebase/installations-types/-/installations-types-0.5.3.tgz", + "integrity": "sha512-2FJI7gkLqIE0iYsNQ1P751lO3hER+Umykel+TkLwHj6plzWVxqvfclPUZhcKFVQObqloEBTmpi2Ozn7EkCABAA==", + "license": "Apache-2.0", + "peerDependencies": { + "@firebase/app-types": "0.x" + } + }, + "node_modules/@firebase/logger": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.5.0.tgz", + "integrity": "sha512-cGskaAvkrnh42b3BA3doDWeBmuHFO/Mx5A83rbRDYakPjO9bJtRL3dX7javzc2Rr/JHZf4HlterTW2lUkfeN4g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/messaging": { + "version": "0.12.25", + "resolved": "https://registry.npmjs.org/@firebase/messaging/-/messaging-0.12.25.tgz", + "integrity": "sha512-7RhDwoDHlOK1/ou0/LeubxmjcngsTjDdrY/ssg2vwAVpUuVAhQzQvuCAOYxcX5wNC1zCgQ54AP1vdngBwbCmOQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.2", + "@firebase/installations": "0.6.21", + "@firebase/messaging-interop-types": "0.2.3", + "@firebase/util": "1.15.0", + "idb": "7.1.1", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/messaging-compat": { + "version": "0.2.25", + "resolved": "https://registry.npmjs.org/@firebase/messaging-compat/-/messaging-compat-0.2.25.tgz", + "integrity": "sha512-eoOQqGLtRlseTdiemTN44LlHZpltK5gnhq8XVUuLgtIOG+odtDzrz2UoTpcJWSzaJQVxNLb/x9f39tHdDM4N4w==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.2", + "@firebase/messaging": "0.12.25", + "@firebase/util": "1.15.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/messaging-interop-types": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@firebase/messaging-interop-types/-/messaging-interop-types-0.2.3.tgz", + "integrity": "sha512-xfzFaJpzcmtDjycpDeCUj0Ge10ATFi/VHVIvEEjDNc3hodVBQADZ7BWQU7CuFpjSHE+eLuBI13z5F/9xOoGX8Q==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/performance": { + "version": "0.7.11", + "resolved": "https://registry.npmjs.org/@firebase/performance/-/performance-0.7.11.tgz", + "integrity": "sha512-V3uAhrz7IYJuji+OgT3qYTGKxpek/TViXti9OSsUJ4AexZ3jQjYH5Yrn7JvBxk8MGiSLsC872hh+BxQiPZsm7g==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.2", + "@firebase/installations": "0.6.21", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.15.0", + "tslib": "^2.1.0", + "web-vitals": "^4.2.4" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/performance-compat": { + "version": "0.2.24", + "resolved": "https://registry.npmjs.org/@firebase/performance-compat/-/performance-compat-0.2.24.tgz", + "integrity": "sha512-YRlejH8wLt7ThWao+HXoKUHUrZKGYq+otxkPS+8nuE5PeN1cBXX7NAJl9ueuUkBwMIrnKdnDqL/voHXxDAAt3g==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.2", + "@firebase/logger": "0.5.0", + "@firebase/performance": "0.7.11", + "@firebase/performance-types": "0.2.3", + "@firebase/util": "1.15.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/performance-types": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@firebase/performance-types/-/performance-types-0.2.3.tgz", + "integrity": "sha512-IgkyTz6QZVPAq8GSkLYJvwSLr3LS9+V6vNPQr0x4YozZJiLF5jYixj0amDtATf1X0EtYHqoPO48a9ija8GocxQ==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/remote-config": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/@firebase/remote-config/-/remote-config-0.8.2.tgz", + "integrity": "sha512-5EXqOThV4upjK9D38d/qOSVwOqRhemlaOFk9vCkMNNALeIlwr+4pLjtLNo4qoY8etQmU/1q4aIATE9N8PFqg0g==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.2", + "@firebase/installations": "0.6.21", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.15.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/remote-config-compat": { + "version": "0.2.23", + "resolved": "https://registry.npmjs.org/@firebase/remote-config-compat/-/remote-config-compat-0.2.23.tgz", + "integrity": "sha512-4+KqRRHEUUmKT6tFmnpWATOsaFfmSuBs1jXH8JzVtMLEYqq/WS9IDM92OdefFDSrAA2xGd0WN004z8mKeIIscw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.2", + "@firebase/logger": "0.5.0", + "@firebase/remote-config": "0.8.2", + "@firebase/remote-config-types": "0.5.0", + "@firebase/util": "1.15.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/remote-config-types": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@firebase/remote-config-types/-/remote-config-types-0.5.0.tgz", + "integrity": "sha512-vI3bqLoF14L/GchtgayMiFpZJF+Ao3uR8WCde0XpYNkSokDpAKca2DxvcfeZv7lZUqkUwQPL2wD83d3vQ4vvrg==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/storage": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/@firebase/storage/-/storage-0.14.2.tgz", + "integrity": "sha512-o/culaTeJ8GRpKXRJov21rux/n9dRaSOWLebyatFP2sqEdCxQPjVA1H9Z2fzYwQxMIU0JVmC7SPPmU11v7L6vQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.2", + "@firebase/util": "1.15.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/storage-compat": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@firebase/storage-compat/-/storage-compat-0.4.2.tgz", + "integrity": "sha512-R+aB38wxCH5zjIO/xu9KznI7fgiPuZAG98uVm1NcidHyyupGgIDLKigGmRGBZMnxibe/m2oxNKoZpfEbUX2aQQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.2", + "@firebase/storage": "0.14.2", + "@firebase/storage-types": "0.8.3", + "@firebase/util": "1.15.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/storage-types": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/@firebase/storage-types/-/storage-types-0.8.3.tgz", + "integrity": "sha512-+Muk7g9uwngTpd8xn9OdF/D48uiQ7I1Fae7ULsWPuKoCH3HU7bfFPhxtJYzyhjdniowhuDpQcfPmuNRAqZEfvg==", + "license": "Apache-2.0", + "peerDependencies": { + "@firebase/app-types": "0.x", + "@firebase/util": "1.x" + } + }, + "node_modules/@firebase/util": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.15.0.tgz", + "integrity": "sha512-AmWf3cHAOMbrCPG4xdPKQaj5iHnyYfyLKZxwz+Xf55bqKbpAmcYifB4jQinT2W9XhDRHISOoPyBOariJpCG6FA==", + "hasInstallScript": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/webchannel-wrapper": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@firebase/webchannel-wrapper/-/webchannel-wrapper-1.0.5.tgz", + "integrity": "sha512-+uGNN7rkfn41HLO0vekTFhTxk61eKa8mTpRGLO0QSqlQdKvIoGAvLp3ppdVIWbTGYJWM6Kp0iN+PjMIOcnVqTw==", + "license": "Apache-2.0" + }, + "node_modules/@grpc/grpc-js": { + "version": "1.9.15", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.9.15.tgz", + "integrity": "sha512-nqE7Hc0AzI+euzUwDAy0aY5hCp10r734gMGRdU+qOPX0XSceI2ULrcXB5U2xSc5VkWwalCj4M7GzCAygZl2KoQ==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/proto-loader": "^0.7.8", + "@types/node": ">=12.12.47" + }, + "engines": { + "node": "^8.13.0 || >=10.10.0" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.7.15", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.15.tgz", + "integrity": "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==", + "license": "Apache-2.0", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.2.5", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "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/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz", + "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.1.tgz", + "integrity": "sha512-mnzgDV26ueAvk7rsbt9L7bE0SuAoqyuys/sMMrmVcN5x9VsxpcG3rqAUSgDyLp0UZlmNfIbQ4fHfCtreVBk8Ew==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.1.tgz", + "integrity": "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==", + "license": "BSD-3-Clause" + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.3", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz", + "integrity": "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", + "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", + "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", + "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", + "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", + "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", + "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", + "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", + "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", + "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", + "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", + "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", + "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", + "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", + "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", + "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", + "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", + "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", + "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", + "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", + "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", + "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", + "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@tailwindcss/node": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.1.tgz", + "integrity": "sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg==", + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.19.0", + "jiti": "^2.6.1", + "lightningcss": "1.31.1", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.2.1" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.1.tgz", + "integrity": "sha512-yv9jeEFWnjKCI6/T3Oq50yQEOqmpmpfzG1hcZsAOaXFQPfzWprWrlHSdGPEF3WQTi8zu8ohC9Mh9J470nT5pUw==", + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.2.1", + "@tailwindcss/oxide-darwin-arm64": "4.2.1", + "@tailwindcss/oxide-darwin-x64": "4.2.1", + "@tailwindcss/oxide-freebsd-x64": "4.2.1", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.1", + "@tailwindcss/oxide-linux-arm64-gnu": "4.2.1", + "@tailwindcss/oxide-linux-arm64-musl": "4.2.1", + "@tailwindcss/oxide-linux-x64-gnu": "4.2.1", + "@tailwindcss/oxide-linux-x64-musl": "4.2.1", + "@tailwindcss/oxide-wasm32-wasi": "4.2.1", + "@tailwindcss/oxide-win32-arm64-msvc": "4.2.1", + "@tailwindcss/oxide-win32-x64-msvc": "4.2.1" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.1.tgz", + "integrity": "sha512-eZ7G1Zm5EC8OOKaesIKuw77jw++QJ2lL9N+dDpdQiAB/c/B2wDh0QPFHbkBVrXnwNugvrbJFk1gK2SsVjwWReg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.1.tgz", + "integrity": "sha512-q/LHkOstoJ7pI1J0q6djesLzRvQSIfEto148ppAd+BVQK0JYjQIFSK3JgYZJa+Yzi0DDa52ZsQx2rqytBnf8Hw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.1.tgz", + "integrity": "sha512-/f/ozlaXGY6QLbpvd/kFTro2l18f7dHKpB+ieXz+Cijl4Mt9AI2rTrpq7V+t04nK+j9XBQHnSMdeQRhbGyt6fw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.1.tgz", + "integrity": "sha512-5e/AkgYJT/cpbkys/OU2Ei2jdETCLlifwm7ogMC7/hksI2fC3iiq6OcXwjibcIjPung0kRtR3TxEITkqgn0TcA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.1.tgz", + "integrity": "sha512-Uny1EcVTTmerCKt/1ZuKTkb0x8ZaiuYucg2/kImO5A5Y/kBz41/+j0gxUZl+hTF3xkWpDmHX+TaWhOtba2Fyuw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.1.tgz", + "integrity": "sha512-CTrwomI+c7n6aSSQlsPL0roRiNMDQ/YzMD9EjcR+H4f0I1SQ8QqIuPnsVp7QgMkC1Qi8rtkekLkOFjo7OlEFRQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.1.tgz", + "integrity": "sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.1.tgz", + "integrity": "sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.1.tgz", + "integrity": "sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.1.tgz", + "integrity": "sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.8.1", + "@emnapi/runtime": "^1.8.1", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.1.1", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": { + "version": "1.8.1", + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.1.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.8.1", + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": { + "version": "1.1.0", + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.1", + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1", + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": { + "version": "2.8.1", + "inBundle": true, + "license": "0BSD", + "optional": true + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.1.tgz", + "integrity": "sha512-YlUEHRHBGnCMh4Nj4GnqQyBtsshUPdiNroZj8VPkvTZSoHsilRCwXcVKnG9kyi0ZFAS/3u+qKHBdDc81SADTRA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.1.tgz", + "integrity": "sha512-rbO34G5sMWWyrN/idLeVxAZgAKWrn5LiR3/I90Q9MkA67s6T1oB0xtTe+0heoBvHSpbU9Mk7i6uwJnpo4u21XQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.2.1.tgz", + "integrity": "sha512-TBf2sJjYeb28jD2U/OhwdW0bbOsxkWPwQ7SrqGf9sVcoYwZj7rkXljroBO9wKBut9XnmQLXanuDUeqQK0lGg/w==", + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.2.1", + "@tailwindcss/oxide": "4.2.1", + "tailwindcss": "4.2.1" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.6.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz", + "integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.19.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.2.0.tgz", + "integrity": "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.29.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-rc.3", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "peer": true, + "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/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "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==", + "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==", + "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/babel-plugin-react-compiler": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/babel-plugin-react-compiler/-/babel-plugin-react-compiler-1.0.0.tgz", + "integrity": "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.26.0" + } + }, + "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/baseline-browser-mapping": { + "version": "2.10.7", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.7.tgz", + "integrity": "sha512-1ghYO3HnxGec0TCGBXiDLVns4eCSx4zJpxnHrlqFQajmhfKMQBzUGDdkMK7fUW7PTHTeLf+j87aTuKuuwWzMGw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "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/caniuse-lite": { + "version": "1.0.30001778", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001778.tgz", + "integrity": "sha512-PN7uxFL+ExFJO61aVmP1aIEG4i9whQd4eoSCebav62UwDyp5OHh06zN4jqKSMePVgxHifCw1QJxdRkA1Pisekg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/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/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "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==", + "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==", + "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/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/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/electron-to-chromium": { + "version": "1.5.313", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.313.tgz", + "integrity": "sha512-QBMrTWEf00GXZmJyx2lbYD45jpI3TUFnNIzJ5BBc8piGUDwMPa1GV6HJWTZVvY/eiN3fSopl7NRbgGp9sZ9LTA==", + "dev": true, + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/enhanced-resolve": { + "version": "5.20.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.0.tgz", + "integrity": "sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/esbuild": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.4.tgz", + "integrity": "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.4", + "@esbuild/android-arm": "0.27.4", + "@esbuild/android-arm64": "0.27.4", + "@esbuild/android-x64": "0.27.4", + "@esbuild/darwin-arm64": "0.27.4", + "@esbuild/darwin-x64": "0.27.4", + "@esbuild/freebsd-arm64": "0.27.4", + "@esbuild/freebsd-x64": "0.27.4", + "@esbuild/linux-arm": "0.27.4", + "@esbuild/linux-arm64": "0.27.4", + "@esbuild/linux-ia32": "0.27.4", + "@esbuild/linux-loong64": "0.27.4", + "@esbuild/linux-mips64el": "0.27.4", + "@esbuild/linux-ppc64": "0.27.4", + "@esbuild/linux-riscv64": "0.27.4", + "@esbuild/linux-s390x": "0.27.4", + "@esbuild/linux-x64": "0.27.4", + "@esbuild/netbsd-arm64": "0.27.4", + "@esbuild/netbsd-x64": "0.27.4", + "@esbuild/openbsd-arm64": "0.27.4", + "@esbuild/openbsd-x64": "0.27.4", + "@esbuild/openharmony-arm64": "0.27.4", + "@esbuild/sunos-x64": "0.27.4", + "@esbuild/win32-arm64": "0.27.4", + "@esbuild/win32-ia32": "0.27.4", + "@esbuild/win32-x64": "0.27.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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": "9.39.4", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", + "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.5", + "@eslint/js": "9.39.4", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz", + "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.26", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.26.tgz", + "integrity": "sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=8.40" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.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/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-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/faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "license": "Apache-2.0", + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "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/firebase": { + "version": "12.12.1", + "resolved": "https://registry.npmjs.org/firebase/-/firebase-12.12.1.tgz", + "integrity": "sha512-ee7xA+bTJLfjB9BP/8FQr3EkxmpAAGc1lNc5QkWgTDpUw24HYXFPm7FEWRdLtGnygxIdYpFmepSc5VjkI6NHhw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/ai": "2.11.1", + "@firebase/analytics": "0.10.21", + "@firebase/analytics-compat": "0.2.27", + "@firebase/app": "0.14.11", + "@firebase/app-check": "0.11.2", + "@firebase/app-check-compat": "0.4.2", + "@firebase/app-compat": "0.5.11", + "@firebase/app-types": "0.9.4", + "@firebase/auth": "1.13.0", + "@firebase/auth-compat": "0.6.5", + "@firebase/data-connect": "0.6.0", + "@firebase/database": "1.1.2", + "@firebase/database-compat": "2.1.3", + "@firebase/firestore": "4.14.0", + "@firebase/firestore-compat": "0.4.8", + "@firebase/functions": "0.13.3", + "@firebase/functions-compat": "0.4.3", + "@firebase/installations": "0.6.21", + "@firebase/installations-compat": "0.2.21", + "@firebase/messaging": "0.12.25", + "@firebase/messaging-compat": "0.2.25", + "@firebase/performance": "0.7.11", + "@firebase/performance-compat": "0.2.24", + "@firebase/remote-config": "0.8.2", + "@firebase/remote-config-compat": "0.2.23", + "@firebase/storage": "0.14.2", + "@firebase/storage-compat": "0.4.2", + "@firebase/util": "1.15.0" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "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/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "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/globals": { + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz", + "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "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/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/http-parser-js": { + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.10.tgz", + "integrity": "sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==", + "license": "MIT" + }, + "node_modules/idb": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz", + "integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==", + "license": "ISC" + }, + "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/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-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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/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/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "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/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "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/lightningcss": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.31.1.tgz", + "integrity": "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ==", + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.31.1", + "lightningcss-darwin-arm64": "1.31.1", + "lightningcss-darwin-x64": "1.31.1", + "lightningcss-freebsd-x64": "1.31.1", + "lightningcss-linux-arm-gnueabihf": "1.31.1", + "lightningcss-linux-arm64-gnu": "1.31.1", + "lightningcss-linux-arm64-musl": "1.31.1", + "lightningcss-linux-x64-gnu": "1.31.1", + "lightningcss-linux-x64-musl": "1.31.1", + "lightningcss-win32-arm64-msvc": "1.31.1", + "lightningcss-win32-x64-msvc": "1.31.1" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.31.1.tgz", + "integrity": "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.31.1.tgz", + "integrity": "sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.31.1.tgz", + "integrity": "sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.31.1.tgz", + "integrity": "sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.31.1.tgz", + "integrity": "sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.31.1.tgz", + "integrity": "sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.31.1.tgz", + "integrity": "sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.31.1.tgz", + "integrity": "sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.31.1.tgz", + "integrity": "sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.31.1.tgz", + "integrity": "sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.31.1.tgz", + "integrity": "sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "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.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "license": "MIT" + }, + "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/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "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/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/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/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/node-releases": { + "version": "2.0.36", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", + "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==", + "dev": true, + "license": "MIT" + }, + "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/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-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/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.12", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.12.tgz", + "integrity": "sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "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/protobufjs": { + "version": "7.5.6", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.6.tgz", + "integrity": "sha512-M71sTMB146U3u0di3yup8iM+zv8yPRNQVr1KK4tyBitl3qFvEGucq/rGDRShD2rsJhtN02RJaJ7j5X5hmy8SJg==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.5", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.1", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.1", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "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/react": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", + "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.4" + } + }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "7.13.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.13.1.tgz", + "integrity": "sha512-td+xP4X2/6BJvZoX6xw++A2DdEi++YypA69bJUV5oVvqf6/9/9nNlD70YO1e9d3MyamJEBQFEzk6mbfDYbqrSA==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.13.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.13.1.tgz", + "integrity": "sha512-UJnV3Rxc5TgUPJt2KJpo1Jpy0OKQr0AjgbZzBFjaPJcFOb2Y8jA5H3LT8HUJAiRLlWrEXWHbF1Z4SCZaQjWDHw==", + "license": "MIT", + "dependencies": { + "react-router": "7.13.1" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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/rollup": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", + "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.59.0", + "@rollup/rollup-android-arm64": "4.59.0", + "@rollup/rollup-darwin-arm64": "4.59.0", + "@rollup/rollup-darwin-x64": "4.59.0", + "@rollup/rollup-freebsd-arm64": "4.59.0", + "@rollup/rollup-freebsd-x64": "4.59.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", + "@rollup/rollup-linux-arm-musleabihf": "4.59.0", + "@rollup/rollup-linux-arm64-gnu": "4.59.0", + "@rollup/rollup-linux-arm64-musl": "4.59.0", + "@rollup/rollup-linux-loong64-gnu": "4.59.0", + "@rollup/rollup-linux-loong64-musl": "4.59.0", + "@rollup/rollup-linux-ppc64-gnu": "4.59.0", + "@rollup/rollup-linux-ppc64-musl": "4.59.0", + "@rollup/rollup-linux-riscv64-gnu": "4.59.0", + "@rollup/rollup-linux-riscv64-musl": "4.59.0", + "@rollup/rollup-linux-s390x-gnu": "4.59.0", + "@rollup/rollup-linux-x64-gnu": "4.59.0", + "@rollup/rollup-linux-x64-musl": "4.59.0", + "@rollup/rollup-openbsd-x64": "4.59.0", + "@rollup/rollup-openharmony-arm64": "4.59.0", + "@rollup/rollup-win32-arm64-msvc": "4.59.0", + "@rollup/rollup-win32-ia32-msvc": "4.59.0", + "@rollup/rollup-win32-x64-gnu": "4.59.0", + "@rollup/rollup-win32-x64-msvc": "4.59.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "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/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "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/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^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/tailwindcss": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.1.tgz", + "integrity": "sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw==", + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "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==", + "license": "0BSD" + }, + "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/undici-types": { + "version": "7.19.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz", + "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==", + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/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/vite": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.2.tgz", + "integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==", + "license": "MIT", + "peer": true, + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/web-vitals": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-4.2.4.tgz", + "integrity": "sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==", + "license": "Apache-2.0" + }, + "node_modules/websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "license": "Apache-2.0", + "dependencies": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8.0" + } + }, + "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/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "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" + } + }, + "node_modules/zod": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "dev": true, + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + } + } +} diff --git a/package.json b/package.json index c6733fb..7a371d3 100644 --- a/package.json +++ b/package.json @@ -1,31 +1,32 @@ -{ - "name": "iiit_insider", - "private": true, - "version": "0.0.0", - "type": "module", - "scripts": { - "dev": "vite", - "build": "vite build", - "lint": "eslint .", - "preview": "vite preview" - }, - "dependencies": { - "@tailwindcss/vite": "^4.2.1", - "react": "^19.2.0", - "react-dom": "^19.2.0", - "react-router-dom": "^7.13.1", - "tailwindcss": "^4.2.1" - }, - "devDependencies": { - "@eslint/js": "^9.39.1", - "@types/react": "^19.2.5", - "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^5.1.1", - "babel-plugin-react-compiler": "^1.0.0", - "eslint": "^9.39.1", - "eslint-plugin-react-hooks": "^7.0.1", - "eslint-plugin-react-refresh": "^0.4.24", - "globals": "^16.5.0", - "vite": "^7.2.4" - } -} +{ + "name": "iiit_insider", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "@tailwindcss/vite": "^4.2.1", + "firebase": "^12.12.1", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "react-router-dom": "^7.13.1", + "tailwindcss": "^4.2.1" + }, + "devDependencies": { + "@eslint/js": "^9.39.1", + "@types/react": "^19.2.5", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.1", + "babel-plugin-react-compiler": "^1.0.0", + "eslint": "^9.39.1", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "globals": "^16.5.0", + "vite": "^7.2.4" + } +} diff --git a/public/firebase-messaging-sw.js b/public/firebase-messaging-sw.js new file mode 100644 index 0000000..0d15f15 --- /dev/null +++ b/public/firebase-messaging-sw.js @@ -0,0 +1,37 @@ +importScripts('https://www.gstatic.com/firebasejs/10.13.2/firebase-app-compat.js'); +importScripts('https://www.gstatic.com/firebasejs/10.13.2/firebase-messaging-compat.js'); + +const firebaseConfig = Object.fromEntries(new URL(self.location.href).searchParams.entries()); + +firebase.initializeApp(firebaseConfig); + +const messaging = firebase.messaging(); + +messaging.onBackgroundMessage((payload) => { + const notification = payload.notification || {}; + const notificationTitle = notification.title || 'IIIT Insider'; + const notificationOptions = { + body: notification.body || '', + icon: notification.icon || '/logo.png', + badge: '/logo.png', + data: payload.data || {}, + }; + + self.registration.showNotification(notificationTitle, notificationOptions); +}); + +self.addEventListener('notificationclick', (event) => { + event.notification.close(); + + event.waitUntil( + clients.matchAll({ type: 'window', includeUncontrolled: true }).then((clientList) => { + for (const client of clientList) { + if ('focus' in client) { + return client.focus(); + } + } + + return clients.openWindow('/'); + }) + ); +}); diff --git a/skills-lock.json b/skills-lock.json new file mode 100644 index 0000000..944107e --- /dev/null +++ b/skills-lock.json @@ -0,0 +1,83 @@ +{ + "version": 1, + "skills": { + "developing-genkit-dart": { + "source": "firebase/agent-skills", + "sourceType": "github", + "skillPath": "skills/developing-genkit-dart/SKILL.md", + "computedHash": "aa92490e4db5038730c629477ad968796f329040433625cf9b7bb13e26a859e3" + }, + "developing-genkit-go": { + "source": "firebase/agent-skills", + "sourceType": "github", + "skillPath": "skills/developing-genkit-go/SKILL.md", + "computedHash": "163d0bbfcb2a067d4cd56d6c27725c23cd628084df94409b31c803e1d24dc3b6" + }, + "developing-genkit-js": { + "source": "firebase/agent-skills", + "sourceType": "github", + "skillPath": "skills/developing-genkit-js/SKILL.md", + "computedHash": "2fa9adb27f7cfc4635decebea65222cd56e36b5de34781f7862be888504c04f4" + }, + "developing-genkit-python": { + "source": "firebase/agent-skills", + "sourceType": "github", + "skillPath": "skills/developing-genkit-python/SKILL.md", + "computedHash": "24576698f88f6b78bbbd9d3b455feab486637685432282cdb52a41ee1bc45dc1" + }, + "firebase-ai-logic-basics": { + "source": "firebase/agent-skills", + "sourceType": "github", + "skillPath": "skills/firebase-ai-logic-basics/SKILL.md", + "computedHash": "9ba85339bb6d34b95bdcf1f336e2072f075b347e20cbf8573773b266faa792f4" + }, + "firebase-app-hosting-basics": { + "source": "firebase/agent-skills", + "sourceType": "github", + "skillPath": "skills/firebase-app-hosting-basics/SKILL.md", + "computedHash": "7f0e0330510b4e6b06bcede472cebb183a491b8a0098f92d7563454c40d78050" + }, + "firebase-auth-basics": { + "source": "firebase/agent-skills", + "sourceType": "github", + "skillPath": "skills/firebase-auth-basics/SKILL.md", + "computedHash": "ff79d278c0968f297d60f37532c32fd3b8bd9aad9c64b9f354585c701e80112a" + }, + "firebase-basics": { + "source": "firebase/agent-skills", + "sourceType": "github", + "skillPath": "skills/firebase-basics/SKILL.md", + "computedHash": "c22edefdc07886432f1361b0412f82ccd53f82d107005acd6dae326f1e106ea7" + }, + "firebase-data-connect": { + "source": "firebase/agent-skills", + "sourceType": "github", + "skillPath": "skills/firebase-data-connect-basics/SKILL.md", + "computedHash": "8970e64edc74a0a2644f129f6981cd92fd07004ad1251366d2fccccc79b308f3" + }, + "firebase-firestore-enterprise-native-mode": { + "source": "firebase/agent-skills", + "sourceType": "github", + "skillPath": "skills/firebase-firestore-enterprise-native-mode/SKILL.md", + "computedHash": "528bd602aa93a4409842a4e6f9888b8438521953253b0a629aeaf10603823bf9" + }, + "firebase-firestore-standard": { + "source": "firebase/agent-skills", + "sourceType": "github", + "skillPath": "skills/firebase-firestore-standard/SKILL.md", + "computedHash": "ab2332607f40ae408e9c56e177b02fad55e071bfac4c2035a16ce0787768953e" + }, + "firebase-hosting-basics": { + "source": "firebase/agent-skills", + "sourceType": "github", + "skillPath": "skills/firebase-hosting-basics/SKILL.md", + "computedHash": "fb86fd4035e8e6379931faeb443557ac6f2e43fde04b397433f287e69b6532a9" + }, + "firebase-security-rules-auditor": { + "source": "firebase/agent-skills", + "sourceType": "github", + "skillPath": "skills/firebase-security-rules-auditor/SKILL.md", + "computedHash": "5a90e991bb9acfd3e43bfb570498dee60b9cef94cbb80cfb99257c7e4f61c1a0" + } + } +} diff --git a/src/App.css b/src/App.css index b9d355d..fe59efc 100644 --- a/src/App.css +++ b/src/App.css @@ -1,42 +1,42 @@ -#root { - max-width: 1280px; - margin: 0 auto; - padding: 2rem; - text-align: center; -} - -.logo { - height: 6em; - padding: 1.5em; - will-change: filter; - transition: filter 300ms; -} -.logo:hover { - filter: drop-shadow(0 0 2em #646cffaa); -} -.logo.react:hover { - filter: drop-shadow(0 0 2em #61dafbaa); -} - -@keyframes logo-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} - -@media (prefers-reduced-motion: no-preference) { - a:nth-of-type(2) .logo { - animation: logo-spin infinite 20s linear; - } -} - -.card { - padding: 2em; -} - -.read-the-docs { - color: #888; -} +#root { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; + transition: filter 300ms; +} +.logo:hover { + filter: drop-shadow(0 0 2em #646cffaa); +} +.logo.react:hover { + filter: drop-shadow(0 0 2em #61dafbaa); +} + +@keyframes logo-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +@media (prefers-reduced-motion: no-preference) { + a:nth-of-type(2) .logo { + animation: logo-spin infinite 20s linear; + } +} + +.card { + padding: 2em; +} + +.read-the-docs { + color: #888; +} diff --git a/src/App.jsx b/src/App.jsx index 1558d09..2881076 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,34 +1,42 @@ -import Navbar from "./Components/Navbar"; -import Home from "../src/Pages/Home"; -import College from "../src/Pages/College"; +import Navbar from "./Components/Navbar"; +import Home from "../src/Pages/Home"; +import College from "../src/Pages/College"; import Compare from "../src/Pages/Compare"; import User_table from "../src/Pages/User_table"; import Register from "../src/Pages/Register"; +import About from "../src/Pages/About"; +import Contact from "../src/Pages/Contact"; +import Support from "../src/Pages/Support"; +import Privacy from "../src/Pages/Privacy"; import { Route, Routes } from "react-router-dom"; -import QuickLink from "./Components/QuickLink"; - -const App = () => { - return ( -
-
- - -
- - } /> - } /> +import QuickLink from "./Components/QuickLink"; + +const App = () => { + return ( +
+
+ + +
+ + } /> + } /> } /> } /> } /> + } /> + } /> + } /> + } /> -
-
- -
- -
-
- ); -}; - -export default App; +
+
+ +
+ +
+
+ ); +}; + +export default App; diff --git a/src/Components/Button.jsx b/src/Components/Button.jsx index d0b446b..6284dec 100644 --- a/src/Components/Button.jsx +++ b/src/Components/Button.jsx @@ -1,24 +1,24 @@ -import React from 'react' -import { NavLink } from 'react-router-dom' - -const Button = ({name,path}) => { - return ( -
- `{ - px-4 py-1 font-semibold transition-all rounded-xl active:tracking-tight cursor-pointer - ${ - isActive - ? "bg-yellow-400/30 text-yellow-200" - : "text-yellow-100 hover:bg-yellow-400/20" - } - }`} - > - {name} - -
- ) -} - -export default Button +import React from 'react' +import { NavLink } from 'react-router-dom' + +const Button = ({name,path}) => { + return ( +
+ `{ + px-4 py-1 font-semibold transition-all rounded-xl active:tracking-tight cursor-pointer + ${ + isActive + ? "bg-yellow-400/30 text-yellow-200" + : "text-yellow-100 hover:bg-yellow-400/20" + } + }`} + > + {name} + +
+ ) +} + +export default Button diff --git a/src/Components/Card.jsx b/src/Components/Card.jsx index a82fecf..1c2245a 100644 --- a/src/Components/Card.jsx +++ b/src/Components/Card.jsx @@ -1,27 +1,27 @@ -import React from "react"; - -const Card = ({title, body}) => { - return ( -
-
- {/* top */} - - {/* bottom */} - - {/* left */} - - {/* right */} - -

- {title} -

-

- {body} -

-
-
- ); -}; - -export default Card; +import React from "react"; + +const Card = ({title, body}) => { + return ( +
+
+ {/* top */} + + {/* bottom */} + + {/* left */} + + {/* right */} + +

+ {title} +

+

+ {body} +

+
+
+ ); +}; + +export default Card; // \ No newline at end of file diff --git a/src/Components/Carousel.jsx b/src/Components/Carousel.jsx index f549dbf..e415254 100644 --- a/src/Components/Carousel.jsx +++ b/src/Components/Carousel.jsx @@ -1,35 +1,35 @@ -import React, { useEffect, useState } from "react"; -import Data from "../Components/Data"; - -const Carousel = () => { - const [index, setIndex] = useState(0); - const data = Data; - useEffect(() => { - const auto = setInterval(() => { - setIndex((prev) => (prev + data.length - 1) % data.length); - }, 3000); - return () => clearInterval(auto); - }, [data.length]); - - return ( -
-

- Indian Institute of Information Technology -

-
-
- {data[index].name} -
- - {data[index].name} - -
-
- ); -}; - -export default Carousel; +import React, { useEffect, useState } from "react"; +import Data from "../Components/Data"; + +const Carousel = () => { + const [index, setIndex] = useState(0); + const data = Data; + useEffect(() => { + const auto = setInterval(() => { + setIndex((prev) => (prev + data.length - 1) % data.length); + }, 3000); + return () => clearInterval(auto); + }, [data.length]); + + return ( +
+

+ Indian Institute of Information Technology +

+
+
+ {data[index].name} +
+ + {data[index].name} + +
+
+ ); +}; + +export default Carousel; diff --git a/src/Components/Charm.jsx b/src/Components/Charm.jsx index 10cacb5..e516d65 100644 --- a/src/Components/Charm.jsx +++ b/src/Components/Charm.jsx @@ -1,30 +1,30 @@ -import React from "react"; -import Card from "./Card"; - -const Charm = () => { - return ( -
-

Charm of IIIT'S

-
- - - - -
-
- ); -}; - -export default Charm; +import React from "react"; +import Card from "./Card"; + +const Charm = () => { + return ( +
+

Charm of IIIT'S

+
+ + + + +
+
+ ); +}; + +export default Charm; diff --git a/src/Components/CollegeMatch.jsx b/src/Components/CollegeMatch.jsx index 43d6371..23db0dc 100644 --- a/src/Components/CollegeMatch.jsx +++ b/src/Components/CollegeMatch.jsx @@ -1,52 +1,52 @@ -import React from "react"; -import Button from "./Button"; - -const CollegeMatch = () => { - return ( -
-

- ARE YOU READY TO FIND YOUR COLLEGE MATCH! - -

-
-
-
- College - -
-
-
- -
- Exams -
-
-
- -
- Cutoff -
-
-
-
-
-
- ); -}; - -export default CollegeMatch; +import React from "react"; +import Button from "./Button"; + +const CollegeMatch = () => { + return ( +
+

+ ARE YOU READY TO FIND YOUR COLLEGE MATCH! + +

+
+
+
+ College + +
+
+
+ +
+ Exams +
+
+
+ +
+ Cutoff +
+
+
+
+
+
+ ); +}; + +export default CollegeMatch; diff --git a/src/Components/ComparingData.jsx b/src/Components/ComparingData.jsx index 5a17aa8..81b6946 100644 --- a/src/Components/ComparingData.jsx +++ b/src/Components/ComparingData.jsx @@ -1,434 +1,434 @@ -const ComparingData = { - "iiit allahabad": { - name: "IIIT Allahabad (IIITA)", - year: "1999", - area: "100 acres", - type: "MoE", - avg: "25.78 LPA", - med: "30 LPA", - mea: "1.21 Cr", - placement: "100%", - curr: "B.Tech, M.Tech, MBA, PhD", - las: "IT (Information Technology)", - pros: "Top-tier coding culture, excellent ROI", - cons: "High academic workload, intense competition", - }, - - "iiit gwalior": { - name: "ABV-IIIT Gwalior", - year: "1997", - area: "160 acres", - type: "MoE", - avg: "24.31 LPA", - med: "22 LPA", - mea: "65 LPA", - placement: "92%", - curr: "IPG (B.Tech+M.Tech/MBA), PhD", - las: "Integrated B.Tech + M.Tech (IT)", - pros: "Green campus, unique integrated courses", - cons: "Strict attendance, older infrastructure", - }, - - "iiit jabalpur": { - name: "PDPM IIIT Jabalpur", - year: "2005", - area: "250 acres", - type: "MoE", - avg: "21.6 LPA", - med: "16 LPA", - mea: "82 LPA", - placement: "99%", - curr: "B.Tech, M.Des, M.Tech, PhD", - las: "CSE and Smart Manufacturing", - pros: "Strong design curriculum, huge campus", - cons: "Remote location, connectivity issues", - }, - - "iiit kancheepuram": { - name: "IIITDM Kancheepuram", - year: "2007", - area: "51 acres", - type: "MoE", - avg: "13 LPA", - med: "11 LPA", - mea: "32 LPA", - placement: "90%", - curr: "B.Tech, M.Des, Dual Degree", - las: "Smart Manufacturing / CSE", - pros: "Proximity to Chennai, modern labs", - cons: "Strict rules, smaller campus size", - }, - - "iiit sri city": { - name: "IIIT Sri City", - year: "2013", - area: "80 acres", - type: "PPP", - avg: "20.3 LPA", - med: "14 LPA", - mea: "1.20 Cr", - placement: "98%", - curr: "B.Tech, M.Tech, PhD", - las: "CSE (Computer Science)", - pros: "Mentored by IIIT-H, great coding", - cons: "Remote location, construction ongoing", - }, - - "iiit vadodara": { - name: "IIIT Vadodara", - year: "2013", - area: "50 acres (Proposed)", - type: "PPP", - avg: "15.43 LPA", - med: "13.5 LPA", - mea: "43 LPA", - placement: "97%", - curr: "B.Tech, M.Tech, PhD", - las: "CSE", - pros: "Excellent curriculum, consistent placements", - cons: "No permanent campus yet, hostel distance", - }, - - "iiit kota": { - name: "IIIT Kota", - year: "2013", - area: "100 acres", - type: "PPP", - avg: "14.6 LPA", - med: "11 LPA", - mea: "53.6 LPA", - placement: "90%", - curr: "B.Tech, M.Tech, PhD", - las: "CSE", - pros: "MNIT Jaipur mentorship alumni base", - cons: "Shifting to new campus, settling in", - }, - - "iiit tiruchirappalli": { - name: "IIIT Tiruchirappalli", - year: "2013", - area: "60 acres", - type: "PPP", - avg: "9.9 LPA", - med: "8.5 LPA", - mea: "20 LPA", - placement: "85%", - curr: "B.Tech, PhD", - las: "CSE", - pros: "Mentored by NIT Trichy previously", - cons: "Infrastructure still under development", - }, - - "iiit guwahati": { - name: "IIIT Guwahati", - year: "2013", - area: "68 acres", - type: "PPP", - avg: "17 LPA", - med: "14.5 LPA", - mea: "1.20 Cr", - placement: "93%", - curr: "B.Tech, M.Tech, PhD", - las: "CSE", - pros: "Own permanent campus, good research", - cons: "Location far from main city", - }, - - "iiit pune": { - name: "IIIT Pune", - year: "2016", - area: "100 acres (Allotted)", - type: "PPP", - avg: "16.83 LPA", - med: "14 LPA", - mea: "53 LPA", - placement: "95%", - curr: "B.Tech, M.Tech, PhD", - las: "CSE", - pros: "Located in IT hub, high growth", - cons: "Operating from temporary campus", - }, - - "iiit kottayam": { - name: "IIIT Kottayam", - year: "2015", - area: "53 acres", - type: "PPP", - avg: "14.32 LPA", - med: "10 LPA", - mea: "58 LPA", - placement: "90%", - curr: "B.Tech, M.Tech, PhD", - las: "CSE (Cyber Security focus)", - pros: "Beautiful campus, fast growing stats", - cons: "Hilly terrain, rainy weather issues", - }, - - "iiit manipur": { - name: "IIIT Manipur", - year: "2015", - area: "150 acres", - type: "PPP", - avg: "9.9 LPA", - med: "7.5 LPA", - mea: "45 LPA", - placement: "80%", - curr: "B.Tech, PhD", - las: "CSE", - pros: "Good faculty-student ratio", - cons: "Political instability, internet issues", - }, - - "iiit dharwad": { - name: "IIIT Dharwad", - year: "2015", - area: "60 acres", - type: "PPP", - avg: "11.52 LPA", - med: "8 LPA", - mea: "35 LPA", - placement: "87%", - curr: "B.Tech, PhD", - las: "Data Science & AI", - pros: "Infosys Foundation support, new campus", - cons: "Away from major metro cities", - }, - - "iiit kurnool": { - name: "IIITDM Kurnool", - year: "2015", - area: "151 acres", - type: "MoE", - avg: "10.79 LPA", - med: "8.5 LPA", - mea: "28 LPA", - placement: "85%", - curr: "B.Tech, M.Tech, PhD", - las: "CSE / Artificial Intelligence", - pros: "Central funding, near Hyderabad highway", - cons: "Hot weather, developing infrastructure", - }, - - "iiit kalyani": { - name: "IIIT Kalyani", - year: "2014", - area: "50 acres", - type: "PPP", - avg: "13.5 LPA", - med: "12 LPA", - mea: "26 LPA", - placement: "89%", - curr: "B.Tech, PhD", - las: "CSE", - pros: "Near Kolkata IT hub, mentors", - cons: "Slow campus construction pace", - }, - - "iiit lucknow": { - name: "IIIT Lucknow", - year: "2015", - area: "50 acres", - type: "PPP", - avg: "30.52 LPA", - med: "25 LPA", - mea: "59 LPA", - placement: "100%", - curr: "B.Tech, M.Tech, MBA", - las: "CSE / AI", - pros: "Exceptional ROI, top coding culture", - cons: "Small campus, hostel shortage issues", - }, - - "iiit una": { - name: "IIIT Una", - year: "2014", - area: "80 acres", - type: "PPP", - avg: "15 LPA", - med: "10 LPA", - mea: "60 LPA", - placement: "90%", - curr: "B.Tech, PhD", - las: "CSE", - pros: "New permanent campus, pleasant weather", - cons: "Limited exposure compared to metros", - }, - - "iiit sonepat": { - name: "IIIT Sonepat", - year: "2014", - area: "50 acres (Proposed)", - type: "PPP", - avg: "16.51 LPA", - med: "13 LPA", - mea: "52 LPA", - placement: "92%", - curr: "B.Tech, PhD", - las: "CSE", - pros: "Proximity to Delhi-NCR region", - cons: "No permanent campus, transit issues", - }, - - "iiit raichur": { - name: "IIIT Raichur", - year: "2019", - area: "60 acres (Proposed)", - type: "PPP", - avg: "18 LPA", - med: "15 LPA", - mea: "45 LPA", - placement: "85%", - curr: "B.Tech", - las: "CSE (AI & DS)", - pros: "Mentored by IIT Hyderabad (strong)", - cons: "Very new, temporary campus setup", - }, - - "iiit ranchi": { - name: "IIIT Ranchi", - year: "2016", - area: "67 acres (Proposed)", - type: "PPP", - avg: "16.7 LPA", - med: "13.3 LPA", - mea: "83 LPA", - placement: "91%", - curr: "B.Tech, M.Tech, PhD", - las: "CSE", - pros: "Good industrial connectivity in region", - cons: "Operating from temporary rented campus", - }, - - "iiit nagpur": { - name: "IIIT Nagpur", - year: "2016", - area: "100 acres", - type: "PPP", - avg: "16.3 LPA", - med: "12 LPA", - mea: "86 LPA", - placement: "94%", - curr: "B.Tech, PhD", - las: "CSE / ECE (IoT focus)", - pros: "Fast growth, curriculum industry-aligned", - cons: "Campus far from main city", - }, - - "iiit bhagalpur": { - name: "IIIT Bhagalpur", - year: "2017", - area: "50 acres", - type: "PPP", - avg: "15.6 LPA", - med: "11 LPA", - mea: "46 LPA", - placement: "95%", - curr: "B.Tech, M.Tech, PhD", - las: "Mechatronics / CSE", - pros: "Unique Mechatronics branch, scenic campus", - cons: "Tier-3 city location disadvantages", - }, - - "iiit bhopal": { - name: "IIIT Bhopal", - year: "2017", - area: "50 acres (Proposed)", - type: "PPP", - avg: "21.94 LPA", - med: "16 LPA", - mea: "85 LPA", - placement: "96%", - curr: "B.Tech", - las: "CSE", - pros: "Located inside MANIT, great peers", - cons: "No own campus, limited hostel", - }, - "iiit delhi": { - name: "Indraprastha Institute of Information Technology, Delhi", - year: "2008", - area: "Delhi", - type: "State University (Autonomous)", - avg: "23 LPA", - med: "20 LPA", - mea: "47 LPA", - placement: "99%", - curr: "CSE, ECE, CSAM, CSB, CSSS, CSDS, CSML", - las: "CSE, CSAM", - pros: "Top faculty; Strong research; Delhi advantage; Exceptional placements; Global opportunities; Modern campus", - cons: "High fees; Very competitive admissions", - }, - - "iiit surat": { - name: "IIIT Surat", - year: "2017", - area: "50 acres (Proposed)", - type: "PPP", - avg: "16.85 LPA", - med: "12.5 LPA", - mea: "34 LPA", - placement: "92%", - curr: "B.Tech, PhD", - las: "CSE", - pros: "Located in Diamond City, SVNIT mentor", - cons: "No permanent campus, temporary facilities", - }, - - "iiit agartala": { - name: "IIIT Agartala", - year: "2018", - area: "52 acres", - type: "PPP", - avg: "22 LPA", - med: "14 LPA", - mea: "1.15 Cr", - placement: "90%", - curr: "B.Tech", - las: "CSE", - pros: "Shared resources with NIT Agartala", - cons: "Very remote location, travel difficult", - }, - "iiit hyderabad": { - name: "International Institute of Information Technology, Hyderabad", - year: "1998", - area: "66 acres", - type: "Deemed (Not under IIIT Act)", - avg: "32.5 LPA", - med: "28 LPA", - mea: "3.6 Cr", - placement: "100%", - curr: "B.Tech, Dual Degree, M.Tech, MS by Research, PhD", - las: "CSE (B.Tech + MS by Research)", - pros: "Best coding culture in India; insane research output; highest packages among IIITs", - cons: "Extremely high workload; expensive fees", - }, - "iiit bhubaneswar": { - name: "International Institute of Information Technology, Bhubaneswar", - year: "2006", - area: "23 acres", - type: "State Government", - avg: "10.8 LPA", - med: "8.2 LPA", - mea: "35 LPA", - placement: "85%", - curr: "B.Tech, M.Tech", - las: "CSE", - pros: "Decent placements; good coding culture; affordable fees", - cons: "State-level exposure; competition lower than top IIITs", - }, - "iiit bangalore": { - name: "International Institute of Information Technology, Bangalore", - year: "1999", - area: "9 acres", - type: "PPP", - avg: "24.7 LPA", - med: "22 LPA", - mea: "54 LPA", - placement: "100%", - curr: "Integrated M.Tech, M.Tech, MS by Research, PhD", - las: "Integrated M.Tech (CSE / ECE)", - pros: "Outstanding placements; Bengaluru tech ecosystem; Research-focused; Industry collaboration; Strong alumni network", - cons: "Small campus; High fees compared to IIITs", - }, -}; - +const ComparingData = { + "iiit allahabad": { + name: "IIIT Allahabad (IIITA)", + year: "1999", + area: "100 acres", + type: "MoE", + avg: "25.78 LPA", + med: "30 LPA", + mea: "1.21 Cr", + placement: "100%", + curr: "B.Tech, M.Tech, MBA, PhD", + las: "IT (Information Technology)", + pros: "Top-tier coding culture, excellent ROI", + cons: "High academic workload, intense competition", + }, + + "iiit gwalior": { + name: "ABV-IIIT Gwalior", + year: "1997", + area: "160 acres", + type: "MoE", + avg: "24.31 LPA", + med: "22 LPA", + mea: "65 LPA", + placement: "92%", + curr: "IPG (B.Tech+M.Tech/MBA), PhD", + las: "Integrated B.Tech + M.Tech (IT)", + pros: "Green campus, unique integrated courses", + cons: "Strict attendance, older infrastructure", + }, + + "iiit jabalpur": { + name: "PDPM IIIT Jabalpur", + year: "2005", + area: "250 acres", + type: "MoE", + avg: "21.6 LPA", + med: "16 LPA", + mea: "82 LPA", + placement: "99%", + curr: "B.Tech, M.Des, M.Tech, PhD", + las: "CSE and Smart Manufacturing", + pros: "Strong design curriculum, huge campus", + cons: "Remote location, connectivity issues", + }, + + "iiit kancheepuram": { + name: "IIITDM Kancheepuram", + year: "2007", + area: "51 acres", + type: "MoE", + avg: "13 LPA", + med: "11 LPA", + mea: "32 LPA", + placement: "90%", + curr: "B.Tech, M.Des, Dual Degree", + las: "Smart Manufacturing / CSE", + pros: "Proximity to Chennai, modern labs", + cons: "Strict rules, smaller campus size", + }, + + "iiit sri city": { + name: "IIIT Sri City", + year: "2013", + area: "80 acres", + type: "PPP", + avg: "20.3 LPA", + med: "14 LPA", + mea: "1.20 Cr", + placement: "98%", + curr: "B.Tech, M.Tech, PhD", + las: "CSE (Computer Science)", + pros: "Mentored by IIIT-H, great coding", + cons: "Remote location, construction ongoing", + }, + + "iiit vadodara": { + name: "IIIT Vadodara", + year: "2013", + area: "50 acres (Proposed)", + type: "PPP", + avg: "15.43 LPA", + med: "13.5 LPA", + mea: "43 LPA", + placement: "97%", + curr: "B.Tech, M.Tech, PhD", + las: "CSE", + pros: "Excellent curriculum, consistent placements", + cons: "No permanent campus yet, hostel distance", + }, + + "iiit kota": { + name: "IIIT Kota", + year: "2013", + area: "100 acres", + type: "PPP", + avg: "14.6 LPA", + med: "11 LPA", + mea: "53.6 LPA", + placement: "90%", + curr: "B.Tech, M.Tech, PhD", + las: "CSE", + pros: "MNIT Jaipur mentorship alumni base", + cons: "Shifting to new campus, settling in", + }, + + "iiit tiruchirappalli": { + name: "IIIT Tiruchirappalli", + year: "2013", + area: "60 acres", + type: "PPP", + avg: "9.9 LPA", + med: "8.5 LPA", + mea: "20 LPA", + placement: "85%", + curr: "B.Tech, PhD", + las: "CSE", + pros: "Mentored by NIT Trichy previously", + cons: "Infrastructure still under development", + }, + + "iiit guwahati": { + name: "IIIT Guwahati", + year: "2013", + area: "68 acres", + type: "PPP", + avg: "17 LPA", + med: "14.5 LPA", + mea: "1.20 Cr", + placement: "93%", + curr: "B.Tech, M.Tech, PhD", + las: "CSE", + pros: "Own permanent campus, good research", + cons: "Location far from main city", + }, + + "iiit pune": { + name: "IIIT Pune", + year: "2016", + area: "100 acres (Allotted)", + type: "PPP", + avg: "16.83 LPA", + med: "14 LPA", + mea: "53 LPA", + placement: "95%", + curr: "B.Tech, M.Tech, PhD", + las: "CSE", + pros: "Located in IT hub, high growth", + cons: "Operating from temporary campus", + }, + + "iiit kottayam": { + name: "IIIT Kottayam", + year: "2015", + area: "53 acres", + type: "PPP", + avg: "14.32 LPA", + med: "10 LPA", + mea: "58 LPA", + placement: "90%", + curr: "B.Tech, M.Tech, PhD", + las: "CSE (Cyber Security focus)", + pros: "Beautiful campus, fast growing stats", + cons: "Hilly terrain, rainy weather issues", + }, + + "iiit manipur": { + name: "IIIT Manipur", + year: "2015", + area: "150 acres", + type: "PPP", + avg: "9.9 LPA", + med: "7.5 LPA", + mea: "45 LPA", + placement: "80%", + curr: "B.Tech, PhD", + las: "CSE", + pros: "Good faculty-student ratio", + cons: "Political instability, internet issues", + }, + + "iiit dharwad": { + name: "IIIT Dharwad", + year: "2015", + area: "60 acres", + type: "PPP", + avg: "11.52 LPA", + med: "8 LPA", + mea: "35 LPA", + placement: "87%", + curr: "B.Tech, PhD", + las: "Data Science & AI", + pros: "Infosys Foundation support, new campus", + cons: "Away from major metro cities", + }, + + "iiit kurnool": { + name: "IIITDM Kurnool", + year: "2015", + area: "151 acres", + type: "MoE", + avg: "10.79 LPA", + med: "8.5 LPA", + mea: "28 LPA", + placement: "85%", + curr: "B.Tech, M.Tech, PhD", + las: "CSE / Artificial Intelligence", + pros: "Central funding, near Hyderabad highway", + cons: "Hot weather, developing infrastructure", + }, + + "iiit kalyani": { + name: "IIIT Kalyani", + year: "2014", + area: "50 acres", + type: "PPP", + avg: "13.5 LPA", + med: "12 LPA", + mea: "26 LPA", + placement: "89%", + curr: "B.Tech, PhD", + las: "CSE", + pros: "Near Kolkata IT hub, mentors", + cons: "Slow campus construction pace", + }, + + "iiit lucknow": { + name: "IIIT Lucknow", + year: "2015", + area: "50 acres", + type: "PPP", + avg: "30.52 LPA", + med: "25 LPA", + mea: "59 LPA", + placement: "100%", + curr: "B.Tech, M.Tech, MBA", + las: "CSE / AI", + pros: "Exceptional ROI, top coding culture", + cons: "Small campus, hostel shortage issues", + }, + + "iiit una": { + name: "IIIT Una", + year: "2014", + area: "80 acres", + type: "PPP", + avg: "15 LPA", + med: "10 LPA", + mea: "60 LPA", + placement: "90%", + curr: "B.Tech, PhD", + las: "CSE", + pros: "New permanent campus, pleasant weather", + cons: "Limited exposure compared to metros", + }, + + "iiit sonepat": { + name: "IIIT Sonepat", + year: "2014", + area: "50 acres (Proposed)", + type: "PPP", + avg: "16.51 LPA", + med: "13 LPA", + mea: "52 LPA", + placement: "92%", + curr: "B.Tech, PhD", + las: "CSE", + pros: "Proximity to Delhi-NCR region", + cons: "No permanent campus, transit issues", + }, + + "iiit raichur": { + name: "IIIT Raichur", + year: "2019", + area: "60 acres (Proposed)", + type: "PPP", + avg: "18 LPA", + med: "15 LPA", + mea: "45 LPA", + placement: "85%", + curr: "B.Tech", + las: "CSE (AI & DS)", + pros: "Mentored by IIT Hyderabad (strong)", + cons: "Very new, temporary campus setup", + }, + + "iiit ranchi": { + name: "IIIT Ranchi", + year: "2016", + area: "67 acres (Proposed)", + type: "PPP", + avg: "16.7 LPA", + med: "13.3 LPA", + mea: "83 LPA", + placement: "91%", + curr: "B.Tech, M.Tech, PhD", + las: "CSE", + pros: "Good industrial connectivity in region", + cons: "Operating from temporary rented campus", + }, + + "iiit nagpur": { + name: "IIIT Nagpur", + year: "2016", + area: "100 acres", + type: "PPP", + avg: "16.3 LPA", + med: "12 LPA", + mea: "86 LPA", + placement: "94%", + curr: "B.Tech, PhD", + las: "CSE / ECE (IoT focus)", + pros: "Fast growth, curriculum industry-aligned", + cons: "Campus far from main city", + }, + + "iiit bhagalpur": { + name: "IIIT Bhagalpur", + year: "2017", + area: "50 acres", + type: "PPP", + avg: "15.6 LPA", + med: "11 LPA", + mea: "46 LPA", + placement: "95%", + curr: "B.Tech, M.Tech, PhD", + las: "Mechatronics / CSE", + pros: "Unique Mechatronics branch, scenic campus", + cons: "Tier-3 city location disadvantages", + }, + + "iiit bhopal": { + name: "IIIT Bhopal", + year: "2017", + area: "50 acres (Proposed)", + type: "PPP", + avg: "21.94 LPA", + med: "16 LPA", + mea: "85 LPA", + placement: "96%", + curr: "B.Tech", + las: "CSE", + pros: "Located inside MANIT, great peers", + cons: "No own campus, limited hostel", + }, + "iiit delhi": { + name: "Indraprastha Institute of Information Technology, Delhi", + year: "2008", + area: "Delhi", + type: "State University (Autonomous)", + avg: "23 LPA", + med: "20 LPA", + mea: "47 LPA", + placement: "99%", + curr: "CSE, ECE, CSAM, CSB, CSSS, CSDS, CSML", + las: "CSE, CSAM", + pros: "Top faculty; Strong research; Delhi advantage; Exceptional placements; Global opportunities; Modern campus", + cons: "High fees; Very competitive admissions", + }, + + "iiit surat": { + name: "IIIT Surat", + year: "2017", + area: "50 acres (Proposed)", + type: "PPP", + avg: "16.85 LPA", + med: "12.5 LPA", + mea: "34 LPA", + placement: "92%", + curr: "B.Tech, PhD", + las: "CSE", + pros: "Located in Diamond City, SVNIT mentor", + cons: "No permanent campus, temporary facilities", + }, + + "iiit agartala": { + name: "IIIT Agartala", + year: "2018", + area: "52 acres", + type: "PPP", + avg: "22 LPA", + med: "14 LPA", + mea: "1.15 Cr", + placement: "90%", + curr: "B.Tech", + las: "CSE", + pros: "Shared resources with NIT Agartala", + cons: "Very remote location, travel difficult", + }, + "iiit hyderabad": { + name: "International Institute of Information Technology, Hyderabad", + year: "1998", + area: "66 acres", + type: "Deemed (Not under IIIT Act)", + avg: "32.5 LPA", + med: "28 LPA", + mea: "3.6 Cr", + placement: "100%", + curr: "B.Tech, Dual Degree, M.Tech, MS by Research, PhD", + las: "CSE (B.Tech + MS by Research)", + pros: "Best coding culture in India; insane research output; highest packages among IIITs", + cons: "Extremely high workload; expensive fees", + }, + "iiit bhubaneswar": { + name: "International Institute of Information Technology, Bhubaneswar", + year: "2006", + area: "23 acres", + type: "State Government", + avg: "10.8 LPA", + med: "8.2 LPA", + mea: "35 LPA", + placement: "85%", + curr: "B.Tech, M.Tech", + las: "CSE", + pros: "Decent placements; good coding culture; affordable fees", + cons: "State-level exposure; competition lower than top IIITs", + }, + "iiit bangalore": { + name: "International Institute of Information Technology, Bangalore", + year: "1999", + area: "9 acres", + type: "PPP", + avg: "24.7 LPA", + med: "22 LPA", + mea: "54 LPA", + placement: "100%", + curr: "Integrated M.Tech, M.Tech, MS by Research, PhD", + las: "Integrated M.Tech (CSE / ECE)", + pros: "Outstanding placements; Bengaluru tech ecosystem; Research-focused; Industry collaboration; Strong alumni network", + cons: "Small campus; High fees compared to IIITs", + }, +}; + export default ComparingData; \ No newline at end of file diff --git a/src/Components/Data.jsx b/src/Components/Data.jsx index b4873a3..96d2073 100644 --- a/src/Components/Data.jsx +++ b/src/Components/Data.jsx @@ -1,289 +1,289 @@ -const Data = [ - { - id: 1, - name: "IIIT Bangalore", - description: `International Institute of Information Technology, Bangalore (IIIT Bangalore) is a research-focused institute located in Electronics City, Bengaluru. - It was established in 1999 and is known for its strong academic rigor, industry collaboration, and specialization in computer science and data-driven fields. - The institute offers programs such as Integrated M.Tech, M.Tech, MSc (Digital Society), and PhD, and consistently records excellent placements in software development, AI/ML, cybersecurity, and research roles.`, - image:"../src/assets/banglore.webp", - region:"south" - }, - - { - id: 2, - name: "IIIT Lucknow", - description: `IIIT Lucknow (Indian Institute of Information Technology, Lucknow) is an Institute of National Importance established in 2015 by the Ministry of Education under the Public-Private Partnership (PPP) model. Situated in the capital city of Uttar Pradesh, the institute aims to be a leading center for education, research, and innovation in the field of Information Technology (IT). - IIIT Lucknow is highly sought after for its strong academic curriculum and outstanding placement performance. The institute offers B.Tech programs in Computer Science and Engineering (CSE) and Information Technology (IT), along with a distinctive B.Tech/M.Tech Dual Degree program in CSE and an MBA with specialization in IT. - Admission to the B.Tech courses is based on JEE Main rank, making the competition very intense. The institute provides a highly rigorous academic environment and modern research exposure in emerging domains such as Artificial Intelligence and Machine Learning. IIIT Lucknow has rapidly risen in prominence due to its impressive placement statistics, attracting top tech companies and offering high salary packages, placing it among the leading IIITs in the country.`, - image:"../src/assets/lucknow.jpeg", - region:"north" - }, - - { - id: 3, - name: "IIIT Bhubaneswar – Odisha", - description: ` - The International Institute of Information Technology, Bhubaneswar (IIIT Bhubaneswar) is a premier technical university established in 2006 by the Government of Odisha with a focus on advancing education, research, and innovation in Information Technology and related fields. - It operates under a unique public–private partnership model, combining strong academic foundations with industry-oriented training. - Located on an expansive 23-acre modern campus in Gothapatna, Bhubaneswar, the institute offers B.Tech programs in Computer Science and Engineering, Information Technology, Electrical and Electronics Engineering, and Electronics and Telecommunication Engineering, along with M.Tech and PhD programs. - Admission to the B.Tech courses is based on JEE Main scores through the JoSAA/CSAB counseling process. The institute has built a consistent track record in placements, with leading multinational companies and fast-growing tech startups recruiting students. - IIIT Bhubaneswar emphasizes holistic student development through tech clubs, research labs, incubators, and strong industry collaboration, making it a dynamic hub for technological learning and career growth.`, - image:"../src/assets/bhubneshwar.jpeg", - region:"east" - }, - - { - id: 4, - name: "IIIT Allahabad", - description: `IIIT Allahabad (IIITA), located in Prayagraj, Uttar Pradesh, is an Institute of National Importance established in 1999. It was founded to serve as a center of excellence in the rapidly advancing fields of Information Technology (IT) and Electronics and Communication Engineering (ECE), along with allied domains including Management and Applied Science. - IIITA is consistently ranked among the top technical institutes in India, recognized for its challenging academic environment and strong emphasis on research and innovation. The institute offers B.Tech across four specializations, as well as M.Tech, MBA, and PhD programs, attracting talented students from across the nation. - The institute is particularly known for its strong coding and competitive programming culture, which has produced numerous global programming contest achievers. This culture of hands-on problem solving directly contributes to excellent placement records, with leading global and domestic tech companies visiting the campus every year. - The campus is spread over 100 acres, fully residential, and equipped with modern infrastructure designed to support both academic growth and personal development.`, - image:"../src/assets/allahabad.jpeg", - region:"north" - }, - - { - id: 5, - name: "IIIT Hyderabad", - description: `The International Institute of Information Technology, Hyderabad (IIIT-H), established in 1998, is a highly reputed, autonomous, not-for-profit university operating under a Public-Private Partnership (N-PPP) model. Widely regarded as one of India's premier technical research institutions, it is a peer to the top IITs and the first IIIT established under this framework. IIIT-H is renowned for its strong focus on research and technology application for both industry and society, encouraging undergraduate students to participate in cutting-edge projects. - The institute is famous for its innovative curriculum, offering flexible programs like the Dual Degree (B.Tech + M.S. by Research) and its unique research-led teaching model. Key research areas include Artificial Intelligence, Robotics, Data Science, and Cognitive Science, housed in world-renowned centers of excellence. Admission is extremely competitive, often through JEE Main, but also via a special research pathway. IIIT-H consistently records exceptional placements with high average salary packages, reflecting the industry's premium on its research-focused graduates.`, - image:"../src/assets/hyderabad.jpeg", - region:"south" - }, - - { - id: 6, - name: "IIIT Delhi", - description: `The Indraprastha Institute of Information Technology, Delhi (IIIT-Delhi), established in 2008 by an Act of the Delhi Legislature, is a comprehensive research-led teaching institute with the empowering authority to grant its own degrees. It is a public, state-level technical university that has rapidly distinguished itself as a top-tier institution in India for Computer Science and Electronics education and research. - IIIT-Delhi places a strong emphasis on interdisciplinary research and a project-based curriculum. It offers B.Tech, M.Tech, and PhD programs across departments like Computer Science and Engineering (CSE), Electronics & Communication Engineering (ECE), and specialized areas such as Computational Biology and Human Centered Design. It houses numerous Centers of Excellence, including those for AI, Cybersecurity, and Design, reflecting its deep commitment to societal and technological challenges. - Admission is highly competitive and primarily through the JEE Main score. The institute boasts excellent placements and a supportive environment, consistently attracting meritorious students and globally recognized faculty.`, - image:"../src/assets/delhi.jpeg", - region:"north" - }, - { - id: 7, - name: "IIIT Gwalior", - description: `ABV-IIITM Gwalior, located in Madhya Pradesh, is an Institute of National Importance established in 1997 by the Government of India. It was the first IIIT founded with the mission of integrating Information Technology and Business Management. The institute was renamed in 2002 to honor former Prime Minister Atal Bihari Vajpayee. - The institute is situated on a lush 160-acre campus near the historic Gwalior Fort. It is well known for its five-year Integrated Post Graduate (IPG) programs that offer B.Tech + M.Tech or B.Tech + MBA degrees, along with regular B.Tech, M.Tech, MBA, and PhD programs. The curriculum focuses on producing professionals skilled in both technology and business. - ABV-IIITM maintains consistently strong placement performance, with high average and top CTCs, making it one of the finest technical and management institutes in India. Admissions to the B.Tech and IPG programs are based on JEE Main scores.`, - image:"../src/assets/gwalior.jpeg", - region:"central" - }, - { - id: 8, - name: "IIIT Kota", - description: `The Indian Institute of Information Technology, Kota, is an Institute of National Importance established in 2013 under the Public-Private Partnership (PPP) model. It is uniquely mentored by the Malaviya National Institute of Technology (MNIT), Jaipur, and currently operates from the MNIT campus, benefiting immensely from its resources, faculty expertise, and established infrastructure. This mentorship ensures that IIIT Kota maintains a high academic standard right from its inception. - The institute is focused on producing high-quality engineers in the fields of Information Technology (IT) and Electronics and Communication Engineering (ECE) to meet the demands of the modern industrial economy. It offers B.Tech programs in Computer Science and Engineering (CSE) and Electronics and Communication Engineering (ECE), with intake based on the JEE Main score. - IIIT Kota’s curriculum is contemporary, emphasizing strong foundational knowledge in core engineering coupled with a practical, hands-on approach. Despite operating from a temporary campus, the institute boasts competitive placement statistics, leveraging the strong industry connections fostered by its mentoring institution, MNIT Jaipur. The future permanent campus in Kota is being planned to become a dedicated center for innovation and research.`, - image:"../src/assets/kota.jpeg", - region:"north" - }, - { - id: 9, - name: "📐 IIITDM Kancheepuram – Tamil Nadu", - description: `The Indian Institute of Information Technology Design and Manufacturing (IIITDM), Kancheepuram, is a specialized Institute of National Importance established in 2007 by the Ministry of Education, Government of India. It operates with a distinctive interdisciplinary focus, integrating Information Technology (IT), Design, and Manufacturing to address the intricate demands of product development in the modern industrial landscape. - Located on a state-of-the-art 51-acre campus near Chennai, IIITDM Kancheepuram is strategically placed within a major industrial and technological hub. The institute is known for its unique curriculum that emphasizes a "design-centric" approach across its engineering disciplines. It offers specialized Dual Degree programs (B.Tech + M.Tech) in fields like Computer Science with specialization in Design, as well as B.Tech, M.Tech, M.Des, and PhD degrees. - A core strength of the institute is its focus on product innovation and practical application, featuring advanced manufacturing and design laboratories. Admission to its undergraduate programs is primarily based on the JEE Main score. IIITDM Kancheepuram maintains a strong reputation for placements, particularly in the manufacturing and software industries, where its graduates' unique skill set is highly valued.`, - image:"../src/assets/kanchepuram.jpeg", - region:"north" - }, - { - id: 10, - name: "💡 IIITDM Jabalpur – Madhya Pradesh", - description: `The Pandit Dwarka Prasad Mishra Indian Institute of Information Technology, Design and Manufacturing (PDPM IIITDM), Jabalpur, is a specialized Institute of National Importance established in 2005 by the Government of India. Its unique mandate is to foster excellence in research and education by integrating Information Technology (IT) with Design and Manufacturing processes, aligning with national initiatives like 'Make in India' and 'Digital India.' - Located on a sprawling 260-acre campus near the Dumna Nature Reserve, IIITDM Jabalpur offers diverse academic programs, including B.Tech, B.Des, M.Tech, M.Des, and PhD. The institute is one of the few IIITs offering a specialized B.Des (Bachelor of Design) degree and B.Tech in Smart Manufacturing, showcasing its interdisciplinary focus. The curriculum strongly emphasizes hands-on experience and project-based learning to create industry-ready professionals. - Admission to its undergraduate programs (B.Tech and B.Des) is primarily through JEE Main and UCEED scores, respectively. The institute maintains a commendable placement record, attracting top companies with high domestic and international package offers.`, - image:"../src/assets/jabalpur.jpeg", - region:"central" - }, - { - id: 11, - name: "💎 IIITDM Kurnool – Andhra Pradesh", - description: ` - The Indian Institute of Information Technology Design and Manufacturing (IIITDM), Kurnool, is a new-generation, specialized Institute of National Importance established in 2015 by the Ministry of Education, Government of India. It was founded under the Andhra Pradesh Reorganization Act, 2014, with the primary objective of creating a center for interdisciplinary excellence integrating Information Technology (IT), Design, and Manufacturing. - Located on a picturesque 151-acre campus at Jagannathagattu, the institute promotes a unique, hands-on, and design-centric engineering education. IIITDM Kurnool offers B.Tech programs in specialized fields, including Computer Science and Engineering, Electronics and Communication Engineering (with Design & Manufacturing), Mechanical Engineering (with Design & Manufacturing), and Artificial Intelligence & Data Science. It also offers Dual Degree (B.Tech + M.Tech) and Ph.D. programs. - Admission to the undergraduate programs is based on the JEE Main score. The institute is rapidly developing its infrastructure and has shown promising placement records, with graduates being recruited by top-tier IT and core engineering companies, reflecting the industry's demand for its uniquely skilled, interdisciplinary professionals.`, - image:"../src/assets/kurnool.jpeg", - region:"south" - }, - - { - id:12, - name: "🏔️ IIIT Una – Himachal Pradesh", - description: ` - The Indian Institute of Information Technology, Una, is one of the 20 new IIITs established under the Public-Private Partnership (PPP) model and was granted the status of an Institute of National Importance in 2017. Located temporarily in Una, Himachal Pradesh, the institute is focused on becoming a hub for IT education and research in the Himalayan region. Its establishment aims at bridging the gap between regional industry needs and academic output in the fields of Information Technology (IT) and Electronics and Communication Engineering (ECE). - IIIT Una currently offers B.Tech programs in Computer Science and Engineering (CSE), Electronics and Communication Engineering (ECE), and Information Technology (IT), along with Ph.D. programs. The institute was initially mentored by NIT Hamirpur, helping establish a strong academic framework during its formative years. - The curriculum is highly industry-relevant, emphasizing contemporary computational techniques and hardware design to help students adapt to modern technological demands. Admission to the B.Tech programs is based on the JEE Main score. As IIIT Una transitions to its permanent campus, it continues to attract motivated students and faculty, contributing meaningfully to the technological growth of the region.`, - image:"../src/assets/una.jpeg", - region:"north" - }, - - { - id: 13, - name: "📡 IIIT Sonepat – Haryana", - description: ` - The Indian Institute of Information Technology, Sonepat, is one of the IIITs established on the Public-Private Partnership (PPP) model and was accorded the status of an Institute of National Importance in 2017. Although officially located in Sonepat, Haryana, the institute currently operates from a temporary campus at the National Institute of Technology (NIT) Kurukshetra, with NIT Kurukshetra serving as its mentoring institution. - IIIT Sonepat benefits immensely from its proximity to the National Capital Region (NCR), giving students access to a wide ecosystem of industries and tech companies. The institution is dedicated to creating a center of excellence in core computing and engineering disciplines. It offers B.Tech programs in Computer Science and Engineering (CSE) and Electronics and Communication Engineering (ECE), along with doctoral programs. - The curriculum is designed to be highly relevant to industry expectations, with an emphasis on cutting-edge areas such as Artificial Intelligence, Machine Learning, and Data Science. Admission to the B.Tech programs is based on JEE Main performance. With support from NIT Kurukshetra and growing industry collaboration, IIIT Sonepat is rapidly shaping into a strong technical institute, preparing students for impactful careers in the global IT sector.`, - image:"../src/assets/soni.jpeg", - region:"north" - }, - - { - id: 14, - name: "🌴 IIIT Sri City (IIITSRI) – Andhra Pradesh", - description: `The Indian Institute of Information Technology, Sri City (IIITSRI), established in 2013, is an Institute of National Importance located in the integrated business city of Sri City, Andhra Pradesh. It was founded under the Public-Private Partnership (PPP) model with funding from the Central Government, State Government, and industry partners. Initially mentored by IIIT Hyderabad, the institute quickly adopted a high standard of academic rigor and industry-centric focus. - Strategically situated near Chennai's industrial and tech corridor, IIITSRI specializes in integrating Information Technology with the demands of industrial technology. It offers B.Tech programs in traditional disciplines like CSE and ECE, along with modern specializations like Artificial Intelligence & Data Science (AI & DS). The curriculum is designed to be highly practical, preparing students for the advanced requirements of the IT and manufacturing sectors. Admission is based on the JEE Main score. - IIIT Sri City leverages its industrial ecosystem for strong collaborations, internships, and placements, ensuring its graduates are well-equipped for the modern workplace.`, - image:"../src/assets/sricity.jpeg", - region:"south" - }, - - { - id:15, - name: "🏞️ IIIT Dharwad – Karnataka", - description: ` - The Indian Institute of Information Technology, Dharwad (IIIT Dharwad), established in 2015, is an Institute of National Importance operating under the Public-Private Partnership (PPP) model. It is strategically funded by the Government of India, the Government of Karnataka, and industry partner KEONICS, reflecting a commitment to regional technological development. The institute shifted to its permanent, sprawling 60-acre campus near Dharwad, a growing educational and industrial hub. - IIIT Dharwad is focused on delivering a strong, applied-research-oriented curriculum in core IT and emerging fields. It offers B.Tech programs in Computer Science and Engineering (CSE), Electronics and Communication Engineering (ECE), and a highly specialized B.Tech in Data Science and Artificial Intelligence. This focus on contemporary fields prepares graduates for high-demand roles. Admission to undergraduate programs is based on the JEE Main score. - Despite being a relatively young institution, it maintains a self-sustaining model and is recognized for its dynamic academic environment and competitive placement record, driven by the strong industry demand in the Karnataka region`, - image:"../src/assets/dharwad.jpeg", - region:"south" - }, - - { - id: 16, - name: "✨ IIIT Raichur – Karnataka", - description: ` - The Indian Institute of Information Technology, Raichur (IIIT Raichur), is one of the newest IIITs, established in 2019 under the Public-Private Partnership (PPP) model and recognized as an Institute of National Importance in 2020. The institute was initially mentored by the prestigious IIT Hyderabad, ensuring a robust academic foundation and guidance in its formative years. IIIT Raichur currently operates from a transit campus in Raichur, with a permanent 74-acre campus under development. - The institute is rapidly emerging as a center for excellence in core computing. It offers B.Tech programs in Computer Science and Engineering (CSE) and the highly sought-after Artificial Intelligence & Data Science (AI & DS). IIIT Raichur follows a unique Fractal Academic System which allows for continuous evaluation and flexibility in learning. - Despite its nascent stage, the institute has demonstrated extremely strong potential, with reported high average placement packages for its first few graduating batches. Admission is highly selective and based on the JEE Main score, drawing talented students motivated by the institute's promising trajectory and research focus.`, - image:"../src/assets/raichur.jpeg", - region:"south" - }, - - { - id:16, - name: "🟦 IIIT Kottayam – Kerala", - description: `The Indian Institute of Information Technology, Kottayam (IIIT Kottayam), established in 2015, is an Institute of National Importance set up under the Public-Private Partnership (PPP) model. The institute is located on its permanent, state-of-the-art campus in Valavoor, Kottayam, providing a dedicated and tranquil environment for learning and research. It is focused on cultivating talent in Information Technology and allied disciplines, with a strong emphasis on societal impact. - IIIT Kottayam offers B.Tech programs in Computer Science and Engineering (CSE) and Electronics and Communication Engineering (ECE), alongside specializations in CSE with AI & Data Science and ECE with Cyber Security. A key feature of the institute is its commitment to innovation and entrepreneurship, housing a dedicated Incubation Centre and specialized labs like CyberLabs and FACTS-H Lab. - The curriculum is designed to be contemporary and problem-solving focused, preparing students to address both local and global technological challenges. Admission is through the JEE Main score. The institute is dedicated to producing ethically sound, technically proficient, and socially conscious graduates.`, - image:"../src/assets/kottyam.jpeg", - region:"south" - }, - - { - id: 17, - name: "IIIT Tiruchirappalli", - description: `IIIT Tiruchirappalli (Indian Institute of Information Technology, Tiruchirappalli), established in 2013, is an Institute of National Importance operating under the Public-Private Partnership (PPP) model. Initially mentored by NIT Tiruchirappalli (NITT), the institute benefited greatly from the academic culture and infrastructure of a premier national institution. It is currently transitioning to its upcoming permanent campus in Sethurappatti, near Trichy. - IIIT Tiruchirappalli maintains a strong academic profile and offers B.Tech programs in Computer Science and Engineering (CSE) and Electronics and Communication Engineering (ECE), along with M.Tech and Ph.D. programs in specialized domains. The institute emphasizes research in areas such as Data Analytics, Machine Learning, IoT, and VLSI Systems. - The curriculum blends core engineering fundamentals with hands-on exposure to emerging technologies, ensuring that students develop both theoretical depth and practical skill sets. Admission to the B.Tech programs is based on the JEE Main score. With solid academic mentorship, growing industry partnerships, and a focus on future-ready learning, IIIT-T is committed to producing highly skilled IT professionals.`, - image:"../src/assets/trichy.jpeg", - region:"south" - }, - { - id: 18, - name: "IIIT Vadodara", - description: `IIIT Vadodara (Indian Institute of Information Technology, Vadodara), established in 2013, is an Institute of National Importance operating under the Public-Private Partnership (PPP) model. The institute currently functions from a temporary campus in Gandhinagar and benefits from the region’s strong presence of IT and corporate sectors. It was founded through a joint initiative by the Government of India, the Government of Gujarat, and key industry partners. - IIIT Vadodara focuses heavily on research and applied Information Technology. It offers B.Tech programs in Computer Science and Engineering (CSE) and Information Technology (IT), along with M.Tech and Ph.D. programs. A significant development in its expansion is the establishment of its satellite campus, IIITV-International Campus Diu (IIITV-ICD). - The curriculum emphasizes a blend of theoretical depth and practical application, ensuring industry-ready skill development. Admission to the B.Tech program is based on the JEE Main score. Leveraging its strategic location, strong industrial collaborations, and rapidly advancing academic ecosystem, IIIT Vadodara has earned an excellent reputation for competitive placement performance, consistently attracting top recruiters in the technology sector.`, - image:"../src/assets/vadodara.jpeg", - region:"east" - }, - - { - id: 19, - name: "IIIT Surat", - description: `IIIT Surat (Indian Institute of Information Technology, Surat), established in 2017, is an Institute of National Importance functioning under the Public-Private Partnership (PPP) model. The institute currently operates from a transit campus within SVNIT Surat, which also serves as its mentoring institution, allowing students to benefit from the well-established facilities and experienced faculty of a premier NIT. - IIIT Surat is focused on producing high-quality professionals in computing and electronics. It offers B.Tech programs in Computer Science and Engineering (CSE) and Electronics and Communication Engineering (ECE), along with Ph.D. programs. The curriculum is aligned with modern industry expectations, emphasizing research, innovation, and hands-on learning based on evolving technological trends. - Admission to the B.Tech programs is based exclusively on the JEE Main score. Although a relatively new IIIT, its strategic location in the industrial hub of Surat and its collaboration with SVNIT have helped the institute secure strong industrial exposure and promising placement opportunities, enabling rapid institutional growth.`, - image:"../src/assets/surat.jpeg", - region:"east" - }, - - { - id: 20, - name: "IIIT Nagpur", - description: `IIIT Nagpur (Indian Institute of Information Technology, Nagpur), established in 2016, is an Institute of National Importance under the Public-Private Partnership (PPP) model. The institute is located on a 100-acre permanent campus in Nagpur — a fast-growing IT and logistics hub in Central India. Its mission is to bridge the gap between academic learning and industry requirements in the IT sector. - IIIT Nagpur focuses strongly on skill development in emerging and high-impact technologies. The institute offers B.Tech programs in Computer Science and Engineering (CSE), Electronics and Communication Engineering (ECE), and specialized programs such as CSE with a focus on Data Science and Artificial Intelligence. - The curriculum promotes hands-on, project-based learning and benefits from collaborations with major industry partners. Admission to the B.Tech programs is based on performance in the JEE Main exam. With modern infrastructure and active research and innovation centers, IIIT Nagpur is developing quickly as a major technical institution in the region, supported by strong placement outcomes.`, - image:"../src/assets/nagpur.jpeg", - region:"central" - }, - - { - id: 21, - name: "IIIT Pune", - description: `IIIT Pune (Indian Institute of Information Technology, Pune), established in 2016, is an Institute of National Importance operating under the Public-Private Partnership (PPP) model. Situated in Pune — a major hub for IT, automotive, and industrial innovation — the institute benefits from strong connections with technology companies and research organizations. A modern 100-acre permanent campus is currently under development. - IIIT Pune offers B.Tech programs in Computer Science and Engineering (CSE) and Electronics and Communication Engineering (ECE). The curriculum is designed to prepare students for rapidly evolving technological demands, with strong emphasis on research-focused learning and industry exposure. - Admission to the undergraduate programs is based on the JEE Main score. The institute is rising quickly in reputation due to its industry-aligned academics and impressive placement records, drawing prominent domestic and global recruiters from both IT and engineering sectors.`, - image:"../src/assets/pune.jpeg", - region:"west" - }, - - { - id: 22, - name: "IIIT Bhopal", - description: `IIIT Bhopal (Indian Institute of Information Technology, Bhopal), established in 2017, is an Institute of National Importance under the Public-Private Partnership (PPP) model. The institute currently functions from a transit campus at Maulana Azad National Institute of Technology (MANIT), Bhopal, which serves as its mentoring institution and provides developed academic infrastructure. - IIIT Bhopal is dedicated to high-quality education and research in the domain of Information Technology. It offers B.Tech programs in Computer Science and Engineering (CSE), Electronics and Communication Engineering (ECE), and Information Technology (IT). The curriculum is contemporary and constantly evolves to include modern subjects such as Blockchain, Cryptography, Artificial Intelligence, and the Internet of Things (IoT). - Admission to the B.Tech programs is based on the JEE Main score. Despite being a young institute, IIIT Bhopal has quickly gained prominence due to its impressive placement performance, with students receiving offers from leading tech companies such as Amazon and Intuit — demonstrating the institute’s rapid growth, industry relevance, and academic strength.`, - image:"../src/assets/bhopal.jpeg", - region:"central" - }, - { - id: 23, - name: "IIIT Ranchi", - description: `IIIT Ranchi (Indian Institute of Information Technology, Ranchi), established in 2016, is an Institute of National Importance functioning under the Public-Private Partnership (PPP) model. The institute currently operates from a temporary campus in Ranchi while a 66-acre permanent campus is being developed in Kanke. - IIIT Ranchi offers B.Tech programs in Computer Science and Engineering (CSE) and Electronics and Communication Engineering (ECE). It also provides specializations such as CSE with specialization in Data Science and AI, and ECE with specialization in Embedded Systems and IoT. The curriculum is modern and designed to deliver a strong understanding of both hardware and software domains. - Admission to the B.Tech programs is based on the JEE Main score. The institute is steadily building a reputation for excellence with competitive placement records and an academic focus on emerging technologies and entrepreneurship, preparing students to meet the needs of the evolving tech industry.`, - image:"../src/assets/ranchi.jpeg", - region:"east" - }, - { - id: 24, - name: "IIIT Bhagalpur", - description: `IIIT Bhagalpur (Indian Institute of Information Technology, Bhagalpur), established in 2017, is an Institute of National Importance under the Public-Private Partnership (PPP) model and was mentored by IIT Guwahati. The institute has recently shifted to its earthquake- and flood-resistant permanent campus in Sabour, marking a major milestone in its institutional growth. - IIIT Bhagalpur offers B.Tech programs in core branches such as Computer Science Engineering (CSE) and Electronics and Communication Engineering (ECE), along with interdisciplinary programs like Mathematics and Computing and Mechatronics and Automation. The curriculum is highly adaptive and offers specializations in modern areas including AI & Data Science, VLSI & Embedded Systems, and Electric Vehicle Technology. - Admission to the B.Tech programs is based on the JEE Main score. The institute is rapidly building its research and innovation ecosystem by leveraging its permanent infrastructure, strong academic foundation, and mentorship legacy, positioning itself to become a leading technical institute in the region.`, - image:"../src/assets/bhagalpur.jpeg", - region:"east" - }, - { - id: 25, - name: "IIIT Guwahati", - description: `IIIT Guwahati (Indian Institute of Information Technology, Guwahati), established in 2013, is an Institute of National Importance operating under the Public-Private Partnership (PPP) model. The institute is located on its dedicated 70-acre permanent campus in Santola village near Guwahati International Airport, making it a strategic hub for driving technological growth in the North-Eastern region. - IIIT Guwahati is committed to high-quality education and research in Information Technology and allied domains. It offers B.Tech, M.Tech, and Ph.D. programs in Computer Science and Engineering (CSE) and Electronics and Communication Engineering (ECE). The curriculum and academic rigor are structured to meet the needs of the digital economy, with active research in areas such as Machine Learning, Robotics, and Communication Systems. - Admission to the B.Tech programs is based on the JEE Main score. The institute promotes balanced growth by combining strong technical fundamentals with extracurricular exposure, hosting annual technical, cultural, and sports festivals. IIIT Guwahati has emerged as a key academic institution in the North-East, fostering innovation, research, and holistic student development.`, - image:"../src/assets/guwahati.jpeg", - region:"east" - }, - { - id: 26, - name: "IIIT Kalyani", - description: `IIIT Kalyani (Indian Institute of Information Technology, Kalyani), established in 2014, is an Institute of National Importance functioning under the Public-Private Partnership (PPP) model. The permanent 50-acre campus is being developed in Kalyani, an emerging educational hub near Kolkata. The institute was initially mentored by IIT Kharagpur, one of India's most prestigious technical institutes, helping set a strong academic foundation right from the start. - IIIT Kalyani is committed to delivering high-quality technical education in fields central to modern computing and electronics. It offers B.Tech programs in Computer Science and Engineering (CSE) and Electronics and Communication Engineering (ECE), along with Ph.D. and online M.Tech programs. The curriculum is designed with high flexibility, enabling faculty to autonomously structure interdisciplinary and industry-aligned courses. - Admission to B.Tech programs is based on the JEE Main score. A low student-teacher ratio and dynamic governance structure enable effective academic interaction and support productive learning. IIIT Kalyani is steadily positioning itself as a major institute driving technological expertise and research in Eastern India.`, - image:"../src/assets/kalyani.jpeg", - region:"east" - }, - { - id: 27, - name: "IIIT Agartala", - description: `IIIT Agartala (Indian Institute of Information Technology, Agartala), established in 2018, is one of the newer Institutes of National Importance set up under the Public-Private Partnership (PPP) model. The institute currently operates from a transit campus within the National Institute of Technology (NIT) Agartala, which also serves as its mentoring institution. This enables students to benefit from NIT Agartala's established infrastructure, academic ecosystem, and experienced faculty. - IIIT Agartala emphasizes a focused and high-quality academic structure rather than an overly broad one. It currently offers a B.Tech program in Computer Science and Engineering (CSE), with a curriculum shaped to meet national and global technological standards. The institute nurtures strong research orientation in contemporary fields such as Data Analytics, Machine Learning, and Cloud Computing. - Admissions are based on JEE Main rank. IIIT Agartala aims to produce highly skilled engineers who can excel in innovation, product development, and real-world problem solving — contributing to sustainable growth in both the northeastern region and the rest of the country.`, - image:"../src/assets/agartala.jpeg", - region:"east" - }, - { - id: 28, - name: "IIIT Manipur", - description: `IIIT Manipur (Indian Institute of Information Technology, Senapati, Manipur) is an Institute of National Importance established in 2015 under the Public-Private Partnership (PPP) model. While the permanent 150-acre campus is coming up in the Senapati district, the institute currently operates from its city campus in Mantripukhri, Imphal, located close to major government and IT facilities. - IIIT Manipur delivers excellence in Information Technology and related fields, offering B.Tech programs in Computer Science and Engineering (CSE) and Electronics and Communication Engineering (ECE). The institute also promotes advanced research through its Ph.D. programs in domains like Speech Processing, Data Mining, and VLSI & Embedded Systems. Its geographic setting encourages research tailored to solving real-world and regional challenges through technology. - Admission to B.Tech programs is based on JEE Main rank. IIIT Manipur is rapidly building a strong academic and innovation ecosystem supported by active technical clubs in AI/ML, Cyber Security, and Web Development.`, - image:"../src/assets/manipur.jpeg", - region:"east" - }, -]; - -export default Data; +const Data = [ + { + id: 1, + name: "IIIT Bangalore", + description: `International Institute of Information Technology, Bangalore (IIIT Bangalore) is a research-focused institute located in Electronics City, Bengaluru. + It was established in 1999 and is known for its strong academic rigor, industry collaboration, and specialization in computer science and data-driven fields. + The institute offers programs such as Integrated M.Tech, M.Tech, MSc (Digital Society), and PhD, and consistently records excellent placements in software development, AI/ML, cybersecurity, and research roles.`, + image:"../src/assets/banglore.webp", + region:"south" + }, + + { + id: 2, + name: "IIIT Lucknow", + description: `IIIT Lucknow (Indian Institute of Information Technology, Lucknow) is an Institute of National Importance established in 2015 by the Ministry of Education under the Public-Private Partnership (PPP) model. Situated in the capital city of Uttar Pradesh, the institute aims to be a leading center for education, research, and innovation in the field of Information Technology (IT). + IIIT Lucknow is highly sought after for its strong academic curriculum and outstanding placement performance. The institute offers B.Tech programs in Computer Science and Engineering (CSE) and Information Technology (IT), along with a distinctive B.Tech/M.Tech Dual Degree program in CSE and an MBA with specialization in IT. + Admission to the B.Tech courses is based on JEE Main rank, making the competition very intense. The institute provides a highly rigorous academic environment and modern research exposure in emerging domains such as Artificial Intelligence and Machine Learning. IIIT Lucknow has rapidly risen in prominence due to its impressive placement statistics, attracting top tech companies and offering high salary packages, placing it among the leading IIITs in the country.`, + image:"../src/assets/lucknow.jpeg", + region:"north" + }, + + { + id: 3, + name: "IIIT Bhubaneswar – Odisha", + description: ` + The International Institute of Information Technology, Bhubaneswar (IIIT Bhubaneswar) is a premier technical university established in 2006 by the Government of Odisha with a focus on advancing education, research, and innovation in Information Technology and related fields. + It operates under a unique public–private partnership model, combining strong academic foundations with industry-oriented training. + Located on an expansive 23-acre modern campus in Gothapatna, Bhubaneswar, the institute offers B.Tech programs in Computer Science and Engineering, Information Technology, Electrical and Electronics Engineering, and Electronics and Telecommunication Engineering, along with M.Tech and PhD programs. + Admission to the B.Tech courses is based on JEE Main scores through the JoSAA/CSAB counseling process. The institute has built a consistent track record in placements, with leading multinational companies and fast-growing tech startups recruiting students. + IIIT Bhubaneswar emphasizes holistic student development through tech clubs, research labs, incubators, and strong industry collaboration, making it a dynamic hub for technological learning and career growth.`, + image:"../src/assets/bhubneshwar.jpeg", + region:"east" + }, + + { + id: 4, + name: "IIIT Allahabad", + description: `IIIT Allahabad (IIITA), located in Prayagraj, Uttar Pradesh, is an Institute of National Importance established in 1999. It was founded to serve as a center of excellence in the rapidly advancing fields of Information Technology (IT) and Electronics and Communication Engineering (ECE), along with allied domains including Management and Applied Science. + IIITA is consistently ranked among the top technical institutes in India, recognized for its challenging academic environment and strong emphasis on research and innovation. The institute offers B.Tech across four specializations, as well as M.Tech, MBA, and PhD programs, attracting talented students from across the nation. + The institute is particularly known for its strong coding and competitive programming culture, which has produced numerous global programming contest achievers. This culture of hands-on problem solving directly contributes to excellent placement records, with leading global and domestic tech companies visiting the campus every year. + The campus is spread over 100 acres, fully residential, and equipped with modern infrastructure designed to support both academic growth and personal development.`, + image:"../src/assets/allahabad.jpeg", + region:"north" + }, + + { + id: 5, + name: "IIIT Hyderabad", + description: `The International Institute of Information Technology, Hyderabad (IIIT-H), established in 1998, is a highly reputed, autonomous, not-for-profit university operating under a Public-Private Partnership (N-PPP) model. Widely regarded as one of India's premier technical research institutions, it is a peer to the top IITs and the first IIIT established under this framework. IIIT-H is renowned for its strong focus on research and technology application for both industry and society, encouraging undergraduate students to participate in cutting-edge projects. + The institute is famous for its innovative curriculum, offering flexible programs like the Dual Degree (B.Tech + M.S. by Research) and its unique research-led teaching model. Key research areas include Artificial Intelligence, Robotics, Data Science, and Cognitive Science, housed in world-renowned centers of excellence. Admission is extremely competitive, often through JEE Main, but also via a special research pathway. IIIT-H consistently records exceptional placements with high average salary packages, reflecting the industry's premium on its research-focused graduates.`, + image:"../src/assets/hyderabad.jpeg", + region:"south" + }, + + { + id: 6, + name: "IIIT Delhi", + description: `The Indraprastha Institute of Information Technology, Delhi (IIIT-Delhi), established in 2008 by an Act of the Delhi Legislature, is a comprehensive research-led teaching institute with the empowering authority to grant its own degrees. It is a public, state-level technical university that has rapidly distinguished itself as a top-tier institution in India for Computer Science and Electronics education and research. + IIIT-Delhi places a strong emphasis on interdisciplinary research and a project-based curriculum. It offers B.Tech, M.Tech, and PhD programs across departments like Computer Science and Engineering (CSE), Electronics & Communication Engineering (ECE), and specialized areas such as Computational Biology and Human Centered Design. It houses numerous Centers of Excellence, including those for AI, Cybersecurity, and Design, reflecting its deep commitment to societal and technological challenges. + Admission is highly competitive and primarily through the JEE Main score. The institute boasts excellent placements and a supportive environment, consistently attracting meritorious students and globally recognized faculty.`, + image:"../src/assets/delhi.jpeg", + region:"north" + }, + { + id: 7, + name: "IIIT Gwalior", + description: `ABV-IIITM Gwalior, located in Madhya Pradesh, is an Institute of National Importance established in 1997 by the Government of India. It was the first IIIT founded with the mission of integrating Information Technology and Business Management. The institute was renamed in 2002 to honor former Prime Minister Atal Bihari Vajpayee. + The institute is situated on a lush 160-acre campus near the historic Gwalior Fort. It is well known for its five-year Integrated Post Graduate (IPG) programs that offer B.Tech + M.Tech or B.Tech + MBA degrees, along with regular B.Tech, M.Tech, MBA, and PhD programs. The curriculum focuses on producing professionals skilled in both technology and business. + ABV-IIITM maintains consistently strong placement performance, with high average and top CTCs, making it one of the finest technical and management institutes in India. Admissions to the B.Tech and IPG programs are based on JEE Main scores.`, + image:"../src/assets/gwalior.jpeg", + region:"central" + }, + { + id: 8, + name: "IIIT Kota", + description: `The Indian Institute of Information Technology, Kota, is an Institute of National Importance established in 2013 under the Public-Private Partnership (PPP) model. It is uniquely mentored by the Malaviya National Institute of Technology (MNIT), Jaipur, and currently operates from the MNIT campus, benefiting immensely from its resources, faculty expertise, and established infrastructure. This mentorship ensures that IIIT Kota maintains a high academic standard right from its inception. + The institute is focused on producing high-quality engineers in the fields of Information Technology (IT) and Electronics and Communication Engineering (ECE) to meet the demands of the modern industrial economy. It offers B.Tech programs in Computer Science and Engineering (CSE) and Electronics and Communication Engineering (ECE), with intake based on the JEE Main score. + IIIT Kota’s curriculum is contemporary, emphasizing strong foundational knowledge in core engineering coupled with a practical, hands-on approach. Despite operating from a temporary campus, the institute boasts competitive placement statistics, leveraging the strong industry connections fostered by its mentoring institution, MNIT Jaipur. The future permanent campus in Kota is being planned to become a dedicated center for innovation and research.`, + image:"../src/assets/kota.jpeg", + region:"north" + }, + { + id: 9, + name: "📐 IIITDM Kancheepuram – Tamil Nadu", + description: `The Indian Institute of Information Technology Design and Manufacturing (IIITDM), Kancheepuram, is a specialized Institute of National Importance established in 2007 by the Ministry of Education, Government of India. It operates with a distinctive interdisciplinary focus, integrating Information Technology (IT), Design, and Manufacturing to address the intricate demands of product development in the modern industrial landscape. + Located on a state-of-the-art 51-acre campus near Chennai, IIITDM Kancheepuram is strategically placed within a major industrial and technological hub. The institute is known for its unique curriculum that emphasizes a "design-centric" approach across its engineering disciplines. It offers specialized Dual Degree programs (B.Tech + M.Tech) in fields like Computer Science with specialization in Design, as well as B.Tech, M.Tech, M.Des, and PhD degrees. + A core strength of the institute is its focus on product innovation and practical application, featuring advanced manufacturing and design laboratories. Admission to its undergraduate programs is primarily based on the JEE Main score. IIITDM Kancheepuram maintains a strong reputation for placements, particularly in the manufacturing and software industries, where its graduates' unique skill set is highly valued.`, + image:"../src/assets/kanchepuram.jpeg", + region:"north" + }, + { + id: 10, + name: "💡 IIITDM Jabalpur – Madhya Pradesh", + description: `The Pandit Dwarka Prasad Mishra Indian Institute of Information Technology, Design and Manufacturing (PDPM IIITDM), Jabalpur, is a specialized Institute of National Importance established in 2005 by the Government of India. Its unique mandate is to foster excellence in research and education by integrating Information Technology (IT) with Design and Manufacturing processes, aligning with national initiatives like 'Make in India' and 'Digital India.' + Located on a sprawling 260-acre campus near the Dumna Nature Reserve, IIITDM Jabalpur offers diverse academic programs, including B.Tech, B.Des, M.Tech, M.Des, and PhD. The institute is one of the few IIITs offering a specialized B.Des (Bachelor of Design) degree and B.Tech in Smart Manufacturing, showcasing its interdisciplinary focus. The curriculum strongly emphasizes hands-on experience and project-based learning to create industry-ready professionals. + Admission to its undergraduate programs (B.Tech and B.Des) is primarily through JEE Main and UCEED scores, respectively. The institute maintains a commendable placement record, attracting top companies with high domestic and international package offers.`, + image:"../src/assets/jabalpur.jpeg", + region:"central" + }, + { + id: 11, + name: "💎 IIITDM Kurnool – Andhra Pradesh", + description: ` + The Indian Institute of Information Technology Design and Manufacturing (IIITDM), Kurnool, is a new-generation, specialized Institute of National Importance established in 2015 by the Ministry of Education, Government of India. It was founded under the Andhra Pradesh Reorganization Act, 2014, with the primary objective of creating a center for interdisciplinary excellence integrating Information Technology (IT), Design, and Manufacturing. + Located on a picturesque 151-acre campus at Jagannathagattu, the institute promotes a unique, hands-on, and design-centric engineering education. IIITDM Kurnool offers B.Tech programs in specialized fields, including Computer Science and Engineering, Electronics and Communication Engineering (with Design & Manufacturing), Mechanical Engineering (with Design & Manufacturing), and Artificial Intelligence & Data Science. It also offers Dual Degree (B.Tech + M.Tech) and Ph.D. programs. + Admission to the undergraduate programs is based on the JEE Main score. The institute is rapidly developing its infrastructure and has shown promising placement records, with graduates being recruited by top-tier IT and core engineering companies, reflecting the industry's demand for its uniquely skilled, interdisciplinary professionals.`, + image:"../src/assets/kurnool.jpeg", + region:"south" + }, + + { + id:12, + name: "🏔️ IIIT Una – Himachal Pradesh", + description: ` + The Indian Institute of Information Technology, Una, is one of the 20 new IIITs established under the Public-Private Partnership (PPP) model and was granted the status of an Institute of National Importance in 2017. Located temporarily in Una, Himachal Pradesh, the institute is focused on becoming a hub for IT education and research in the Himalayan region. Its establishment aims at bridging the gap between regional industry needs and academic output in the fields of Information Technology (IT) and Electronics and Communication Engineering (ECE). + IIIT Una currently offers B.Tech programs in Computer Science and Engineering (CSE), Electronics and Communication Engineering (ECE), and Information Technology (IT), along with Ph.D. programs. The institute was initially mentored by NIT Hamirpur, helping establish a strong academic framework during its formative years. + The curriculum is highly industry-relevant, emphasizing contemporary computational techniques and hardware design to help students adapt to modern technological demands. Admission to the B.Tech programs is based on the JEE Main score. As IIIT Una transitions to its permanent campus, it continues to attract motivated students and faculty, contributing meaningfully to the technological growth of the region.`, + image:"../src/assets/una.jpeg", + region:"north" + }, + + { + id: 13, + name: "📡 IIIT Sonepat – Haryana", + description: ` + The Indian Institute of Information Technology, Sonepat, is one of the IIITs established on the Public-Private Partnership (PPP) model and was accorded the status of an Institute of National Importance in 2017. Although officially located in Sonepat, Haryana, the institute currently operates from a temporary campus at the National Institute of Technology (NIT) Kurukshetra, with NIT Kurukshetra serving as its mentoring institution. + IIIT Sonepat benefits immensely from its proximity to the National Capital Region (NCR), giving students access to a wide ecosystem of industries and tech companies. The institution is dedicated to creating a center of excellence in core computing and engineering disciplines. It offers B.Tech programs in Computer Science and Engineering (CSE) and Electronics and Communication Engineering (ECE), along with doctoral programs. + The curriculum is designed to be highly relevant to industry expectations, with an emphasis on cutting-edge areas such as Artificial Intelligence, Machine Learning, and Data Science. Admission to the B.Tech programs is based on JEE Main performance. With support from NIT Kurukshetra and growing industry collaboration, IIIT Sonepat is rapidly shaping into a strong technical institute, preparing students for impactful careers in the global IT sector.`, + image:"../src/assets/soni.jpeg", + region:"north" + }, + + { + id: 14, + name: "🌴 IIIT Sri City (IIITSRI) – Andhra Pradesh", + description: `The Indian Institute of Information Technology, Sri City (IIITSRI), established in 2013, is an Institute of National Importance located in the integrated business city of Sri City, Andhra Pradesh. It was founded under the Public-Private Partnership (PPP) model with funding from the Central Government, State Government, and industry partners. Initially mentored by IIIT Hyderabad, the institute quickly adopted a high standard of academic rigor and industry-centric focus. + Strategically situated near Chennai's industrial and tech corridor, IIITSRI specializes in integrating Information Technology with the demands of industrial technology. It offers B.Tech programs in traditional disciplines like CSE and ECE, along with modern specializations like Artificial Intelligence & Data Science (AI & DS). The curriculum is designed to be highly practical, preparing students for the advanced requirements of the IT and manufacturing sectors. Admission is based on the JEE Main score. + IIIT Sri City leverages its industrial ecosystem for strong collaborations, internships, and placements, ensuring its graduates are well-equipped for the modern workplace.`, + image:"../src/assets/sricity.jpeg", + region:"south" + }, + + { + id:15, + name: "🏞️ IIIT Dharwad – Karnataka", + description: ` + The Indian Institute of Information Technology, Dharwad (IIIT Dharwad), established in 2015, is an Institute of National Importance operating under the Public-Private Partnership (PPP) model. It is strategically funded by the Government of India, the Government of Karnataka, and industry partner KEONICS, reflecting a commitment to regional technological development. The institute shifted to its permanent, sprawling 60-acre campus near Dharwad, a growing educational and industrial hub. + IIIT Dharwad is focused on delivering a strong, applied-research-oriented curriculum in core IT and emerging fields. It offers B.Tech programs in Computer Science and Engineering (CSE), Electronics and Communication Engineering (ECE), and a highly specialized B.Tech in Data Science and Artificial Intelligence. This focus on contemporary fields prepares graduates for high-demand roles. Admission to undergraduate programs is based on the JEE Main score. + Despite being a relatively young institution, it maintains a self-sustaining model and is recognized for its dynamic academic environment and competitive placement record, driven by the strong industry demand in the Karnataka region`, + image:"../src/assets/dharwad.jpeg", + region:"south" + }, + + { + id: 16, + name: "✨ IIIT Raichur – Karnataka", + description: ` + The Indian Institute of Information Technology, Raichur (IIIT Raichur), is one of the newest IIITs, established in 2019 under the Public-Private Partnership (PPP) model and recognized as an Institute of National Importance in 2020. The institute was initially mentored by the prestigious IIT Hyderabad, ensuring a robust academic foundation and guidance in its formative years. IIIT Raichur currently operates from a transit campus in Raichur, with a permanent 74-acre campus under development. + The institute is rapidly emerging as a center for excellence in core computing. It offers B.Tech programs in Computer Science and Engineering (CSE) and the highly sought-after Artificial Intelligence & Data Science (AI & DS). IIIT Raichur follows a unique Fractal Academic System which allows for continuous evaluation and flexibility in learning. + Despite its nascent stage, the institute has demonstrated extremely strong potential, with reported high average placement packages for its first few graduating batches. Admission is highly selective and based on the JEE Main score, drawing talented students motivated by the institute's promising trajectory and research focus.`, + image:"../src/assets/raichur.jpeg", + region:"south" + }, + + { + id:16, + name: "🟦 IIIT Kottayam – Kerala", + description: `The Indian Institute of Information Technology, Kottayam (IIIT Kottayam), established in 2015, is an Institute of National Importance set up under the Public-Private Partnership (PPP) model. The institute is located on its permanent, state-of-the-art campus in Valavoor, Kottayam, providing a dedicated and tranquil environment for learning and research. It is focused on cultivating talent in Information Technology and allied disciplines, with a strong emphasis on societal impact. + IIIT Kottayam offers B.Tech programs in Computer Science and Engineering (CSE) and Electronics and Communication Engineering (ECE), alongside specializations in CSE with AI & Data Science and ECE with Cyber Security. A key feature of the institute is its commitment to innovation and entrepreneurship, housing a dedicated Incubation Centre and specialized labs like CyberLabs and FACTS-H Lab. + The curriculum is designed to be contemporary and problem-solving focused, preparing students to address both local and global technological challenges. Admission is through the JEE Main score. The institute is dedicated to producing ethically sound, technically proficient, and socially conscious graduates.`, + image:"../src/assets/kottyam.jpeg", + region:"south" + }, + + { + id: 17, + name: "IIIT Tiruchirappalli", + description: `IIIT Tiruchirappalli (Indian Institute of Information Technology, Tiruchirappalli), established in 2013, is an Institute of National Importance operating under the Public-Private Partnership (PPP) model. Initially mentored by NIT Tiruchirappalli (NITT), the institute benefited greatly from the academic culture and infrastructure of a premier national institution. It is currently transitioning to its upcoming permanent campus in Sethurappatti, near Trichy. + IIIT Tiruchirappalli maintains a strong academic profile and offers B.Tech programs in Computer Science and Engineering (CSE) and Electronics and Communication Engineering (ECE), along with M.Tech and Ph.D. programs in specialized domains. The institute emphasizes research in areas such as Data Analytics, Machine Learning, IoT, and VLSI Systems. + The curriculum blends core engineering fundamentals with hands-on exposure to emerging technologies, ensuring that students develop both theoretical depth and practical skill sets. Admission to the B.Tech programs is based on the JEE Main score. With solid academic mentorship, growing industry partnerships, and a focus on future-ready learning, IIIT-T is committed to producing highly skilled IT professionals.`, + image:"../src/assets/trichy.jpeg", + region:"south" + }, + { + id: 18, + name: "IIIT Vadodara", + description: `IIIT Vadodara (Indian Institute of Information Technology, Vadodara), established in 2013, is an Institute of National Importance operating under the Public-Private Partnership (PPP) model. The institute currently functions from a temporary campus in Gandhinagar and benefits from the region’s strong presence of IT and corporate sectors. It was founded through a joint initiative by the Government of India, the Government of Gujarat, and key industry partners. + IIIT Vadodara focuses heavily on research and applied Information Technology. It offers B.Tech programs in Computer Science and Engineering (CSE) and Information Technology (IT), along with M.Tech and Ph.D. programs. A significant development in its expansion is the establishment of its satellite campus, IIITV-International Campus Diu (IIITV-ICD). + The curriculum emphasizes a blend of theoretical depth and practical application, ensuring industry-ready skill development. Admission to the B.Tech program is based on the JEE Main score. Leveraging its strategic location, strong industrial collaborations, and rapidly advancing academic ecosystem, IIIT Vadodara has earned an excellent reputation for competitive placement performance, consistently attracting top recruiters in the technology sector.`, + image:"../src/assets/vadodara.jpeg", + region:"east" + }, + + { + id: 19, + name: "IIIT Surat", + description: `IIIT Surat (Indian Institute of Information Technology, Surat), established in 2017, is an Institute of National Importance functioning under the Public-Private Partnership (PPP) model. The institute currently operates from a transit campus within SVNIT Surat, which also serves as its mentoring institution, allowing students to benefit from the well-established facilities and experienced faculty of a premier NIT. + IIIT Surat is focused on producing high-quality professionals in computing and electronics. It offers B.Tech programs in Computer Science and Engineering (CSE) and Electronics and Communication Engineering (ECE), along with Ph.D. programs. The curriculum is aligned with modern industry expectations, emphasizing research, innovation, and hands-on learning based on evolving technological trends. + Admission to the B.Tech programs is based exclusively on the JEE Main score. Although a relatively new IIIT, its strategic location in the industrial hub of Surat and its collaboration with SVNIT have helped the institute secure strong industrial exposure and promising placement opportunities, enabling rapid institutional growth.`, + image:"../src/assets/surat.jpeg", + region:"east" + }, + + { + id: 20, + name: "IIIT Nagpur", + description: `IIIT Nagpur (Indian Institute of Information Technology, Nagpur), established in 2016, is an Institute of National Importance under the Public-Private Partnership (PPP) model. The institute is located on a 100-acre permanent campus in Nagpur — a fast-growing IT and logistics hub in Central India. Its mission is to bridge the gap between academic learning and industry requirements in the IT sector. + IIIT Nagpur focuses strongly on skill development in emerging and high-impact technologies. The institute offers B.Tech programs in Computer Science and Engineering (CSE), Electronics and Communication Engineering (ECE), and specialized programs such as CSE with a focus on Data Science and Artificial Intelligence. + The curriculum promotes hands-on, project-based learning and benefits from collaborations with major industry partners. Admission to the B.Tech programs is based on performance in the JEE Main exam. With modern infrastructure and active research and innovation centers, IIIT Nagpur is developing quickly as a major technical institution in the region, supported by strong placement outcomes.`, + image:"../src/assets/nagpur.jpeg", + region:"central" + }, + + { + id: 21, + name: "IIIT Pune", + description: `IIIT Pune (Indian Institute of Information Technology, Pune), established in 2016, is an Institute of National Importance operating under the Public-Private Partnership (PPP) model. Situated in Pune — a major hub for IT, automotive, and industrial innovation — the institute benefits from strong connections with technology companies and research organizations. A modern 100-acre permanent campus is currently under development. + IIIT Pune offers B.Tech programs in Computer Science and Engineering (CSE) and Electronics and Communication Engineering (ECE). The curriculum is designed to prepare students for rapidly evolving technological demands, with strong emphasis on research-focused learning and industry exposure. + Admission to the undergraduate programs is based on the JEE Main score. The institute is rising quickly in reputation due to its industry-aligned academics and impressive placement records, drawing prominent domestic and global recruiters from both IT and engineering sectors.`, + image:"../src/assets/pune.jpeg", + region:"west" + }, + + { + id: 22, + name: "IIIT Bhopal", + description: `IIIT Bhopal (Indian Institute of Information Technology, Bhopal), established in 2017, is an Institute of National Importance under the Public-Private Partnership (PPP) model. The institute currently functions from a transit campus at Maulana Azad National Institute of Technology (MANIT), Bhopal, which serves as its mentoring institution and provides developed academic infrastructure. + IIIT Bhopal is dedicated to high-quality education and research in the domain of Information Technology. It offers B.Tech programs in Computer Science and Engineering (CSE), Electronics and Communication Engineering (ECE), and Information Technology (IT). The curriculum is contemporary and constantly evolves to include modern subjects such as Blockchain, Cryptography, Artificial Intelligence, and the Internet of Things (IoT). + Admission to the B.Tech programs is based on the JEE Main score. Despite being a young institute, IIIT Bhopal has quickly gained prominence due to its impressive placement performance, with students receiving offers from leading tech companies such as Amazon and Intuit — demonstrating the institute’s rapid growth, industry relevance, and academic strength.`, + image:"../src/assets/bhopal.jpeg", + region:"central" + }, + { + id: 23, + name: "IIIT Ranchi", + description: `IIIT Ranchi (Indian Institute of Information Technology, Ranchi), established in 2016, is an Institute of National Importance functioning under the Public-Private Partnership (PPP) model. The institute currently operates from a temporary campus in Ranchi while a 66-acre permanent campus is being developed in Kanke. + IIIT Ranchi offers B.Tech programs in Computer Science and Engineering (CSE) and Electronics and Communication Engineering (ECE). It also provides specializations such as CSE with specialization in Data Science and AI, and ECE with specialization in Embedded Systems and IoT. The curriculum is modern and designed to deliver a strong understanding of both hardware and software domains. + Admission to the B.Tech programs is based on the JEE Main score. The institute is steadily building a reputation for excellence with competitive placement records and an academic focus on emerging technologies and entrepreneurship, preparing students to meet the needs of the evolving tech industry.`, + image:"../src/assets/ranchi.jpeg", + region:"east" + }, + { + id: 24, + name: "IIIT Bhagalpur", + description: `IIIT Bhagalpur (Indian Institute of Information Technology, Bhagalpur), established in 2017, is an Institute of National Importance under the Public-Private Partnership (PPP) model and was mentored by IIT Guwahati. The institute has recently shifted to its earthquake- and flood-resistant permanent campus in Sabour, marking a major milestone in its institutional growth. + IIIT Bhagalpur offers B.Tech programs in core branches such as Computer Science Engineering (CSE) and Electronics and Communication Engineering (ECE), along with interdisciplinary programs like Mathematics and Computing and Mechatronics and Automation. The curriculum is highly adaptive and offers specializations in modern areas including AI & Data Science, VLSI & Embedded Systems, and Electric Vehicle Technology. + Admission to the B.Tech programs is based on the JEE Main score. The institute is rapidly building its research and innovation ecosystem by leveraging its permanent infrastructure, strong academic foundation, and mentorship legacy, positioning itself to become a leading technical institute in the region.`, + image:"../src/assets/bhagalpur.jpeg", + region:"east" + }, + { + id: 25, + name: "IIIT Guwahati", + description: `IIIT Guwahati (Indian Institute of Information Technology, Guwahati), established in 2013, is an Institute of National Importance operating under the Public-Private Partnership (PPP) model. The institute is located on its dedicated 70-acre permanent campus in Santola village near Guwahati International Airport, making it a strategic hub for driving technological growth in the North-Eastern region. + IIIT Guwahati is committed to high-quality education and research in Information Technology and allied domains. It offers B.Tech, M.Tech, and Ph.D. programs in Computer Science and Engineering (CSE) and Electronics and Communication Engineering (ECE). The curriculum and academic rigor are structured to meet the needs of the digital economy, with active research in areas such as Machine Learning, Robotics, and Communication Systems. + Admission to the B.Tech programs is based on the JEE Main score. The institute promotes balanced growth by combining strong technical fundamentals with extracurricular exposure, hosting annual technical, cultural, and sports festivals. IIIT Guwahati has emerged as a key academic institution in the North-East, fostering innovation, research, and holistic student development.`, + image:"../src/assets/guwahati.jpeg", + region:"east" + }, + { + id: 26, + name: "IIIT Kalyani", + description: `IIIT Kalyani (Indian Institute of Information Technology, Kalyani), established in 2014, is an Institute of National Importance functioning under the Public-Private Partnership (PPP) model. The permanent 50-acre campus is being developed in Kalyani, an emerging educational hub near Kolkata. The institute was initially mentored by IIT Kharagpur, one of India's most prestigious technical institutes, helping set a strong academic foundation right from the start. + IIIT Kalyani is committed to delivering high-quality technical education in fields central to modern computing and electronics. It offers B.Tech programs in Computer Science and Engineering (CSE) and Electronics and Communication Engineering (ECE), along with Ph.D. and online M.Tech programs. The curriculum is designed with high flexibility, enabling faculty to autonomously structure interdisciplinary and industry-aligned courses. + Admission to B.Tech programs is based on the JEE Main score. A low student-teacher ratio and dynamic governance structure enable effective academic interaction and support productive learning. IIIT Kalyani is steadily positioning itself as a major institute driving technological expertise and research in Eastern India.`, + image:"../src/assets/kalyani.jpeg", + region:"east" + }, + { + id: 27, + name: "IIIT Agartala", + description: `IIIT Agartala (Indian Institute of Information Technology, Agartala), established in 2018, is one of the newer Institutes of National Importance set up under the Public-Private Partnership (PPP) model. The institute currently operates from a transit campus within the National Institute of Technology (NIT) Agartala, which also serves as its mentoring institution. This enables students to benefit from NIT Agartala's established infrastructure, academic ecosystem, and experienced faculty. + IIIT Agartala emphasizes a focused and high-quality academic structure rather than an overly broad one. It currently offers a B.Tech program in Computer Science and Engineering (CSE), with a curriculum shaped to meet national and global technological standards. The institute nurtures strong research orientation in contemporary fields such as Data Analytics, Machine Learning, and Cloud Computing. + Admissions are based on JEE Main rank. IIIT Agartala aims to produce highly skilled engineers who can excel in innovation, product development, and real-world problem solving — contributing to sustainable growth in both the northeastern region and the rest of the country.`, + image:"../src/assets/agartala.jpeg", + region:"east" + }, + { + id: 28, + name: "IIIT Manipur", + description: `IIIT Manipur (Indian Institute of Information Technology, Senapati, Manipur) is an Institute of National Importance established in 2015 under the Public-Private Partnership (PPP) model. While the permanent 150-acre campus is coming up in the Senapati district, the institute currently operates from its city campus in Mantripukhri, Imphal, located close to major government and IT facilities. + IIIT Manipur delivers excellence in Information Technology and related fields, offering B.Tech programs in Computer Science and Engineering (CSE) and Electronics and Communication Engineering (ECE). The institute also promotes advanced research through its Ph.D. programs in domains like Speech Processing, Data Mining, and VLSI & Embedded Systems. Its geographic setting encourages research tailored to solving real-world and regional challenges through technology. + Admission to B.Tech programs is based on JEE Main rank. IIIT Manipur is rapidly building a strong academic and innovation ecosystem supported by active technical clubs in AI/ML, Cyber Security, and Web Development.`, + image:"../src/assets/manipur.jpeg", + region:"east" + }, +]; + +export default Data; diff --git a/src/Components/Logo.jsx b/src/Components/Logo.jsx index ab6cef9..1615ca0 100644 --- a/src/Components/Logo.jsx +++ b/src/Components/Logo.jsx @@ -1,9 +1,9 @@ -import React from 'react' - +import React from 'react' + const Logo = () => { return ( - Logo + Logo ) } - -export default Logo + +export default Logo diff --git a/src/Components/Name.jsx b/src/Components/Name.jsx index 23d0097..cd979cb 100644 --- a/src/Components/Name.jsx +++ b/src/Components/Name.jsx @@ -1,11 +1,11 @@ -import React from 'react' - -const Name = () => { - return ( - - IIIT INSIDER - - ) -} - -export default Name +import React from 'react' + +const Name = () => { + return ( + + IIIT INSIDER + + ) +} + +export default Name diff --git a/src/Components/Navbar.jsx b/src/Components/Navbar.jsx index f0799fb..c2d046f 100644 --- a/src/Components/Navbar.jsx +++ b/src/Components/Navbar.jsx @@ -1,50 +1,54 @@ -import React from "react"; -import Name from "./Name"; -import Logo from "./Logo"; -import Button from "./Button"; -import Data from "./Data"; -import { NavLink } from "react-router-dom"; - -const Navbar = () => { - const filterCollege = function (region) { - if (region === "all") { - return Data; - } - return Data.filter((item) => item.region === region); - }; - - return ( -
-
- - -
-
-
-
-
-
-
-
- ); -}; - -export default Navbar; +import React from "react"; +import Name from "./Name"; +import Logo from "./Logo"; +import Button from "./Button"; +import Data from "./Data"; +import { NavLink } from "react-router-dom"; +import NotificationButton from "./NotificationButton"; +import SocialShare from "./SocialShare"; + +const Navbar = () => { + const filterCollege = function (region) { + if (region === "all") { + return Data; + } + return Data.filter((item) => item.region === region); + }; + + return ( +
+
+ + +
+
+
+
+
+
+ + +
+
+ ); +}; + +export default Navbar; diff --git a/src/Components/NotificationButton.jsx b/src/Components/NotificationButton.jsx new file mode 100644 index 0000000..5d4ae6e --- /dev/null +++ b/src/Components/NotificationButton.jsx @@ -0,0 +1,167 @@ +import React, { useState, useEffect } from 'react'; +import api from '../services/api'; +import { requestFirebaseNotificationToken } from '../services/firebaseService'; + +function getDeviceName() { + const platform = navigator.platform || 'Unknown platform'; + const browser = navigator.userAgent.includes('Chrome') + ? 'Chrome' + : navigator.userAgent.includes('Firefox') + ? 'Firefox' + : navigator.userAgent.includes('Safari') + ? 'Safari' + : 'Browser'; + + return `${browser} on ${platform}`.slice(0, 100); +} + +const NotificationButton = () => { + const [isSubscribed, setIsSubscribed] = useState(false); + const [loading, setLoading] = useState(false); + const [showMenu, setShowMenu] = useState(false); + + useEffect(() => { + const subscribed = localStorage.getItem('notificationsSubscribed'); + setIsSubscribed(subscribed === 'true'); + }, []); + + const requestNotificationPermission = async () => { + if (!('Notification' in window)) { + alert('This browser does not support notifications'); + return; + } + + const permission = await Notification.requestPermission(); + if (permission !== 'granted') { + alert('Notification permission denied'); + return; + } + + setLoading(true); + try { + if (!api.auth.isLoggedIn()) { + alert('Please log in before enabling notifications'); + return; + } + + const token = await requestFirebaseNotificationToken(); + if (!token) { + throw new Error('Firebase did not return a device token'); + } + + await api.notifications.registerDevice(token, 'WEB', getDeviceName()); + + localStorage.setItem('notificationsSubscribed', 'true'); + localStorage.setItem('notificationToken', token); + setIsSubscribed(true); + + new Notification('Notifications Enabled', { + body: 'You will now receive updates from IIIT Insider', + icon: '/logo.png' + }); + } catch (error) { + console.error('Error subscribing to notifications:', error); + alert(`Failed to subscribe to notifications: ${error.message}`); + } finally { + setLoading(false); + } + }; + + const unsubscribe = async () => { + try { + const token = localStorage.getItem('notificationToken'); + if (token) { + await api.notifications.unregisterDevice(token); + } + + localStorage.removeItem('notificationsSubscribed'); + localStorage.removeItem('notificationToken'); + setIsSubscribed(false); + + new Notification('Notifications Disabled', { + body: 'You have unsubscribed from notifications', + icon: '/logo.png' + }); + } catch (error) { + console.error('Error unsubscribing:', error); + } + }; + + const sendTestNotification = async () => { + try { + if (!api.auth.isLoggedIn()) { + alert('Please log in before sending a test notification'); + return; + } + + await api.notifications.sendNotification( + 'Test Notification', + 'This is a test notification from IIIT Insider', + '' + ); + alert('Test notification sent!'); + } catch (error) { + alert('Failed to send notification: ' + error.message); + } + }; + + return ( +
+ + + {showMenu && ( + <> +
setShowMenu(false)} + /> +
+

Notification Settings

+ + {isSubscribed ? ( +
+
+ + Notifications enabled +
+ + +
+ ) : ( + + )} +
+ + )} +
+ ); +}; + +export default NotificationButton; diff --git a/src/Components/QuickLink.jsx b/src/Components/QuickLink.jsx index 1d13465..d6105f3 100644 --- a/src/Components/QuickLink.jsx +++ b/src/Components/QuickLink.jsx @@ -2,166 +2,153 @@ import React from "react"; import Logo from "./Logo"; import Name from "./Name"; import Button from "./Button"; -import Data from "../Components/Data" import { NavLink } from "react-router-dom"; const QuickLink = () => { - const filterCollege=function(region){ - if(region==='all'){ - return Data - } - return Data.filter((item)=>item.region===region) - } - return ( -
-
-
- - - - Your Preferred Place For Dreams - -
- - -
- Quick Link -
- - -
- - Region Wise College - - +
+
+
+ + + + Your Preferred Place For Dreams + +
+ + +
+ Quick Link +
+ + +
+ + Region Wise College + + West North South East Central - -
- - - -
- - -
- Follow Us - - - - -

- © 2025 IIIT INSIDER. All Rights Reserved. -

-
-
- ); -}; - -export default QuickLink; + +
+ + + +
+ + +
+ Follow Us + + + + +

+ © 2025 IIIT INSIDER. All Rights Reserved. +

+
+
+ ); +}; + +export default QuickLink; diff --git a/src/Components/SocialShare.jsx b/src/Components/SocialShare.jsx new file mode 100644 index 0000000..ff67543 --- /dev/null +++ b/src/Components/SocialShare.jsx @@ -0,0 +1,70 @@ +import React, { useState, useEffect } from 'react'; +import api from '../services/api'; + +const SocialShare = ({ url, title }) => { + const [shareLinks, setShareLinks] = useState({}); + const [showShareMenu, setShowShareMenu] = useState(false); + + useEffect(() => { + if (url) { + api.social.getShareLinks(url, title || document.title).then(setShareLinks); + } + }, [url, title]); + + const openShareWindow = (shareUrl) => { + if (shareUrl) { + window.open(shareUrl, '_blank', 'width=600,height=400'); + } + }; + + const socialPlatforms = [ + { name: 'Facebook', icon: 'M18 2h-3a5 5 0 0 0-5 5v3H7v4h3v8h4v-8h3l1-4h-4V7a1 1 0 0 1 1-1h3z', color: '#1877F2' }, + { name: 'Twitter', icon: 'M23 3a10.9 10.9 0 0 1-3.14 1.53 4.48 4.48 0 0 0-7.86 3v1A10.66 10.66 0 0 1 3 4s-4 9 5 13a11.64 11.64 0 0 1-7 2c9 5 20 0 20-11.5a4.5 4.5 0 0 0-.08-.83A7.72 7.72 0 0 0 23 3z', color: '#1DA1F2' }, + { name: 'LinkedIn', icon: 'M16 8a6 6 0 0 1 6 6v7h-4v-7a2 2 0 0 0-2-2 2 2 0 0 0-2 2v7h-4v-7a6 6 0 0 1 6-6z M2 9h4v12H2z M4 2a2 2 0 1 1-2 2 2 2 0 0 1 2-2z', color: '#0A66C2' }, + { name: 'WhatsApp', icon: 'M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z', color: '#25D366' }, + { name: 'Reddit', icon: 'M12 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0zm5.01 4.744c.688 0 1.25.561 1.25 1.249a1.25 1.25 0 0 1-2.496.056 2.51 2.51 0 0 0-2.383-1.703c-.483 0-.936.14-1.32.372a4.942 4.942 0 0 0-2.655-.784c-2.225 0-4.104 1.48-4.728 3.513a3.47 3.47 0 0 0-1.385 2.793c0 .562.14 1.09.38 1.56a3.47 3.47 0 0 0 2.478 4.39c.395.14.816.21 1.249.21.84 0 1.633-.196 2.344-.547.562.312 1.207.492 1.895.492.562 0 1.09-.14 1.56-.38.312.562.492 1.207.492 1.895 0 .84-.196 1.633-.547 2.344.312.562.492 1.207.492 1.895 0 2.207-1.79 3.997-3.997 3.997-.562 0-1.09-.14-1.56-.38-.395.14-.816.21-1.249.21-2.484 0-4.504-2.02-4.504-4.504 0-.433.07-.854.21-1.249a3.47 3.47 0 0 1-2.793-1.385 3.47 3.47 0 0 1-.56-2.478 3.47 3.47 0 0 1 1.385-2.793c.469-.24.997-.38 1.56-.38.84 0 1.633.196 2.344.547.562-.312 1.207-.492 1.895-.492.562 0 1.09.14 1.56.38.312-.562.492-1.207.492-1.895 0-2.207 1.79-3.997 3.997-3.997z', color: '#FF4500' } + ]; + + return ( +
+ + + {showShareMenu && ( + <> +
setShowShareMenu(false)} + /> +
+

Share on:

+
+ {socialPlatforms.map((platform) => ( + + ))} +
+
+ + )} +
+ ); +}; + +export default SocialShare; diff --git a/src/Pages/About.jsx b/src/Pages/About.jsx new file mode 100644 index 0000000..73001b2 --- /dev/null +++ b/src/Pages/About.jsx @@ -0,0 +1,64 @@ +import React from "react"; + +const About = () => { + return ( +
+
+

+ About IIIT Insider +

+
+

+ Your one-stop platform to explore IIIT colleges, insights, and opportunities. +

+
+ +
+
+

Who We Are

+

+ IIIT Insider is a student-focused platform designed to help aspirants + explore Indian Institutes of Information Technology. We simplify the + research process with structured information about colleges, courses, + placements, and campus life. +

+
+
+ +
+
+

Our Mission

+

+ We aim to empower students with reliable information so they can make + informed decisions about their future. Choosing the right college is + crucial, and we make that journey easier. +

+
+
+ +
+
+

What We Offer

+
+ {[ + ["Search", "Easily find IIITs based on your preferences."], + ["Insights", "Get placement stats and course details."], + ["Profiles", "Explore detailed college information."], + ["Simple UI", "Use a clean and student-friendly interface."], + ].map(([title, description]) => ( +
+

{title}

+

{description}

+
+ ))} +
+
+
+
+ ); +}; + +export default About; diff --git a/src/Pages/College.jsx b/src/Pages/College.jsx index adaa552..6bbe261 100644 --- a/src/Pages/College.jsx +++ b/src/Pages/College.jsx @@ -1,65 +1,146 @@ -import React from "react"; -import { useLocation, Link } from "react-router-dom"; +import React, { useEffect, useMemo, useState } from "react"; +import { useLocation, useSearchParams, Link } from "react-router-dom"; +import Data from "../Components/Data"; +import api from "../services/api"; const College = () => { const location = useLocation(); - const colleges = location.state?.filteredCollege || []; - if (colleges.length === 0) { - return ( -
-

No Colleges Found

-

Please select a region from the Quick Links to view colleges.

- - Return Home - -
+ const [searchParams] = useSearchParams(); + const region = searchParams.get("region") || "all"; + const initialColleges = useMemo(() => { + if (location.state?.filteredCollege) { + return location.state.filteredCollege; + } + + if (region === "all") { + return Data; + } + + return Data.filter((item) => item.region === region); + }, [location.state, region]); + const [colleges, setColleges] = useState(initialColleges); + const [loading, setLoading] = useState(false); + + const normalizeCollegeName = (name) => ( + name || "" + ) + .toLowerCase() + .replace(/[^\w\s]/g, "") + .replace(/\s+/g, " ") + .trim(); + + const mergeCollegeData = (localColleges, backendColleges) => { + const backendByName = new Map( + backendColleges.map((college) => [normalizeCollegeName(college.name), college]) ); - } - return ( -
- + return localColleges.map((college) => { + const backendCollege = backendByName.get(normalizeCollegeName(college.name)); + + if (!backendCollege) { + return college; + } + + return { + ...college, + ...backendCollege, + image: backendCollege.image || college.image, + description: backendCollege.description || college.description, + }; + }); + }; + + useEffect(() => { + let isMounted = true; + + async function loadColleges() { + setLoading(true); + try { + const backendColleges = await api.colleges.getAll(region); + if (isMounted && Array.isArray(backendColleges) && backendColleges.length > 0) { + setColleges( + backendColleges.length >= initialColleges.length + ? backendColleges + : mergeCollegeData(initialColleges, backendColleges) + ); + } else if (isMounted) { + setColleges(initialColleges); + } + } catch (error) { + if (isMounted) { + setColleges(initialColleges); + } + } finally { + if (isMounted) { + setLoading(false); + } + } + } + + loadColleges(); + + return () => { + isMounted = false; + }; + }, [region, initialColleges]); + + if (colleges.length === 0) { + return ( +
+

No Colleges Found

+

Please select a region from the Quick Links to view colleges.

+ + Return Home + +
+ ); + } + + return ( +
+

Explore Colleges

+ {loading &&

Loading latest college data...

}
- -
- {colleges.map((college, index) => ( -
-
- {college.name} -
- {college.region} -
-
- -
-

- {college.name} -

-

- {college.description} -

- -
-
- ))} -
-
- ); -}; - -export default College; \ No newline at end of file + +
+ {colleges.map((college, index) => ( +
+
+ {college.name} +
+ {college.region} +
+
+ +
+

+ {college.name} +

+

+ {college.description} +

+ +
+
+ ))} +
+
+ ); +}; + +export default College; diff --git a/src/Pages/Compare.jsx b/src/Pages/Compare.jsx index dae1bcd..117919d 100644 --- a/src/Pages/Compare.jsx +++ b/src/Pages/Compare.jsx @@ -1,141 +1,156 @@ import React, { useState } from "react"; import ComparingData from "../Components/ComparingData"; +import api from "../services/api"; + +const Compare = () => { + const [college1, setCollege1] = useState(""); + const [college2, setCollege2] = useState(""); + const [result1, setResult1] = useState(null); + const [result2, setResult2] = useState(null); + const [error, setError] = useState(""); + + + const getCompareData = async (collegeName) => { + const key = collegeName.trim().toLowerCase(); + if (!key) return null; + + try { + const backendData = await api.colleges.compare(key); + if (backendData && backendData.name) { + return backendData; + } + } catch (error) { + // Keep the comparison usable before backend data is seeded. + } -const Compare = () => { - const [college1, setCollege1] = useState(""); - const [college2, setCollege2] = useState(""); - const [result1, setResult1] = useState(null); - const [result2, setResult2] = useState(null); - const [error, setError] = useState(""); + return ComparingData[key] || null; + }; - - const handleSearch = () => { + const handleSearch = async () => { setError(""); setResult1(null); setResult2(null); - setTimeout(() => { - const key1 = college1.trim().toLowerCase(); - const key2 = college2.trim().toLowerCase(); - - const data1 = ComparingData[key1]; - const data2 = ComparingData[key2]; + const key1 = college1.trim().toLowerCase(); + const key2 = college2.trim().toLowerCase(); - if (!data1 && key1 !== "") { - setError((prev) => `${prev} '${college1}' not found. `); - } - if (!data2 && key2 !== "") { - setError((prev) => `${prev} '${college2}' not found.`); - } + const data1 = await getCompareData(college1); + const data2 = await getCompareData(college2); - setResult1(data1 || null); - setResult2(data2 || null); - }, 50); - }; + if (!data1 && key1 !== "") { + setError((prev) => `${prev} '${college1}' not found. `); + } + if (!data2 && key2 !== "") { + setError((prev) => `${prev} '${college2}' not found.`); + } - const renderTable = (data, delayClass) => { - if (!data) return null; - return ( -
- - - {Object.entries(data).map(([key, value]) => ( - - - - - ))} - -
- {key} - - {value} -
-
- ); + setResult1(data1 || null); + setResult2(data2 || null); }; - - return ( - <> - - -
- -
- - -
- - setCollege1(e.target.value)} - className="px-4 py-3 rounded-lg border border-gray-700 bg-gray-800 text-white focus:outline-none focus:ring-2 focus:ring-yellow-500/50 focus:border-yellow-500 transition-all duration-300 placeholder-gray-500" - /> -
- - -
- - setCollege2(e.target.value)} - className="px-4 py-3 rounded-lg border border-gray-700 bg-gray-800 text-white focus:outline-none focus:ring-2 focus:ring-yellow-500/50 focus:border-yellow-500 transition-all duration-300 placeholder-gray-500" - /> -
- - - -
- - - {error && ( -
- {error} -
- )} - - -
-
- {renderTable(result1, "delay-100")} -
-
- {renderTable(result2, "delay-200")} -
-
- -
- - ); -}; - -export default Compare; \ No newline at end of file + + const renderTable = (data, delayClass) => { + if (!data) return null; + return ( +
+ + + {Object.entries(data).map(([key, value]) => ( + + + + + ))} + +
+ {key} + + {value} +
+
+ ); + }; + + return ( + <> + + +
+ +
+ + +
+ + setCollege1(e.target.value)} + className="px-4 py-3 rounded-lg border border-gray-700 bg-gray-800 text-white focus:outline-none focus:ring-2 focus:ring-yellow-500/50 focus:border-yellow-500 transition-all duration-300 placeholder-gray-500" + /> +
+ + +
+ + setCollege2(e.target.value)} + className="px-4 py-3 rounded-lg border border-gray-700 bg-gray-800 text-white focus:outline-none focus:ring-2 focus:ring-yellow-500/50 focus:border-yellow-500 transition-all duration-300 placeholder-gray-500" + /> +
+ + + +
+ + + {error && ( +
+ {error} +
+ )} + + +
+
+ {renderTable(result1, "delay-100")} +
+
+ {renderTable(result2, "delay-200")} +
+
+ +
+ + ); +}; + +export default Compare; diff --git a/src/Pages/Contact.jsx b/src/Pages/Contact.jsx new file mode 100644 index 0000000..95e3180 --- /dev/null +++ b/src/Pages/Contact.jsx @@ -0,0 +1,57 @@ +import React from "react"; + +const Contact = () => { + const handleSubmit = (event) => { + event.preventDefault(); + alert("Thanks for contacting us. We will get back to you soon."); + event.currentTarget.reset(); + }; + + return ( +
+
+

+ Contact Us +

+
+

+ Have a question or want to connect? We would love to hear from you. +

+
+ +
+
+
+

Get in Touch

+
+ +
+

Response Time

+

We usually respond within 48 hours.

+
+
+
+ +
+

Send a Message

+
+ + +