diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 19e20387..aee7722e 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -4,14 +4,14 @@ This file provides essential information for coding agents working with the Quer ## Repository Overview -QueryWeaver is an open-source Text2SQL tool that transforms natural language into SQL using graph-powered schema understanding. Built with Python/Flask and FalkorDB (graph database), it provides a web interface for natural language database queries with OAuth authentication. +QueryWeaver is an open-source Text2SQL tool that transforms natural language into SQL using graph-powered schema understanding. Built with Python/FastAPI and FalkorDB (graph database), it provides a web interface for natural language database queries with OAuth authentication. **Key Technologies:** -- **Backend**: Python 3.12+, Flask 3.1+, FalkorDB (Redis-based graph database) +- **Backend**: Python 3.12+, FastAPI 0.115.0+, FalkorDB (Redis-based graph database) - **AI/ML**: LiteLLM with Azure OpenAI/OpenAI integration for text-to-SQL generation - **Testing**: pytest for unit tests, Playwright for E2E testing - **Dependencies**: pipenv for package management -- **Authentication**: Flask-Dance with Google/GitHub OAuth +- **Authentication**: authlib with Google/GitHub OAuth - **Deployment**: Docker support, Vercel configuration **Repository Size**: ~50 Python files, medium complexity web application with comprehensive test suite. @@ -81,11 +81,11 @@ make lint ```bash # Development server with debug mode make run-dev -# OR manually: pipenv run flask --app api.index run --debug +# OR manually: pipenv run uvicorn api.index:app --host "localhost" --port "5000" --reload # Production mode make run-prod -# OR manually: pipenv run flask --app api.index run +# OR manually: pipenv run uvicorn api.index:app --host "localhost" --port "5000" ``` Important: If you're preparing a production deployment or have changed frontend code, run `make build-prod` (or `make build-dev` for a development build) first to produce the static bundle used by the app. @@ -125,7 +125,7 @@ make clean **CRITICAL**: Create `.env` file from `.env.example` and configure these essential variables: ```bash -# REQUIRED for Flask to start +# REQUIRED for FastAPI to start FLASK_SECRET_KEY=your_super_secret_key_here FLASK_DEBUG=False @@ -188,7 +188,7 @@ pipenv run pytest tests/ -k "not e2e" ``` ### 5. Port Conflicts -**Error**: Flask app fails to start on port 5000 +**Error**: FastAPI app fails to start on port 5000 **Solution**: Check if port is in use, kill conflicting processes or change port ## Project Architecture & Layout @@ -196,7 +196,7 @@ pipenv run pytest tests/ -k "not e2e" ### Core Application Structure ``` api/ # Main application package -├── index.py # Flask application entry point +├── index.py # FastAPI application entry point ├── app_factory.py # Application factory pattern ├── config.py # AI model configuration and prompts ├── agents/ # AI agents for query processing @@ -204,7 +204,7 @@ api/ # Main application package │ ├── relevancy_agent.py # Schema relevance detection │ └── follow_up_agent.py # Follow-up question generation ├── auth/ # Authentication modules -├── routes/ # Flask route handlers +├── routes/ # FastAPI route handlers │ ├── auth.py # Authentication routes │ ├── graphs.py # Graph/database routes │ └── database.py # Database management routes @@ -234,7 +234,7 @@ tests/ - `vercel.json`: Deployment configuration ### Key Dependencies -- **Flask ecosystem**: Flask, Flask-Dance (OAuth) +- **FastAPI ecosystem**: FastAPI, Authlib (OAuth) - **Database**: falkordb, psycopg2-binary (PostgreSQL support) - **AI/ML**: litellm (LLM abstraction), boto3 (AWS) - **Development**: pytest, pylint, playwright @@ -290,7 +290,7 @@ Before submitting any changes, run these validation steps: ## Key Files to Understand ### Application Entry Points -- `api/index.py`: Main Flask app entry point +- `api/index.py`: Main FastAPI app entry point - `api/app_factory.py`: Application factory with OAuth setup (lines 1-50 contain core configuration) ### Configuration & Prompts @@ -300,7 +300,7 @@ Before submitting any changes, run these validation steps: ### Core Logic - `api/agents/`: Contains the AI agents that process natural language queries - `api/loaders/`: Database schema loading and graph construction -- `api/routes/`: Flask routes for web interface and API +- `api/routes/`: FastAPI routes for web interface and API ### Testing Infrastructure - `tests/conftest.py`: Pytest fixtures and test configuration diff --git a/.github/dependabot.yml b/.github/dependabot.yml index e0d6de17..2e90d42c 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -7,9 +7,11 @@ version: 2 updates: - package-ecosystem: "pip" directory: "/" + target-branch: "staging" schedule: interval: "weekly" - package-ecosystem: "npm" directory: "/app" + target-branch: "staging" schedule: interval: "weekly" diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 8c8dd0b5..c3354bd6 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -37,14 +37,18 @@ jobs: python -m pip install --upgrade pip pip install pipenv + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + - name: Install dependencies run: | - pipenv sync --dev + make install - - name: Install Playwright browsers + - name: Setup development environment run: | - pipenv run playwright install chromium - pipenv run playwright install-deps + make setup-dev - name: Create test environment file run: | @@ -63,7 +67,7 @@ jobs: - name: Run E2E tests run: | - pipenv run pytest tests/e2e/ --browser chromium --video=on --screenshot=on + make test-e2e env: CI: true diff --git a/Makefile b/Makefile index 19dbcd88..92a8ec80 100644 --- a/Makefile +++ b/Makefile @@ -23,8 +23,8 @@ build-dev: build-prod: npm --prefix ./app run build -test: build-dev ## Run all tests - test-unit test-e2e +test: build-dev test-unit test-e2e ## Run all tests + test-unit: ## Run unit tests only @@ -32,7 +32,7 @@ test-unit: ## Run unit tests only test-e2e: build-dev ## Run E2E tests headless - pipenv run python -m pytest tests/e2e/ --browser chromium + pipenv run python -m pytest tests/e2e/ --browser chromium --video=on --screenshot=on test-e2e-headed: build-dev ## Run E2E tests with browser visible @@ -63,11 +63,10 @@ clean: ## Clean up test artifacts find . -name "*.pyo" -delete run-dev: build-dev ## Run development server - pipenv run python -m flask --app api.index run --debug + pipenv run uvicorn api.index:app --host 127.0.0.1 --port 5000 --reload run-prod: build-prod ## Run production server - npm --prefix ./app run build - pipenv run python -m flask --app api.index run + pipenv run uvicorn api.index:app --host 127.0.0.1 --port 5000 docker-falkordb: ## Start FalkorDB in Docker for testing docker run -d --name falkordb-test -p 6379:6379 falkordb/falkordb:latest diff --git a/Pipfile b/Pipfile index 949a57c4..c53905eb 100644 --- a/Pipfile +++ b/Pipfile @@ -4,16 +4,18 @@ verify_ssl = true name = "pypi" [packages] -litellm = {extras = ["bedrock"], version = "~=1.74.14"} +fastapi = "~=0.116.1" +uvicorn = "~=0.32.0" +litellm = "~=1.74.14" falkordb = "~=1.2.0" -flask = "~=3.1.0" +psycopg2-binary = "~=2.9.9" +pymysql = "~=1.1.0" +authlib = "~=1.4.0" +itsdangerous = "~=2.2.0" jsonschema = "~=4.25.0" tqdm = "~=4.67.1" -boto3 = "~=1.40.11" -psycopg2-binary = "~=2.9.9" -flask-dance = "~=7.1.0" -async-timeout = "~=4.0.3" -mysql-connector-python = "~=9.4.0" +python-multipart = "~=0.0.10" +jinja2 = "~=3.1.4" [dev-packages] pytest = "~=8.4.1" diff --git a/Pipfile.lock b/Pipfile.lock index b1d44130..73be691d 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "112a740337d656543f6cd811ff78b861aca43a1038371f0284797c02c2c1862b" + "sha256": "a601aec67637af6bc6737f23f2c7e8496d620baa4c6b0553d335e33c352be31c" }, "pipfile-spec": 6, "requires": { @@ -140,15 +140,6 @@ "markers": "python_version >= '3.9'", "version": "==4.10.0" }, - "async-timeout": { - "hashes": [ - "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f", - "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028" - ], - "index": "pypi", - "markers": "python_version >= '3.7'", - "version": "==4.0.3" - }, "attrs": { "hashes": [ "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", @@ -157,30 +148,14 @@ "markers": "python_version >= '3.8'", "version": "==25.3.0" }, - "blinker": { - "hashes": [ - "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", - "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc" - ], - "markers": "python_version >= '3.9'", - "version": "==1.9.0" - }, - "boto3": { + "authlib": { "hashes": [ - "sha256:0c03da130467d51c6b940d19be295c56314e14ce0f0464cc86145e98d3c9e983", - "sha256:9d2d211d9cb3efc9a2b2ceec3c510b4e62e389618fd5c871e74d2cbca4561ff5" + "sha256:30ead9ea4993cdbab821dc6e01e818362f92da290c04c7f6a1940f86507a790d", + "sha256:edc29c3f6a3e72cd9e9f45fff67fc663a2c364022eb0371c003f22d5405915c1" ], "index": "pypi", "markers": "python_version >= '3.9'", - "version": "==1.40.11" - }, - "botocore": { - "hashes": [ - "sha256:4beca0c5f92201da1bf1bc0a55038538ad2defded32ab0638cb68f5631dcc665", - "sha256:95af22e1b2230bdd5faa9d1c87e8b147028b14b531770a1148bf495967ccba5e" - ], - "markers": "python_version >= '3.9'", - "version": "==1.40.11" + "version": "==1.4.1" }, "certifi": { "hashes": [ @@ -190,6 +165,79 @@ "markers": "python_version >= '3.7'", "version": "==2025.8.3" }, + "cffi": { + "hashes": [ + "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8", + "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", + "sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1", + "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15", + "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", + "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", + "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8", + "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36", + "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17", + "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf", + "sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc", + "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", + "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", + "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702", + "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", + "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", + "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", + "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6", + "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", + "sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b", + "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e", + "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be", + "sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c", + "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", + "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", + "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", + "sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8", + "sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1", + "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", + "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655", + "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67", + "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595", + "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0", + "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", + "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", + "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", + "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401", + "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", + "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3", + "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16", + "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", + "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e", + "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", + "sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964", + "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c", + "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576", + "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0", + "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3", + "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662", + "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", + "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", + "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", + "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", + "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", + "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", + "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14", + "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", + "sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9", + "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7", + "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382", + "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a", + "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", + "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", + "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", + "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", + "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87", + "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b" + ], + "markers": "python_version >= '3.8'", + "version": "==1.17.1" + }, "charset-normalizer": { "hashes": [ "sha256:00237675befef519d9af72169d8604a067d92755e84fe76492fef5441db05b91", @@ -283,6 +331,49 @@ "markers": "python_version >= '3.10'", "version": "==8.2.1" }, + "cryptography": { + "hashes": [ + "sha256:00e8724bdad672d75e6f069b27970883179bd472cd24a63f6e620ca7e41cc0c5", + "sha256:048e7ad9e08cf4c0ab07ff7f36cc3115924e22e2266e034450a890d9e312dd74", + "sha256:0d9ef57b6768d9fa58e92f4947cea96ade1233c0e236db22ba44748ffedca394", + "sha256:18f878a34b90d688982e43f4b700408b478102dd58b3e39de21b5ebf6509c301", + "sha256:1b7fa6a1c1188c7ee32e47590d16a5a0646270921f8020efc9a511648e1b2e08", + "sha256:20ae4906a13716139d6d762ceb3e0e7e110f7955f3bc3876e3a07f5daadec5f3", + "sha256:20d15aed3ee522faac1a39fbfdfee25d17b1284bafd808e1640a74846d7c4d1b", + "sha256:2384f2ab18d9be88a6e4f8972923405e2dbb8d3e16c6b43f15ca491d7831bd18", + "sha256:275ba5cc0d9e320cd70f8e7b96d9e59903c815ca579ab96c1e37278d231fc402", + "sha256:2dac5ec199038b8e131365e2324c03d20e97fe214af051d20c49db129844e8b3", + "sha256:31a2b9a10530a1cb04ffd6aa1cd4d3be9ed49f7d77a4dafe198f3b382f41545c", + "sha256:3436128a60a5e5490603ab2adbabc8763613f638513ffa7d311c900a8349a2a0", + "sha256:3b5bf5267e98661b9b888a9250d05b063220dfa917a8203744454573c7eb79db", + "sha256:3de77e4df42ac8d4e4d6cdb342d989803ad37707cf8f3fbf7b088c9cbdd46427", + "sha256:44647c5d796f5fc042bbc6d61307d04bf29bccb74d188f18051b635f20a9c75f", + "sha256:550ae02148206beb722cfe4ef0933f9352bab26b087af00e48fdfb9ade35c5b3", + "sha256:599c8d7df950aa68baa7e98f7b73f4f414c9f02d0e8104a30c0182a07732638b", + "sha256:5b64e668fc3528e77efa51ca70fadcd6610e8ab231e3e06ae2bab3b31c2b8ed9", + "sha256:5bd6020c80c5b2b2242d6c48487d7b85700f5e0038e67b29d706f98440d66eb5", + "sha256:5c966c732cf6e4a276ce83b6e4c729edda2df6929083a952cc7da973c539c719", + "sha256:629127cfdcdc6806dfe234734d7cb8ac54edaf572148274fa377a7d3405b0043", + "sha256:705bb7c7ecc3d79a50f236adda12ca331c8e7ecfbea51edd931ce5a7a7c4f012", + "sha256:780c40fb751c7d2b0c6786ceee6b6f871e86e8718a8ff4bc35073ac353c7cd02", + "sha256:7a3085d1b319d35296176af31c90338eeb2ddac8104661df79f80e1d9787b8b2", + "sha256:826b46dae41a1155a0c0e66fafba43d0ede1dc16570b95e40c4d83bfcf0a451d", + "sha256:833dc32dfc1e39b7376a87b9a6a4288a10aae234631268486558920029b086ec", + "sha256:cc4d66f5dc4dc37b89cfef1bd5044387f7a1f6f0abb490815628501909332d5d", + "sha256:d063341378d7ee9c91f9d23b431a3502fc8bfacd54ef0a27baa72a0843b29159", + "sha256:e2a21a8eda2d86bb604934b6b37691585bd095c1f788530c1fcefc53a82b3453", + "sha256:e40b80ecf35ec265c452eea0ba94c9587ca763e739b8e559c128d23bff7ebbbf", + "sha256:e5b3dda1b00fb41da3af4c5ef3f922a200e33ee5ba0f0bc9ecf0b0c173958385", + "sha256:ea3c42f2016a5bbf71825537c2ad753f2870191134933196bee408aac397b3d9", + "sha256:eccddbd986e43014263eda489abbddfbc287af5cddfd690477993dbb31e31016", + "sha256:ee411a1b977f40bd075392c80c10b58025ee5c6b47a822a33c1198598a7a5f05", + "sha256:f4028f29a9f38a2025abedb2e409973709c660d44319c61762202206ed577c42", + "sha256:f68f833a9d445cc49f01097d95c83a850795921b3f7cc6488731e69bde3288da", + "sha256:fc022c1fa5acff6def2fc6d7819bbbd31ccddfe67d075331a65d9cfb28a20983" + ], + "markers": "python_version >= '3.7' and python_full_version not in '3.9.0, 3.9.1'", + "version": "==45.0.6" + }, "distro": { "hashes": [ "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", @@ -300,31 +391,22 @@ "markers": "python_version >= '3.8' and python_version < '4.0'", "version": "==1.2.0" }, - "filelock": { - "hashes": [ - "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2", - "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de" - ], - "markers": "python_version >= '3.9'", - "version": "==3.18.0" - }, - "flask": { + "fastapi": { "hashes": [ - "sha256:07aae2bb5eaf77993ef57e357491839f5fd9f4dc281593a81a9e4d79a24f295c", - "sha256:284c7b8f2f58cb737f0cf1c30fd7eaf0ccfcde196099d24ecede3fc2005aa59e" + "sha256:c46ac7c312df840f0c9e220f7964bada936781bc4e2e6eb71f1c4d7553786565", + "sha256:ed52cbf946abfd70c5a0dccb24673f0670deeb517a88b3544d03c2a6bf283143" ], "index": "pypi", - "markers": "python_version >= '3.9'", - "version": "==3.1.1" + "markers": "python_version >= '3.8'", + "version": "==0.116.1" }, - "flask-dance": { + "filelock": { "hashes": [ - "sha256:6d0510e284f3d6ff05af918849791b17ef93a008628ec33f3a80578a44b51674", - "sha256:81599328a2b3604fd4332b3d41a901cf36980c2067e5e38c44ce3b85c4e1ae9c" + "sha256:66eda1888b0171c998b35be2bcc0f6d75c388a7ce20c3f3f37aa8e96c2dddf58", + "sha256:d38e30481def20772f5baf097c122c3babc4fcdb7e14e57049eb9d88c6dc017d" ], - "index": "pypi", - "markers": "python_version >= '3.6'", - "version": "==7.1.0" + "markers": "python_version >= '3.9'", + "version": "==3.19.1" }, "frozenlist": { "hashes": [ @@ -454,17 +536,17 @@ }, "hf-xet": { "hashes": [ - "sha256:18b61bbae92d56ae731b92087c44efcac216071182c603fc535f8e29ec4b09b8", - "sha256:20cec8db4561338824a3b5f8c19774055b04a8df7fff0cb1ff2cb1a0c1607b80", - "sha256:2e356da7d284479ae0f1dea3cf5a2f74fdf925d6dca84ac4341930d892c7cb34", - "sha256:60dae4b44d520819e54e216a2505685248ec0adbdb2dd4848b17aa85a0375cde", - "sha256:6efaaf1a5a9fc3a501d3e71e88a6bfebc69ee3a716d0e713a931c8b8d920038f", - "sha256:713f2bff61b252f8523739969f247aa354ad8e6d869b8281e174e2ea1bb8d604", - "sha256:751571540f9c1fbad9afcf222a5fb96daf2384bf821317b8bfb0c59d86078513", - "sha256:b109f4c11e01c057fc82004c9e51e6cdfe2cb230637644ade40c599739067b2e" + "sha256:09e86514c3c4284ed8a57d6b0f3d089f9836a0af0a1ceb3c9dd664f1f3eaefef", + "sha256:25b9d43333bbef39aeae1616789ec329c21401a7fe30969d538791076227b591", + "sha256:3d5f82e533fc51c7daad0f9b655d9c7811b5308e5890236828bd1dd3ed8fea74", + "sha256:4171f31d87b13da4af1ed86c98cf763292e4720c088b4957cf9d564f92904ca9", + "sha256:4a9b99ab721d385b83f4fc8ee4e0366b0b59dce03b5888a86029cc0ca634efbf", + "sha256:62a0043e441753bbc446dcb5a3fe40a4d03f5fb9f13589ef1df9ab19252beb53", + "sha256:8e2dba5896bca3ab61d0bef4f01a1647004de59640701b37e37eaa57087bbd9d", + "sha256:bfe5700bc729be3d33d4e9a9b5cc17a951bf8c7ada7ba0c9198a6ab2053b7453" ], "markers": "python_version >= '3.8'", - "version": "==1.1.7" + "version": "==1.1.8" }, "httpcore": { "hashes": [ @@ -511,6 +593,7 @@ "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173" ], + "index": "pypi", "markers": "python_version >= '3.8'", "version": "==2.2.0" }, @@ -519,6 +602,7 @@ "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67" ], + "index": "pypi", "markers": "python_version >= '3.7'", "version": "==3.1.6" }, @@ -605,22 +689,14 @@ "markers": "python_version >= '3.9'", "version": "==0.10.0" }, - "jmespath": { - "hashes": [ - "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980", - "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe" - ], - "markers": "python_version >= '3.7'", - "version": "==1.0.1" - }, "jsonschema": { "hashes": [ - "sha256:24c2e8da302de79c8b9382fee3e76b355e44d2a4364bb207159ce10b517bd716", - "sha256:e63acf5c11762c0e6672ffb61482bdf57f0876684d8d249c0fe2d730d48bc55f" + "sha256:3fba0169e345c7175110351d456342c364814cfcf3b964ba4587f22915230a63", + "sha256:e4a9655ce0da0c0b67a085847e00a3a51449e1157f4f75e9fb5aa545e122eb85" ], "index": "pypi", "markers": "python_version >= '3.9'", - "version": "==4.25.0" + "version": "==4.25.1" }, "jsonschema-specifications": { "hashes": [ @@ -631,12 +707,10 @@ "version": "==2025.4.1" }, "litellm": { - "extras": [ - "bedrock" - ], "hashes": [ "sha256:8eddb1c8a6a5a7048f8ba16e652aba23d6ca996dd87cb853c874ba375aa32479" ], + "index": "pypi", "markers": "python_version not in '2.7, 3.0, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6, 3.7' and python_version >= '3.8'", "version": "==1.74.15.post2" }, @@ -709,169 +783,127 @@ }, "multidict": { "hashes": [ - "sha256:02fd8f32d403a6ff13864b0851f1f523d4c988051eea0471d4f1fd8010f11134", - "sha256:04cbcce84f63b9af41bad04a54d4cc4e60e90c35b9e6ccb130be2d75b71f8c17", - "sha256:056bebbeda16b2e38642d75e9e5310c484b7c24e3841dc0fb943206a72ec89d6", - "sha256:05db2f66c9addb10cfa226e1acb363450fab2ff8a6df73c622fefe2f5af6d4e7", - "sha256:0b9e59946b49dafaf990fd9c17ceafa62976e8471a14952163d10a7a630413a9", - "sha256:0db58da8eafb514db832a1b44f8fa7906fdd102f7d982025f816a93ba45e3dcb", - "sha256:0f1130b896ecb52d2a1e615260f3ea2af55fa7dc3d7c3003ba0c3121a759b18b", - "sha256:10bea2ee839a759ee368b5a6e47787f399b41e70cf0c20d90dfaf4158dfb4e55", - "sha256:12f4581d2930840295c461764b9a65732ec01250b46c6b2c510d7ee68872b140", - "sha256:1328201ee930f069961ae707d59c6627ac92e351ed5b92397cf534d1336ce557", - "sha256:135631cb6c58eac37d7ac0df380294fecdc026b28837fa07c02e459c7fb9c54e", - "sha256:14117a41c8fdb3ee19c743b1c027da0736fdb79584d61a766da53d399b71176c", - "sha256:15332783596f227db50fb261c2c251a58ac3873c457f3a550a95d5c0aa3c770d", - "sha256:159ca68bfd284a8860f8d8112cf0521113bffd9c17568579e4d13d1f1dc76b65", - "sha256:18f4eba0cbac3546b8ae31e0bbc55b02c801ae3cbaf80c247fcdd89b456ff58c", - "sha256:1bf99b4daf908c73856bd87ee0a2499c3c9a3d19bb04b9c6025e66af3fd07462", - "sha256:1c8082e5814b662de8589d6a06c17e77940d5539080cbab9fe6794b5241b76d9", - "sha256:208b9b9757060b9faa6f11ab4bc52846e4f3c2fb8b14d5680c8aac80af3dc751", - "sha256:20c5a0c3c13a15fd5ea86c42311859f970070e4e24de5a550e99d7c271d76318", - "sha256:2334cfb0fa9549d6ce2c21af2bfbcd3ac4ec3646b1b1581c88e3e2b1779ec92b", - "sha256:233ad16999afc2bbd3e534ad8dbe685ef8ee49a37dbc2cdc9514e57b6d589ced", - "sha256:274d416b0df887aef98f19f21578653982cfb8a05b4e187d4a17103322eeaf8f", - "sha256:295adc9c0551e5d5214b45cf29ca23dbc28c2d197a9c30d51aed9e037cb7c578", - "sha256:2e4cc8d848cd4fe1cdee28c13ea79ab0ed37fc2e89dd77bac86a2e7959a8c3bc", - "sha256:346055630a2df2115cd23ae271910b4cae40f4e336773550dca4889b12916e75", - "sha256:35712f1748d409e0707b165bf49f9f17f9e28ae85470c41615778f8d4f7d9609", - "sha256:3713303e4a6663c6d01d648a68f2848701001f3390a030edaaf3fc949c90bf7c", - "sha256:37b09ca60998e87734699e88c2363abfd457ed18cfbf88e4009a4e83788e63ed", - "sha256:3893a0d7d28a7fe6ca7a1f760593bc13038d1d35daf52199d431b61d2660602b", - "sha256:41bb9d1d4c303886e2d85bade86e59885112a7f4277af5ad47ab919a2251f306", - "sha256:42ca5aa9329a63be8dc49040f63817d1ac980e02eeddba763a9ae5b4027b9c9c", - "sha256:43571f785b86afd02b3855c5ac8e86ec921b760298d6f82ff2a61daf5a35330b", - "sha256:448e4a9afccbf297577f2eaa586f07067441e7b63c8362a3540ba5a38dc0f14a", - "sha256:4ef421045f13879e21c994b36e728d8e7d126c91a64b9185810ab51d474f27e7", - "sha256:500b84f51654fdc3944e936f2922114349bf8fdcac77c3092b03449f0e5bc2b3", - "sha256:531e331a2ee53543ab32b16334e2deb26f4e6b9b28e41f8e0c87e99a6c8e2d69", - "sha256:53becb01dd8ebd19d1724bebe369cfa87e4e7f29abbbe5c14c98ce4c383e16cd", - "sha256:540d3c06d48507357a7d57721e5094b4f7093399a0106c211f33540fdc374d55", - "sha256:555ff55a359302b79de97e0468e9ee80637b0de1fce77721639f7cd9440b3a10", - "sha256:5633a82fba8e841bc5c5c06b16e21529573cd654f67fd833650a215520a6210e", - "sha256:5bd8d6f793a787153956cd35e24f60485bf0651c238e207b9a54f7458b16d539", - "sha256:61af8a4b771f1d4d000b3168c12c3120ccf7284502a94aa58c68a81f5afac090", - "sha256:639ecc9fe7cd73f2495f62c213e964843826f44505a3e5d82805aa85cac6f89e", - "sha256:67c92ed673049dec52d7ed39f8cf9ebbadf5032c774058b4406d18c8f8fe7063", - "sha256:68e9e12ed00e2089725669bdc88602b0b6f8d23c0c95e52b95f0bc69f7fe9b55", - "sha256:6c1e61bb4f80895c081790b6b09fa49e13566df8fbff817da3f85b3a8192e36b", - "sha256:70b72e749a4f6e7ed8fb334fa8d8496384840319512746a5f42fa0aec79f4d61", - "sha256:70d974eaaa37211390cd02ef93b7e938de564bbffa866f0b08d07e5e65da783d", - "sha256:712b348f7f449948e0a6c4564a21c7db965af900973a67db432d724619b3c680", - "sha256:72d8815f2cd3cf3df0f83cac3f3ef801d908b2d90409ae28102e0553af85545a", - "sha256:7394888236621f61dcdd25189b2768ae5cc280f041029a5bcf1122ac63df79f9", - "sha256:73ab034fb8d58ff85c2bcbadc470efc3fafeea8affcf8722855fb94557f14cc5", - "sha256:766a4a5996f54361d8d5a9050140aa5362fe48ce51c755a50c0bc3706460c430", - "sha256:769841d70ca8bdd140a715746199fc6473414bd02efd678d75681d2d6a8986c5", - "sha256:775b464d31dac90f23192af9c291dc9f423101857e33e9ebf0020a10bfcf4144", - "sha256:798a9eb12dab0a6c2e29c1de6f3468af5cb2da6053a20dfa3344907eed0937cc", - "sha256:7af039820cfd00effec86bda5d8debef711a3e86a1d3772e85bea0f243a4bd65", - "sha256:7c6df517cf177da5d47ab15407143a89cd1a23f8b335f3a28d57e8b0a3dbb884", - "sha256:81ef2f64593aba09c5212a3d0f8c906a0d38d710a011f2f42759704d4557d3f2", - "sha256:877443eaaabcd0b74ff32ebeed6f6176c71850feb7d6a1d2db65945256ea535c", - "sha256:8db10f29c7541fc5da4defd8cd697e1ca429db743fa716325f236079b96f775a", - "sha256:8df25594989aebff8a130f7899fa03cbfcc5d2b5f4a461cf2518236fe6f15961", - "sha256:900eb9f9da25ada070f8ee4a23f884e0ee66fe4e1a38c3af644256a508ad81ca", - "sha256:934796c81ea996e61914ba58064920d6cad5d99140ac3167901eb932150e2e56", - "sha256:94c47ea3ade005b5976789baaed66d4de4480d0a0bf31cef6edaa41c1e7b56a6", - "sha256:9c19cea2a690f04247d43f366d03e4eb110a0dc4cd1bbeee4d445435428ed35b", - "sha256:9e236a7094b9c4c1b7585f6b9cca34b9d833cf079f7e4c49e6a4a6ec9bfdc68f", - "sha256:9e864486ef4ab07db5e9cb997bad2b681514158d6954dd1958dfb163b83d53e6", - "sha256:9ed948328aec2072bc00f05d961ceadfd3e9bfc2966c1319aeaf7b7c21219183", - "sha256:9f5b28c074c76afc3e4c610c488e3493976fe0e596dd3db6c8ddfbb0134dcac5", - "sha256:9f97e181f344a0ef3881b573d31de8542cc0dbc559ec68c8f8b5ce2c2e91646d", - "sha256:a2be5b7b35271f7fff1397204ba6708365e3d773579fe2a30625e16c4b4ce817", - "sha256:ab0a34a007704c625e25a9116c6770b4d3617a071c8a7c30cd338dfbadfe6485", - "sha256:acf6b97bd0884891af6a8b43d0f586ab2fcf8e717cbd47ab4bdddc09e20652d8", - "sha256:b1db4d2093d6b235de76932febf9d50766cf49a5692277b2c28a501c9637f616", - "sha256:b24576f208793ebae00280c59927c3b7c2a3b1655e443a25f753c4611bc1c373", - "sha256:b8fee016722550a2276ca2cb5bb624480e0ed2bd49125b2b73b7010b9090e888", - "sha256:b9cbc60010de3562545fa198bfc6d3825df430ea96d2cc509c39bd71e2e7d648", - "sha256:b9fe5a0e57c6dbd0e2ce81ca66272282c32cd11d31658ee9553849d91289e1c1", - "sha256:bb933c891cd4da6bdcc9733d048e994e22e1883287ff7540c2a0f3b117605092", - "sha256:bc7f6fbc61b1c16050a389c630da0b32fc6d4a3d191394ab78972bf5edc568c2", - "sha256:bd0578596e3a835ef451784053cfd327d607fc39ea1a14812139339a18a0dbc3", - "sha256:bf9bd1fd5eec01494e0f2e8e446a74a85d5e49afb63d75a9934e4a5423dba21d", - "sha256:c60b401f192e79caec61f166da9c924e9f8bc65548d4246842df91651e83d600", - "sha256:c8161b5a7778d3137ea2ee7ae8a08cce0010de3b00ac671c5ebddeaa17cefd22", - "sha256:cdf22e4db76d323bcdc733514bf732e9fb349707c98d341d40ebcc6e9318ef3d", - "sha256:ce8b7693da41a3c4fde5871c738a81490cea5496c671d74374c8ab889e1834fb", - "sha256:d04d01f0a913202205a598246cf77826fe3baa5a63e9f6ccf1ab0601cf56eca0", - "sha256:d25594d3b38a2e6cabfdcafef339f754ca6e81fbbdb6650ad773ea9775af35ab", - "sha256:d4e47d8faffaae822fb5cba20937c048d4f734f43572e7079298a6c39fb172cb", - "sha256:dbc7cf464cc6d67e83e136c9f55726da3a30176f020a36ead246eceed87f1cd8", - "sha256:dd7793bab517e706c9ed9d7310b06c8672fd0aeee5781bfad612f56b8e0f7d14", - "sha256:e098c17856a8c9ade81b4810888c5ad1914099657226283cab3062c0540b0643", - "sha256:e0cb0ab69915c55627c933f0b555a943d98ba71b4d1c57bc0d0a66e2567c7471", - "sha256:e252017a817fad7ce05cafbe5711ed40faeb580e63b16755a3a24e66fa1d87c0", - "sha256:e2db616467070d0533832d204c54eea6836a5e628f2cb1e6dfd8cd6ba7277cb7", - "sha256:e4e15d2138ee2694e038e33b7c3da70e6b0ad8868b9f8094a72e1414aeda9c1a", - "sha256:e5511cb35f5c50a2db21047c875eb42f308c5583edf96bd8ebf7d770a9d68f6d", - "sha256:e5e8523bb12d7623cd8300dbd91b9e439a46a028cd078ca695eb66ba31adee3c", - "sha256:e5f481cccb3c5c5e5de5d00b5141dc589c1047e60d07e85bbd7dea3d4580d63f", - "sha256:e924fb978615a5e33ff644cc42e6aa241effcf4f3322c09d4f8cebde95aff5f8", - "sha256:e93089c1570a4ad54c3714a12c2cef549dc9d58e97bcded193d928649cab78e9", - "sha256:e995a34c3d44ab511bfc11aa26869b9d66c2d8c799fa0e74b28a473a692532d6", - "sha256:ef43b5dd842382329e4797c46f10748d8c2b6e0614f46b4afe4aee9ac33159df", - "sha256:ef58340cc896219e4e653dade08fea5c55c6df41bcc68122e3be3e9d873d9a7b", - "sha256:f114d8478733ca7388e7c7e0ab34b72547476b97009d643644ac33d4d3fe1821", - "sha256:f3aa090106b1543f3f87b2041eef3c156c8da2aed90c63a2fbed62d875c49c37", - "sha256:f3fc723ab8a5c5ed6c50418e9bfcd8e6dceba6c271cee6728a10a4ed8561520c", - "sha256:f54cb79d26d0cd420637d184af38f0668558f3c4bbe22ab7ad830e67249f2e0b", - "sha256:fc9dc435ec8699e7b602b94fe0cd4703e69273a01cbc34409af29e7820f777f1" + "sha256:01368e3c94032ba6ca0b78e7ccb099643466cf24f8dc8eefcfdc0571d56e58f9", + "sha256:01d0959807a451fe9fdd4da3e139cb5b77f7328baf2140feeaf233e1d777b729", + "sha256:024ce601f92d780ca1617ad4be5ac15b501cc2414970ffa2bb2bbc2bd5a68fa5", + "sha256:047d9425860a8c9544fed1b9584f0c8bcd31bcde9568b047c5e567a1025ecd6e", + "sha256:0a2088c126b6f72db6c9212ad827d0ba088c01d951cee25e758c450da732c138", + "sha256:0af5f9dee472371e36d6ae38bde009bd8ce65ac7335f55dcc240379d7bed1495", + "sha256:0b2e886624be5773e69cf32bcb8534aecdeb38943520b240fed3d5596a430f2f", + "sha256:0c5cbac6b55ad69cb6aa17ee9343dfbba903118fd530348c330211dc7aa756d1", + "sha256:0e0558693063c75f3d952abf645c78f3c5dfdd825a41d8c4d8156fc0b0da6e7e", + "sha256:0f37bed7319b848097085d7d48116f545985db988e2256b2e6f00563a3416ee6", + "sha256:0ffb87be160942d56d7b87b0fdf098e81ed565add09eaa1294268c7f3caac4c8", + "sha256:105245cc6b76f51e408451a844a54e6823bbd5a490ebfe5bdfc79798511ceded", + "sha256:10a68a9191f284fe9d501fef4efe93226e74df92ce7a24e301371293bd4918ae", + "sha256:14616a30fe6d0a48d0a48d1a633ab3b8bec4cf293aac65f32ed116f620adfd69", + "sha256:14754eb72feaa1e8ae528468f24250dd997b8e2188c3d2f593f9eba259e4b364", + "sha256:163c7ea522ea9365a8a57832dea7618e6cbdc3cd75f8c627663587459a4e328f", + "sha256:17d2cbbfa6ff20821396b25890f155f40c986f9cfbce5667759696d83504954f", + "sha256:190766dac95aab54cae5b152a56520fd99298f32a1266d66d27fdd1b5ac00f4e", + "sha256:1a0ccbfe93ca114c5d65a2471d52d8829e56d467c97b0e341cf5ee45410033b3", + "sha256:21f216669109e02ef3e2415ede07f4f8987f00de8cdfa0cc0b3440d42534f9f0", + "sha256:22e38b2bc176c5eb9c0a0e379f9d188ae4cd8b28c0f53b52bce7ab0a9e534657", + "sha256:27d8f8e125c07cb954e54d75d04905a9bba8a439c1d84aca94949d4d03d8601c", + "sha256:2a4c6875c37aae9794308ec43e3530e4aa0d36579ce38d89979bbf89582002bb", + "sha256:34d8f2a5ffdceab9dcd97c7a016deb2308531d5f0fced2bb0c9e1df45b3363d7", + "sha256:350f6b0fe1ced61e778037fdc7613f4051c8baf64b1ee19371b42a3acdb016a0", + "sha256:37b7187197da6af3ee0b044dbc9625afd0c885f2800815b228a0e70f9a7f473d", + "sha256:38a0956dd92d918ad5feff3db8fcb4a5eb7dba114da917e1a88475619781b57b", + "sha256:3ba5aaf600edaf2a868a391779f7a85d93bed147854925f34edd24cc70a3e141", + "sha256:3bb0eae408fa1996d87247ca0d6a57b7fc1dcf83e8a5c47ab82c558c250d4adf", + "sha256:3f8e2384cb83ebd23fd07e9eada8ba64afc4c759cd94817433ab8c81ee4b403f", + "sha256:40cd05eaeb39e2bc8939451f033e57feaa2ac99e07dbca8afe2be450a4a3b6cf", + "sha256:43868297a5759a845fa3a483fb4392973a95fb1de891605a3728130c52b8f40f", + "sha256:452ff5da78d4720d7516a3a2abd804957532dd69296cb77319c193e3ffb87e24", + "sha256:467fe64138cfac771f0e949b938c2e1ada2b5af22f39692aa9258715e9ea613a", + "sha256:49517449b58d043023720aa58e62b2f74ce9b28f740a0b5d33971149553d72aa", + "sha256:497a2954adc25c08daff36f795077f63ad33e13f19bfff7736e72c785391534f", + "sha256:4a1fb393a2c9d202cb766c76208bd7945bc194eba8ac920ce98c6e458f0b524b", + "sha256:4bb7627fd7a968f41905a4d6343b0d63244a0623f006e9ed989fa2b78f4438a0", + "sha256:4d09384e75788861e046330308e7af54dd306aaf20eb760eb1d0de26b2bea2cb", + "sha256:4fefd4a815e362d4f011919d97d7b4a1e566f1dde83dc4ad8cfb5b41de1df68d", + "sha256:52e3c8d43cdfff587ceedce9deb25e6ae77daba560b626e97a56ddcad3756879", + "sha256:55624b3f321d84c403cb7d8e6e982f41ae233d85f85db54ba6286f7295dc8a9c", + "sha256:56c6b3652f945c9bc3ac6c8178cd93132b8d82dd581fcbc3a00676c51302bc1a", + "sha256:580b643b7fd2c295d83cad90d78419081f53fd532d1f1eb67ceb7060f61cff0d", + "sha256:59e8d40ab1f5a8597abcef00d04845155a5693b5da00d2c93dbe88f2050f2812", + "sha256:5df8afd26f162da59e218ac0eefaa01b01b2e6cd606cffa46608f699539246da", + "sha256:630f70c32b8066ddfd920350bc236225814ad94dfa493fe1910ee17fe4365cbb", + "sha256:66247d72ed62d5dd29752ffc1d3b88f135c6a8de8b5f63b7c14e973ef5bda19e", + "sha256:6865f6d3b7900ae020b495d599fcf3765653bc927951c1abb959017f81ae8287", + "sha256:6bf2f10f70acc7a2446965ffbc726e5fc0b272c97a90b485857e5c70022213eb", + "sha256:6c84378acd4f37d1b507dfa0d459b449e2321b3ba5f2338f9b085cf7a7ba95eb", + "sha256:6d46a180acdf6e87cc41dc15d8f5c2986e1e8739dc25dbb7dac826731ef381a4", + "sha256:756989334015e3335d087a27331659820d53ba432befdef6a718398b0a8493ad", + "sha256:75aa52fba2d96bf972e85451b99d8e19cc37ce26fd016f6d4aa60da9ab2b005f", + "sha256:7dd57515bebffd8ebd714d101d4c434063322e4fe24042e90ced41f18b6d3395", + "sha256:7f683a551e92bdb7fac545b9c6f9fa2aebdeefa61d607510b3533286fcab67f5", + "sha256:87a32d20759dc52a9e850fe1061b6e41ab28e2998d44168a8a341b99ded1dba0", + "sha256:8c2fcb12136530ed19572bbba61b407f655e3953ba669b96a35036a11a485793", + "sha256:8c91cdb30809a96d9ecf442ec9bc45e8cfaa0f7f8bdf534e082c2443a196727e", + "sha256:8c9854df0eaa610a23494c32a6f44a3a550fb398b6b51a56e8c6b9b3689578db", + "sha256:8e42332cf8276bb7645d310cdecca93a16920256a5b01bebf747365f86a1675b", + "sha256:8fe323540c255db0bffee79ad7f048c909f2ab0edb87a597e1c17da6a54e493c", + "sha256:967af5f238ebc2eb1da4e77af5492219fbd9b4b812347da39a7b5f5c72c0fa45", + "sha256:9a950b7cf54099c1209f455ac5970b1ea81410f2af60ed9eb3c3f14f0bfcf987", + "sha256:a1b20a9d56b2d81e2ff52ecc0670d583eaabaa55f402e8d16dd062373dbbe796", + "sha256:a506a77ddee1efcca81ecbeae27ade3e09cdf21a8ae854d766c2bb4f14053f92", + "sha256:a59c63061f1a07b861c004e53869eb1211ffd1a4acbca330e3322efa6dd02978", + "sha256:a650629970fa21ac1fb06ba25dabfc5b8a2054fcbf6ae97c758aa956b8dba802", + "sha256:a693fc5ed9bdd1c9e898013e0da4dcc640de7963a371c0bd458e50e046bf6438", + "sha256:aaea28ba20a9026dfa77f4b80369e51cb767c61e33a2d4043399c67bd95fb7c6", + "sha256:ad8850921d3a8d8ff6fbef790e773cecfc260bbfa0566998980d3fa8f520bc4a", + "sha256:ad887a8250eb47d3ab083d2f98db7f48098d13d42eb7a3b67d8a5c795f224ace", + "sha256:ae9408439537c5afdca05edd128a63f56a62680f4b3c234301055d7a2000220f", + "sha256:af7618b591bae552b40dbb6f93f5518328a949dac626ee75927bba1ecdeea9f4", + "sha256:b6819f83aef06f560cb15482d619d0e623ce9bf155115150a85ab11b8342a665", + "sha256:b8aa6f0bd8125ddd04a6593437bad6a7e70f300ff4180a531654aa2ab3f6d58f", + "sha256:b8eb3025f17b0a4c3cd08cda49acf312a19ad6e8a4edd9dbd591e6506d999402", + "sha256:b95494daf857602eccf4c18ca33337dd2be705bccdb6dddbfc9d513e6addb9d9", + "sha256:b9e5853bbd7264baca42ffc53391b490d65fe62849bf2c690fa3f6273dbcd0cb", + "sha256:bbc14f0365534d35a06970d6a83478b249752e922d662dc24d489af1aa0d1be7", + "sha256:be5bf4b3224948032a845d12ab0f69f208293742df96dc14c4ff9b09e508fc17", + "sha256:c5c97aa666cf70e667dfa5af945424ba1329af5dd988a437efeb3a09430389fb", + "sha256:c7a0e9b561e6460484318a7612e725df1145d46b0ef57c6b9866441bf6e27e0c", + "sha256:caebafea30ed049c57c673d0b36238b1748683be2593965614d7b0e99125c877", + "sha256:cbbc54e58b34c3bae389ef00046be0961f30fef7cb0dd9c7756aee376a4f7683", + "sha256:cc356250cffd6e78416cf5b40dc6a74f1edf3be8e834cf8862d9ed5265cf9b0e", + "sha256:ce9a40fbe52e57e7edf20113a4eaddfacac0561a0879734e636aa6d4bb5e3fb0", + "sha256:d191de6cbab2aff5de6c5723101705fd044b3e4c7cfd587a1929b5028b9714b3", + "sha256:d24f351e4d759f5054b641c81e8291e5d122af0fca5c72454ff77f7cbe492de8", + "sha256:d2d4e4787672911b48350df02ed3fa3fffdc2f2e8ca06dd6afdf34189b76a9dd", + "sha256:d8c112f7a90d8ca5d20213aa41eac690bb50a76da153e3afb3886418e61cb22e", + "sha256:d9890d68c45d1aeac5178ded1d1cccf3bc8d7accf1f976f79bf63099fb16e4bd", + "sha256:dadf95aa862714ea468a49ad1e09fe00fcc9ec67d122f6596a8d40caf6cec7d0", + "sha256:db6a3810eec08280a172a6cd541ff4a5f6a97b161d93ec94e6c4018917deb6b7", + "sha256:db9801fe021f59a5b375ab778973127ca0ac52429a26e2fd86aa9508f4d26eb7", + "sha256:e167bf899c3d724f9662ef00b4f7fef87a19c22b2fead198a6f68b263618df52", + "sha256:e1b93790ed0bc26feb72e2f08299691ceb6da5e9e14a0d13cc74f1869af327a0", + "sha256:e5b1413361cef15340ab9dc61523e653d25723e82d488ef7d60a12878227ed50", + "sha256:ecab51ad2462197a4c000b6d5701fc8585b80eecb90583635d7e327b7b6923eb", + "sha256:ed3b94c5e362a8a84d69642dbeac615452e8af9b8eb825b7bc9f31a53a1051e2", + "sha256:ed8358ae7d94ffb7c397cecb62cbac9578a83ecefc1eba27b9090ee910e2efb6", + "sha256:edfdcae97cdc5d1a89477c436b61f472c4d40971774ac4729c613b4b133163cb", + "sha256:ee25f82f53262f9ac93bd7e58e47ea1bdcc3393cef815847e397cba17e284210", + "sha256:f3be27440f7644ab9a13a6fc86f09cdd90b347c3c5e30c6d6d860de822d7cb53", + "sha256:f46a6e8597f9bd71b31cc708195d42b634c8527fecbcf93febf1052cacc1f16e", + "sha256:f6eb37d511bfae9e13e82cb4d1af36b91150466f24d9b2b8a9785816deb16605", + "sha256:f8d4916a81697faec6cb724a273bd5457e4c6c43d82b29f9dc02c5542fd21fc9", + "sha256:f93b2b2279883d1d0a9e1bd01f312d6fc315c5e4c1f09e112e4736e2f650bc4e", + "sha256:f9867e55590e0855bcec60d4f9a092b69476db64573c9fe17e92b0c50614c16a", + "sha256:f996b87b420995a9174b2a7c1a8daf7db4750be6848b03eb5e639674f7963773" ], "markers": "python_version >= '3.9'", - "version": "==6.6.3" - }, - "mysql-connector-python": { - "hashes": [ - "sha256:0f5ad70355720e64b72d7c068e858c9fd1f69b671d9575f857f235a10f878939", - "sha256:1c6b95404e80d003cd452e38674e91528e2b3a089fe505c882f813b564e64f9d", - "sha256:20f8154ab5c0ed444f8ef8e5fa91e65215037db102c137b5f995ebfffd309b78", - "sha256:227dd420c71e6d4788d52d98f298e563f16b6853577e5ade4bd82d644257c812", - "sha256:25f77ad7d845df3b5a5a3a6a8d1fed68248dc418a6938a371d1ddaaab6b9a8e3", - "sha256:3892f20472e13e63b1fb4983f454771dd29f211b09724e69a9750e299542f2f8", - "sha256:3c2603e00516cf4208c6266e85c5c87d5f4d0ac79768106d50de42ccc8414c05", - "sha256:47884fcb050112b8bef3458e17eac47cc81a6cbbf3524e3456146c949772d9b4", - "sha256:4ee4fe1b067e243aae21981e4b9f9d300a3104814b8274033ca8fc7a89b1729e", - "sha256:4efa3898a24aba6a4bfdbf7c1f5023c78acca3150d72cc91199cca2ccd22f76f", - "sha256:5163381a312d38122eded2197eb5cd7ccf1a5c5881d4e7a6de10d6ea314d088e", - "sha256:56e679169c704dab279b176fab2a9ee32d2c632a866c0f7cd48a8a1e2cf802c4", - "sha256:57b0c224676946b70548c56798d5023f65afa1ba5b8ac9f04a143d27976c7029", - "sha256:665c13e7402235162e5b7a2bfdee5895192121b64ea455c90a81edac6a48ede5", - "sha256:7106670abce510e440d393e27fc3602b8cf21e7a8a80216cc9ad9a68cd2e4595", - "sha256:7b8976d89d67c8b0dc452471cb557d9998ed30601fb69a876bf1f0ecaa7954a4", - "sha256:7df1a8ddd182dd8adc914f6dc902a986787bf9599705c29aca7b2ce84e79d361", - "sha256:815aa6cad0f351c1223ef345781a538f2e5e44ef405fdb3851eb322bd9c4ca2b", - "sha256:a8f820c111335f225d63367307456eb7e10494f87e7a94acded3bb762e55a6d4", - "sha256:b27fcd403436fe83bafb2fe7fcb785891e821e639275c4ad3b3bd1e25f533206", - "sha256:b3436a2c8c0ec7052932213e8d01882e6eb069dbab33402e685409084b133a1c", - "sha256:c727cb1f82b40c9aaa7a15ab5cf0a7f87c5d8dce32eab5ff2530a4aa6054e7df", - "sha256:d111360332ae78933daf3d48ff497b70739aa292ab0017791a33e826234e743b", - "sha256:d3e87142103d71c4df647ece30f98e85e826652272ed1c74822b56f6acdc38e7", - "sha256:f14b6936cd326e212fc9ab5f666dea3efea654f0cb644460334e60e22986e735", - "sha256:fd6ff5afb9c324b0bbeae958c93156cce4168c743bf130faf224d52818d1f0ee", - "sha256:fde3bbffb5270a4b02077029914e6a9d2ec08f67d8375b4111432a2778e7540b" - ], - "index": "pypi", - "markers": "python_version >= '3.9'", - "version": "==9.4.0" - }, - "oauthlib": { - "hashes": [ - "sha256:0f0f8aa759826a193cf66c12ea1af1637f87b9b4622d46e866952bb022e538c9", - "sha256:88119c938d2b8fb88561af5f6ee0eec8cc8d552b7bb1f712743136eb7523b7a1" - ], - "markers": "python_version >= '3.8'", - "version": "==3.3.1" + "version": "==6.6.4" }, "openai": { "hashes": [ - "sha256:e40d44b2989588c45ce13819598788b77b8fb80ba2f7ae95ce90d14e46f1bd26", - "sha256:f48f4239b938ef187062f3d5199a05b69711d8b600b9a9b6a3853cd271799183" + "sha256:54d3457b2c8d7303a1bc002a058de46bdd8f37a8117751c7cf4ed4438051f151", + "sha256:787b4c3c8a65895182c58c424f790c25c790cc9a0330e34f73d55b6ee5a00e32" ], "markers": "python_version >= '3.8'", - "version": "==1.99.6" + "version": "==1.100.2" }, "packaging": { "hashes": [ @@ -1060,6 +1092,14 @@ "markers": "python_version >= '3.8'", "version": "==2.9.10" }, + "pycparser": { + "hashes": [ + "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", + "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc" + ], + "markers": "python_version >= '3.8'", + "version": "==2.22" + }, "pydantic": { "hashes": [ "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db", @@ -1181,6 +1221,15 @@ "markers": "python_version >= '3.9'", "version": "==2.10.1" }, + "pymysql": { + "hashes": [ + "sha256:4de15da4c61dc132f4fb9ab763063e693d521a80fd0e87943b9a453dd4c19d6c", + "sha256:e127611aaf2b417403c60bf4dc570124aeb4a57f5f37b8e95ae399a42f904cd0" + ], + "index": "pypi", + "markers": "python_version >= '3.7'", + "version": "==1.1.1" + }, "python-dateutil": { "hashes": [ "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", @@ -1197,6 +1246,15 @@ "markers": "python_version >= '3.9'", "version": "==1.1.1" }, + "python-multipart": { + "hashes": [ + "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", + "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==0.0.20" + }, "pyyaml": { "hashes": [ "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff", @@ -1367,19 +1425,11 @@ }, "requests": { "hashes": [ - "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", - "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422" - ], - "markers": "python_version >= '3.8'", - "version": "==2.32.4" - }, - "requests-oauthlib": { - "hashes": [ - "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36", - "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9" + "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", + "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf" ], - "markers": "python_version >= '3.4'", - "version": "==2.0.0" + "markers": "python_version >= '3.9'", + "version": "==2.32.5" }, "rpds-py": { "hashes": [ @@ -1542,14 +1592,6 @@ "markers": "python_version >= '3.9'", "version": "==0.27.0" }, - "s3transfer": { - "hashes": [ - "sha256:a981aa7429be23fe6dfc13e80e4020057cbab622b08c0315288758d67cabc724", - "sha256:c3fdba22ba1bd367922f27ec8032d6a1cf5f10c934fb5d68cf60fd5a23d936cf" - ], - "markers": "python_version >= '3.9'", - "version": "==0.13.1" - }, "six": { "hashes": [ "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", @@ -1566,6 +1608,14 @@ "markers": "python_version >= '3.7'", "version": "==1.3.1" }, + "starlette": { + "hashes": [ + "sha256:6ae9aa5db235e4846decc1e7b79c4f346adf41e9777aebeb49dfd09bbd7023d8", + "sha256:c5847e96134e5c5371ee9fac6fdf1a67336d5815e09eb2a01fdb57a351ef915b" + ], + "markers": "python_version >= '3.9'", + "version": "==0.47.2" + }, "tiktoken": { "hashes": [ "sha256:10331d08b5ecf7a780b4fe4d0281328b23ab22cdb4ff65e68d56caeda9940ecc", @@ -1657,20 +1707,14 @@ "markers": "python_version >= '3.9'", "version": "==2.5.0" }, - "urlobject": { - "hashes": [ - "sha256:bfdfe70746d92a039a33e964959bb12cecd9807a434fdb7fef5f38e70a295818", - "sha256:fd2465520d0a8c5ed983aa47518a2c5bcde0c276a4fd0eb28b0de5dcefd93b1e" - ], - "version": "==3.0.0" - }, - "werkzeug": { + "uvicorn": { "hashes": [ - "sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e", - "sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746" + "sha256:82ad92fd58da0d12af7482ecdb5f2470a04c9c9a53ced65b9bbb4a205377602e", + "sha256:ee9519c246a72b1c084cea8d3b44ed6026e78a4a309cbedae9c37e4cb9fbb175" ], - "markers": "python_version >= '3.9'", - "version": "==3.1.3" + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==0.32.1" }, "yarl": { "hashes": [ @@ -2105,11 +2149,11 @@ }, "requests": { "hashes": [ - "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", - "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422" + "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", + "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf" ], - "markers": "python_version >= '3.8'", - "version": "==2.32.4" + "markers": "python_version >= '3.9'", + "version": "==2.32.5" }, "text-unidecode": { "hashes": [ diff --git a/api/app_factory.py b/api/app_factory.py index c0e20700..25c3ceee 100644 --- a/api/app_factory.py +++ b/api/app_factory.py @@ -1,102 +1,103 @@ -"""Application factory for the text2sql Flask app.""" +"""Application factory for the text2sql FastAPI app.""" import logging import os import secrets from dotenv import load_dotenv -from flask import Flask, redirect, url_for, request, abort, session -from werkzeug.exceptions import HTTPException -from werkzeug.utils import secure_filename -from flask_dance.contrib.google import make_google_blueprint -from flask_dance.contrib.github import make_github_blueprint -from flask_dance.consumer.storage.session import SessionStorage - -from api.auth.oauth_handlers import setup_oauth_handlers -from api.routes.auth import auth_bp -from api.routes.graphs import graphs_bp -from api.routes.database import database_bp +from fastapi import FastAPI, Request, HTTPException +from fastapi.responses import RedirectResponse, JSONResponse +from fastapi.staticfiles import StaticFiles +from starlette.middleware.sessions import SessionMiddleware +from starlette.middleware.base import BaseHTTPMiddleware + +from api.routes.auth import auth_router, init_auth +from api.routes.graphs import graphs_router +from api.routes.database import database_router # Load environment variables from .env file load_dotenv() logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s") +class SecurityMiddleware(BaseHTTPMiddleware): + """Middleware for security checks including static file access""" + + STATIC_PREFIX = '/static/' + + async def dispatch(self, request: Request, call_next): + # Block directory access in static files + if request.url.path.startswith(self.STATIC_PREFIX): + # Remove /static/ prefix to get the actual path + filename = request.url.path[len(self.STATIC_PREFIX):] + # Basic security check for directory traversal + if not filename or '../' in filename or filename.endswith('/'): + return JSONResponse( + status_code=403, + content={"detail": "Forbidden"} + ) + + response = await call_next(request) + return response + + def create_app(): - """Create and configure the Flask application.""" - app = Flask(__name__, template_folder="../app/templates", static_folder="../app/public") - app.secret_key = os.getenv("FLASK_SECRET_KEY") - if not app.secret_key: - app.secret_key = secrets.token_hex(32) + """Create and configure the FastAPI application.""" + app = FastAPI(title="QueryWeaver", description="Text2SQL with Graph-Powered Schema Understanding") + + # Get secret key for sessions + secret_key = os.getenv("FLASK_SECRET_KEY") + if not secret_key: + secret_key = secrets.token_hex(32) logging.warning("FLASK_SECRET_KEY not set, using generated key. Set this in production!") - # Google OAuth setup - google_client_id = os.getenv("GOOGLE_CLIENT_ID") - google_client_secret = os.getenv("GOOGLE_CLIENT_SECRET") - google_bp = make_google_blueprint( - client_id=google_client_id, - client_secret=google_client_secret, - scope=[ - "https://www.googleapis.com/auth/userinfo.email", - "https://www.googleapis.com/auth/userinfo.profile", - "openid" - ] - ) - app.register_blueprint(google_bp, url_prefix="/login") - - # GitHub OAuth setup - github_client_id = os.getenv("GITHUB_CLIENT_ID") - github_client_secret = os.getenv("GITHUB_CLIENT_SECRET") - github_bp = make_github_blueprint( - client_id=github_client_id, - client_secret=github_client_secret, - scope="user:email", - storage=SessionStorage() + # Add session middleware with explicit settings to ensure OAuth state persists + app.add_middleware( + SessionMiddleware, + secret_key=secret_key, + session_cookie="qw_session", + same_site="lax", # allow top-level OAuth GET redirects to send cookies + https_only=False, # allow http on localhost in development + max_age=60 * 60 * 24 * 14, # 14 days - measured by seconds ) - app.register_blueprint(github_bp, url_prefix="/login") - # Set up OAuth signal handlers - setup_oauth_handlers(google_bp, github_bp) + # Add security middleware + app.add_middleware(SecurityMiddleware) + + # Mount static files + static_path = os.path.join(os.path.dirname(__file__), "../app/public") + if os.path.exists(static_path): + app.mount("/static", StaticFiles(directory=static_path), name="static") - # Register blueprints - app.register_blueprint(auth_bp) - app.register_blueprint(graphs_bp) - app.register_blueprint(database_bp) + # Initialize authentication (OAuth and sessions) + init_auth(app) - @app.errorhandler(Exception) - def handle_oauth_error(error): + # Include routers + app.include_router(auth_router) + app.include_router(graphs_router, prefix="/graphs") + app.include_router(database_router) + + @app.exception_handler(Exception) + async def handle_oauth_error(request: Request, exc: Exception): """Handle OAuth-related errors gracefully""" # Check if it's an OAuth-related error - if "token" in str(error).lower() or "oauth" in str(error).lower(): - logging.warning("OAuth error occurred: %s", error) - session.clear() - return redirect(url_for("auth.home")) + if "token" in str(exc).lower() or "oauth" in str(exc).lower(): + logging.warning("OAuth error occurred: %s", exc) + request.session.clear() + return RedirectResponse(url="/", status_code=302) - # If it's an HTTPException (like abort(403)), re-raise so Flask handles it properly - if isinstance(error, HTTPException): - return error + # If it's an HTTPException, re-raise so FastAPI handles it properly + if isinstance(exc, HTTPException): + raise exc # For other errors, let them bubble up - raise error - - @app.before_request - def block_static_directories(): - if request.path.startswith('/static/'): - # Remove /static/ prefix to get the actual path - filename = secure_filename(request.path[8:]) - # Normalize and ensure the path stays within static_folder - static_folder = os.path.abspath(app.static_folder) - file_path = os.path.normpath(os.path.join(static_folder, filename)) - if not file_path.startswith(static_folder): - abort(400) # Bad request, attempted traversal - if os.path.isdir(file_path): - abort(405) - - @app.context_processor - def inject_google_tag_manager(): - """Inject Google Tag Manager ID into template context.""" - return { - 'google_tag_manager_id': os.getenv("GOOGLE_TAG_MANAGER_ID") - } + raise exc + + # Add template globals + @app.middleware("http") + async def add_template_globals(request: Request, call_next): + request.state.google_tag_manager_id = os.getenv("GOOGLE_TAG_MANAGER_ID") + response = await call_next(request) + return response return app diff --git a/api/auth/oauth_handlers.py b/api/auth/oauth_handlers.py index fbeaf3b4..8b7d894f 100644 --- a/api/auth/oauth_handlers.py +++ b/api/auth/oauth_handlers.py @@ -2,121 +2,65 @@ import logging import time +from typing import Dict, Any import requests -from flask import session -from flask_dance.consumer import oauth_authorized -from flask_dance.contrib.google import google -from flask_dance.contrib.github import github +from fastapi import FastAPI, Request +from authlib.integrations.starlette_client import OAuth from .user_management import ensure_user_in_organizations -def setup_oauth_handlers(google_bp, github_bp): - """Set up OAuth signal handlers for both Google and GitHub blueprints.""" - - @oauth_authorized.connect_via(google_bp) - def google_logged_in(blueprint, token): # pylint: disable=unused-argument - """Handle Google OAuth authorization callback.""" - if not token: - return False - +def setup_oauth_handlers(app: FastAPI, oauth: OAuth): + """Set up OAuth handlers for both Google and GitHub.""" + + # Store oauth in app state for access in routes + app.state.oauth = oauth + + async def handle_google_callback(request: Request, token: Dict[str, Any], user_info: Dict[str, Any]): + """Handle Google OAuth callback processing""" try: - # Get user profile - resp = google.get("/oauth2/v2/userinfo") - if resp.ok: - google_user = resp.json() - user_id = google_user.get("id") - email = google_user.get("email") - name = google_user.get("name") - - # Validate required fields - if not user_id or not email: - logging.error("Missing required fields from Google OAuth response") - return False - - if user_id and email: - # Check if identity exists in Organizations graph, create if new - _, _ = ensure_user_in_organizations( - user_id, email, name, "google", google_user.get("picture") - ) - - # If existing identity, just update last login - # (already done in ensure_user_in_organizations) - - # Normalize user info structure for session - user_info_session = { - "id": str(user_id), # Ensure string type - "name": name or "", - "email": email, - "picture": google_user.get("picture", ""), - "provider": "google" - } - session["user_info"] = user_info_session - session["token_validated_at"] = time.time() - return False # Don't create default flask-dance entry in session - - except (requests.RequestException, KeyError, ValueError, AttributeError) as e: - logging.error("Google OAuth signal error: %s", e) - - return False - - @oauth_authorized.connect_via(github_bp) - def github_logged_in(blueprint, token): # pylint: disable=unused-argument - """Handle GitHub OAuth authorization callback.""" - if not token: + user_id = user_info.get("id") + email = user_info.get("email") + name = user_info.get("name") + + # Validate required fields + if not user_id or not email: + logging.error("Missing required fields from Google OAuth response") + return False + + # Check if identity exists in Organizations graph, create if new + _, _ = ensure_user_in_organizations( + user_id, email, name, "google", user_info.get("picture") + ) + + return True + except Exception as e: + logging.error("Error handling Google OAuth callback: %s", e) return False + async def handle_github_callback(request: Request, token: Dict[str, Any], user_info: Dict[str, Any]): + """Handle GitHub OAuth callback processing""" try: - # Get user profile - resp = github.get("/user") - if resp.ok: - github_user = resp.json() - - # Get user email (GitHub may require separate call for email) - email_resp = github.get("/user/emails") - email = None - if email_resp.ok: - emails = email_resp.json() - # Find primary email - for email_obj in emails: - if email_obj.get("primary", False): - email = email_obj.get("email") - break - # If no primary email found, use the first one - if not email and emails: - email = emails[0].get("email") - - user_id = str(github_user.get("id")) - name = github_user.get("name") or github_user.get("login") - - # Validate required fields - if not user_id or not email: - logging.error("Missing required fields from GitHub OAuth response") - return False - - if user_id and email: - # Check if identity exists in Organizations graph, create if new - _, _ = ensure_user_in_organizations( - user_id, email, name, "github", github_user.get("avatar_url") - ) - - # If existing identity, just update last login - # (already done in ensure_user_in_organizations) - - # Normalize user info structure for session - user_info_session = { - "id": user_id, - "name": name or "", - "email": email, - "picture": github_user.get("avatar_url", ""), - "provider": "github" - } - session["user_info"] = user_info_session - session["token_validated_at"] = time.time() - return False # Don't create default flask-dance entry in session - - except (requests.RequestException, KeyError, ValueError, AttributeError) as e: - logging.error("GitHub OAuth signal error: %s", e) - - return False + user_id = user_info.get("id") + email = user_info.get("email") + name = user_info.get("name") or user_info.get("login") + + # Validate required fields + if not user_id or not email: + logging.error("Missing required fields from GitHub OAuth response") + return False + + # Check if identity exists in Organizations graph, create if new + _, _ = ensure_user_in_organizations( + user_id, email, name, "github", user_info.get("picture") + ) + + return True + except Exception as e: + logging.error("Error handling GitHub OAuth callback: %s", e) + return False + + # Store handlers in app state for use in route callbacks + app.state.google_callback_handler = handle_google_callback + app.state.github_callback_handler = handle_github_callback diff --git a/api/auth/user_management.py b/api/auth/user_management.py index adc2d921..a4aa7980 100644 --- a/api/auth/user_management.py +++ b/api/auth/user_management.py @@ -3,11 +3,12 @@ import logging import time from functools import wraps +from typing import Tuple, Optional, Dict, Any import requests -from flask import g, session, jsonify -from flask_dance.contrib.google import google -from flask_dance.contrib.github import github +from fastapi import Request, HTTPException, status +from fastapi.responses import JSONResponse +from authlib.integrations.starlette_client import OAuth from api.extensions import db @@ -158,128 +159,164 @@ def update_identity_last_login(provider, provider_user_id): provider, provider_user_id, e) -def validate_and_cache_user(): +async def validate_and_cache_user(request: Request) -> Tuple[Optional[Dict[str, Any]], bool]: """ Helper function to validate OAuth token and cache user info. - Returns (user_info, is_authenticated) tuple. + Returns (user_info, is_authenticated). Supports both Google and GitHub OAuth. + Includes refresh handling for Google. """ try: - # Check for cached user info from either provider - user_info = session.get("user_info") - token_validated_at = session.get("token_validated_at", 0) + user_info = request.session.get("user_info") + token_validated_at = request.session.get("token_validated_at", 0) current_time = time.time() # Use cached user info if it's less than 15 minutes old - if user_info and (current_time - token_validated_at) < 900: # 15 minutes + if user_info and (current_time - token_validated_at) < 900: return user_info, True - # Check Google OAuth first - if google.authorized: + oauth: OAuth = request.app.state.oauth + + # ---- Google OAuth ---- + google_token = request.session.get("google_token") + if google_token and hasattr(oauth, "google"): try: - resp = google.get("/oauth2/v2/userinfo") - if resp.ok: + resp = await oauth.google.get("/oauth2/v2/userinfo", token=google_token) + + if resp.status_code == 401 and "refresh_token" in google_token: + # Token expired, try refreshing + try: + new_token = await oauth.google.refresh_token( + "https://oauth2.googleapis.com/token", + refresh_token=google_token["refresh_token"], + ) + request.session["google_token"] = new_token + resp = await oauth.google.get("/oauth2/v2/userinfo", token=new_token) + logging.info("Google access token refreshed successfully") + except Exception as e: + logging.error("Google token refresh failed: %s", e) + request.session.pop("google_token", None) + request.session.pop("user_info", None) + return None, False + + if resp.status_code == 200: google_user = resp.json() - # Validate required fields if not google_user.get("id") or not google_user.get("email"): logging.warning("Invalid Google user data received") - session.clear() + request.session.pop("google_token", None) + request.session.pop("user_info", None) return None, False - # Normalize user info structure + # Normalize user_info = { - "id": str(google_user.get("id")), # Ensure string type + "id": str(google_user.get("id")), "name": google_user.get("name", ""), "email": google_user.get("email"), "picture": google_user.get("picture", ""), - "provider": "google" + "provider": "google", } - session["user_info"] = user_info - session["token_validated_at"] = current_time + request.session["user_info"] = user_info + request.session["token_validated_at"] = current_time return user_info, True - except (requests.RequestException, KeyError, ValueError) as e: + except Exception as e: logging.warning("Google OAuth validation error: %s", e) - session.clear() + request.session.pop("google_token", None) + request.session.pop("user_info", None) - # Check GitHub OAuth - if github.authorized: + # ---- GitHub OAuth ---- + github_token = request.session.get("github_token") + if github_token and hasattr(oauth, "github"): try: - # Get user profile - resp = github.get("/user") - if resp.ok: + resp = await oauth.github.get("/user", token=github_token) + if resp.status_code == 200: github_user = resp.json() - - # Validate required fields if not github_user.get("id"): logging.warning("Invalid GitHub user data received") - session.clear() + request.session.pop("github_token", None) + request.session.pop("user_info", None) return None, False - # Get user email (GitHub may require separate call for email) - email_resp = github.get("/user/emails") + # Get primary email + email_resp = await oauth.github.get("/user/emails", token=github_token) email = None - if email_resp.ok: - emails = email_resp.json() - # Find primary email - for email_obj in emails: + if email_resp.status_code == 200: + for email_obj in email_resp.json(): if email_obj.get("primary", False): email = email_obj.get("email") break - - # If no primary email found, use the first one - if not email and emails: - email = emails[0].get("email") + if not email and email_resp.json(): + email = email_resp.json()[0].get("email") if not email: logging.warning("No email found for GitHub user") - session.clear() + request.session.pop("github_token", None) + request.session.pop("user_info", None) return None, False - # Normalize user info structure user_info = { - "id": str(github_user.get("id")), # Convert to string for consistency + "id": str(github_user.get("id")), "name": github_user.get("name") or github_user.get("login", ""), "email": email, "picture": github_user.get("avatar_url", ""), - "provider": "github" + "provider": "github", } - session["user_info"] = user_info - session["token_validated_at"] = current_time + request.session["user_info"] = user_info + request.session["token_validated_at"] = current_time return user_info, True - except (requests.RequestException, KeyError, ValueError) as e: + except Exception as e: logging.warning("GitHub OAuth validation error: %s", e) - session.clear() + request.session.pop("github_token", None) + request.session.pop("user_info", None) - # If no valid authentication found, clear session - session.clear() + # No valid auth + request.session.pop("user_info", None) return None, False except Exception as e: logging.error("Unexpected error in validate_and_cache_user: %s", e) - session.clear() + request.session.pop("user_info", None) return None, False +def token_required(func): + """Decorator to protect FastAPI routes with token authentication. + Automatically refreshes tokens if expired. + """ -def token_required(f): - """Decorator to protect routes with token authentication""" - - @wraps(f) - def decorated_function(*args, **kwargs): + @wraps(func) + async def wrapper(request: Request, *args, **kwargs): try: - user_info, is_authenticated = validate_and_cache_user() + user_info, is_authenticated = await validate_and_cache_user(request) if not is_authenticated: - return jsonify(message="Unauthorized - Please log in"), 401 + # Second attempt after clearing session to force re-validation + request.session.pop("user_info", None) + user_info, is_authenticated = await validate_and_cache_user(request) - g.user_id = user_info.get("id") - if not g.user_id: - session.clear() - return jsonify(message="Unauthorized - Invalid user"), 401 - - return f(*args, **kwargs) + if not is_authenticated: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Unauthorized - Please log in" + ) + + # Attach user_id to request.state (like Flask's g.user_id) + request.state.user_id = user_info.get("id") + if not request.state.user_id: + request.session.pop("user_info", None) + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Unauthorized - Invalid user" + ) + + return await func(request, *args, **kwargs) + + except HTTPException: + raise except Exception as e: logging.error("Unexpected error in token_required: %s", e) - session.clear() - return jsonify(message="Unauthorized - Authentication error"), 401 + request.session.pop("user_info", None) + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Unauthorized - Authentication error" + ) - return decorated_function + return wrapper diff --git a/api/index.py b/api/index.py index 4d7505ca..8b30c4e8 100644 --- a/api/index.py +++ b/api/index.py @@ -6,8 +6,16 @@ if __name__ == "__main__": import os + import uvicorn + debug_mode = os.environ.get('FLASK_DEBUG', 'False').lower() == 'true' - app.run(debug=debug_mode) -# This allows running the app with `flask run` or directly with `python api/index.py` + uvicorn.run( + "api.index:app", + host="127.0.0.1", + port=5000, + reload=debug_mode, + log_level="info" if debug_mode else "warning" + ) +# This allows running the app with `uvicorn api.index:app` or directly with `python api/index.py` # Ensure the environment variable FLASK_DEBUG is set to 'True' for debug mode # or 'False' for production mode. diff --git a/api/loaders/mysql_loader.py b/api/loaders/mysql_loader.py index 7f185911..6d453860 100644 --- a/api/loaders/mysql_loader.py +++ b/api/loaders/mysql_loader.py @@ -6,8 +6,10 @@ import re from typing import Tuple, Dict, Any, List -import mysql.connector import tqdm +import pymysql +from pymysql.cursors import DictCursor + from api.loaders.base_loader import BaseLoader from api.loaders.graph_loader import load_to_graph @@ -136,8 +138,8 @@ def load(prefix: str, connection_url: str) -> Tuple[bool, str]: conn_params = MySQLLoader._parse_mysql_url(connection_url) # Connect to MySQL database - conn = mysql.connector.connect(**conn_params) - cursor = conn.cursor(dictionary=True) + conn = pymysql.connect(**conn_params) + cursor = conn.cursor(DictCursor) # Get database name db_name = conn_params['database'] @@ -159,7 +161,7 @@ def load(prefix: str, connection_url: str) -> Tuple[bool, str]: return True, (f"MySQL schema loaded successfully. " f"Found {len(entities)} tables.") - except mysql.connector.Error as e: + except pymysql.MySQLError as e: return False, f"MySQL connection error: {str(e)}" except Exception as e: return False, f"Error loading MySQL schema: {str(e)}" @@ -465,7 +467,7 @@ def execute_sql_query(sql_query: str, db_url: str) -> List[Dict[str, Any]]: conn_params = MySQLLoader._parse_mysql_url(db_url) # Connect to MySQL database - conn = mysql.connector.connect(**conn_params) + conn = pymysql.connect(**conn_params) cursor = conn.cursor(dictionary=True) # Execute the SQL query @@ -511,7 +513,13 @@ def execute_sql_query(sql_query: str, db_url: str) -> List[Dict[str, Any]]: return result_list - except mysql.connector.Error as e: + except pymysql.MySQLError as e: + # Rollback in case of error + if 'conn' in locals(): + conn.rollback() + cursor.close() + conn.close() + except pymysql.MySQLError as e: # Rollback in case of error if 'conn' in locals(): conn.rollback() diff --git a/api/routes/__init__.py b/api/routes/__init__.py index 375b53c1..c8d3ba5c 100644 --- a/api/routes/__init__.py +++ b/api/routes/__init__.py @@ -1,7 +1,7 @@ # Routes module for text2sql API -from .auth import auth_bp -from .graphs import graphs_bp -from .database import database_bp +from .auth import auth_router +from .graphs import graphs_router +from .database import database_router -__all__ = ["auth_bp", "graphs_bp", "database_bp"] +__all__ = ["auth_router", "graphs_router", "database_router"] diff --git a/api/routes/auth.py b/api/routes/auth.py index d6f483f5..e3dd420b 100644 --- a/api/routes/auth.py +++ b/api/routes/auth.py @@ -1,101 +1,307 @@ """Authentication routes for the text2sql API.""" import logging +import os import time +from pathlib import Path +from urllib.parse import urljoin -import requests -from flask import Blueprint, render_template, redirect, url_for, session -from flask_dance.contrib.google import google -from flask_dance.contrib.github import github +import httpx +from fastapi import APIRouter, Request, HTTPException, status +from fastapi.responses import RedirectResponse, HTMLResponse +from fastapi.templating import Jinja2Templates +from authlib.common.errors import AuthlibBaseError +from starlette.config import Config from api.auth.user_management import validate_and_cache_user -auth_bp = Blueprint("auth", __name__) +# Router +auth_router = APIRouter() +TEMPLATES_DIR = str((Path(__file__).resolve().parents[1] / "../app/templates").resolve()) +templates = Jinja2Templates(directory=TEMPLATES_DIR) +# ---- Helpers ---- +def _get_provider_client(request: Request, provider: str): + """Get an OAuth provider client from app.state.oauth""" + oauth = getattr(request.app.state, "oauth", None) + if not oauth: + raise HTTPException(status_code=500, detail="OAuth not configured") -@auth_bp.route("/") -def home(): - """Home route""" - user_info, is_authenticated = validate_and_cache_user() + client = getattr(oauth, provider, None) + if not client: + raise HTTPException(status_code=500, detail=f"OAuth provider {provider} not configured") + return client - # If not authenticated through OAuth, check for any stale session data - if not is_authenticated and not google.authorized and not github.authorized: - session.pop("user_info", None) +def _clear_auth_session(session: dict): + """Remove only auth-related keys from session instead of clearing everything.""" + for key in ["user_info", "google_token", "github_token", "token_validated_at", "oauth_google_auth"]: + session.pop(key, None) - # If unauthenticated, show a simple landing page that invites sign-in or continuing as guest - if not is_authenticated: - return render_template("landing.j2", is_authenticated=False, user_info=None) +@auth_router.get("/chat", name="auth.chat", response_class=HTMLResponse) +async def chat(request: Request) -> HTMLResponse: + """Explicit chat route (renders main chat UI).""" + user_info, is_authenticated = await validate_and_cache_user(request) + return templates.TemplateResponse( + "chat.j2", + { + "request": request, + "is_authenticated": is_authenticated, + "user_info": user_info, + }, + ) - return render_template("chat.j2", is_authenticated=is_authenticated, user_info=user_info) +def _build_callback_url(request: Request, path: str) -> str: + """Build absolute callback URL, honoring OAUTH_BASE_URL if provided.""" + base_override = os.getenv("OAUTH_BASE_URL") + base = base_override if base_override else str(request.base_url) + if not base.endswith("/"): + base += "/" + return urljoin(base, path.lstrip("/")) +# ---- Routes ---- +@auth_router.get("/", response_class=HTMLResponse) +async def home(request: Request) -> HTMLResponse: + user_info, is_authenticated_flag = await validate_and_cache_user(request) + + if not is_authenticated_flag: + _clear_auth_session(request.session) + + if not is_authenticated_flag: + return templates.TemplateResponse( + "landing.j2", + { + "request": request, + "is_authenticated": False, + "user_info": None + } + ) + + return templates.TemplateResponse( + "chat.j2", + { + "request": request, + "is_authenticated": is_authenticated_flag, + "user_info": user_info, + }, + ) -@auth_bp.route('/chat') -def chat(): - """Explicit chat route (renders main chat UI).""" - user_info, is_authenticated = validate_and_cache_user() - return render_template("chat.j2", is_authenticated=is_authenticated, user_info=user_info) +@auth_router.get("/login", response_class=RedirectResponse) +async def login_page(_: Request) -> RedirectResponse: + return RedirectResponse(url="/login/google", status_code=status.HTTP_302_FOUND) -@auth_bp.route("/login") -def login_google(): - """Handle Google OAuth login route.""" - if not google.authorized: - return redirect(url_for("google.login")) +@auth_router.get("/login/google", name="google.login", response_class=RedirectResponse) +async def login_google(request: Request) -> RedirectResponse: + google = _get_provider_client(request, "google") + redirect_uri = _build_callback_url(request, "login/google/authorized") + + # Helpful hint if localhost vs 127.0.0.1 mismatch is likely + if not os.getenv("OAUTH_BASE_URL") and "127.0.0.1" in str(request.base_url): + logging.warning( + "OAUTH_BASE_URL not set and base URL is 127.0.0.1; " + "if your Google OAuth app uses 'http://localhost:5000', " + "set OAUTH_BASE_URL=http://localhost:5000 to avoid redirect_uri mismatch." + ) + + return await google.authorize_redirect(request, redirect_uri) + + +@auth_router.get("/login/google/authorized", response_class=RedirectResponse) +async def google_authorized(request: Request) -> RedirectResponse: try: - resp = google.get("/oauth2/v2/userinfo") - if resp.ok: - google_user = resp.json() - - # Validate required fields - if not google_user.get("id") or not google_user.get("email"): - logging.error("Invalid Google user data received during login") - session.clear() - return redirect(url_for("google.login")) - - # Normalize user info structure - user_info = { - "id": str(google_user.get("id")), # Ensure string type - "name": google_user.get("name", ""), - "email": google_user.get("email"), - "picture": google_user.get("picture", ""), - "provider": "google" - } - session["user_info"] = user_info - session["token_validated_at"] = time.time() - return redirect(url_for("auth.home")) - - # OAuth token might be expired, redirect to login - session.clear() - return redirect(url_for("google.login")) - except (requests.RequestException, KeyError, ValueError) as e: - logging.error("Google login error: %s", e) - session.clear() - return redirect(url_for("google.login")) - - -@auth_bp.route("/logout") -def logout(): - """Handle user logout and token revocation.""" - session.clear() - - # Revoke Google OAuth token if authorized - if google.authorized: - try: - google.get( - "https://accounts.google.com/o/oauth2/revoke", - params={"token": google.access_token} - ) - except (requests.RequestException, AttributeError) as e: - logging.warning("Error revoking Google token: %s", e) - - # Revoke GitHub OAuth token if authorized - if github.authorized: - try: - # GitHub doesn't have a simple revoke endpoint like Google - # The token will expire naturally or can be revoked from GitHub settings - pass - except AttributeError as e: - logging.warning("Error with GitHub token cleanup: %s", e) - - return redirect(url_for("auth.home")) + google = _get_provider_client(request, "google") + token = await google.authorize_access_token(request) + + # Always fetch userinfo explicitly + resp = await google.get("https://www.googleapis.com/oauth2/v2/userinfo", token=token) + if resp.status_code != 200: + logging.error("Failed to fetch Google user info: %s", resp.text) + _clear_auth_session(request.session) + return RedirectResponse(url="/", status_code=status.HTTP_302_FOUND) + + user_info = resp.json() + if not user_info.get("email"): + logging.error("Invalid Google user data received") + _clear_auth_session(request.session) + return RedirectResponse(url="/", status_code=status.HTTP_302_FOUND) + + # Normalize + request.session["user_info"] = { + "id": str(user_info.get("id") or user_info.get("sub")), + "name": user_info.get("name", ""), + "email": user_info.get("email"), + "picture": user_info.get("picture", ""), + "provider": "google", + } + request.session["google_token"] = token + request.session["token_validated_at"] = time.time() + + return RedirectResponse(url="/", status_code=status.HTTP_302_FOUND) + + except AuthlibBaseError as e: + logging.error("Google OAuth error: %s", e) + _clear_auth_session(request.session) + return RedirectResponse(url="/", status_code=status.HTTP_302_FOUND) + + +@auth_router.get("/login/google/callback", response_class=RedirectResponse) +async def google_callback_compat(request: Request) -> RedirectResponse: + qs = f"?{request.url.query}" if request.url.query else "" + return RedirectResponse(url=f"/login/google/authorized{qs}", status_code=status.HTTP_307_TEMPORARY_REDIRECT) + + +@auth_router.get("/login/github", name="github.login", response_class=RedirectResponse) +async def login_github(request: Request) -> RedirectResponse: + github = _get_provider_client(request, "github") + redirect_uri = _build_callback_url(request, "login/github/authorized") + + # Helpful hint if localhost vs 127.0.0.1 mismatch is likely + if not os.getenv("OAUTH_BASE_URL") and "127.0.0.1" in str(request.base_url): + logging.warning( + "OAUTH_BASE_URL not set and base URL is 127.0.0.1; " + "if your GitHub OAuth app uses 'http://localhost:5000', " + "set OAUTH_BASE_URL=http://localhost:5000 to avoid redirect_uri mismatch." + ) + + return await github.authorize_redirect(request, redirect_uri) + + +@auth_router.get("/login/github/authorized", response_class=RedirectResponse) +async def github_authorized(request: Request) -> RedirectResponse: + try: + github = _get_provider_client(request, "github") + token = await github.authorize_access_token(request) + + # Fetch GitHub user info + resp = await github.get("https://api.github.com/user", token=token) + if resp.status_code != 200: + logging.error("Failed to fetch GitHub user info: %s", resp.text) + _clear_auth_session(request.session) + return RedirectResponse(url="/", status_code=status.HTTP_302_FOUND) + + user_info = resp.json() + + # Get user email if not public + email = user_info.get("email") + if not email: + # Try to get primary email from emails endpoint + email_resp = await github.get("https://api.github.com/user/emails", token=token) + if email_resp.status_code == 200: + emails = email_resp.json() + for email_obj in emails: + if email_obj.get("primary"): + email = email_obj.get("email") + break + + if not user_info.get("id") or not email: + logging.error("Invalid GitHub user data received") + _clear_auth_session(request.session) + return RedirectResponse(url="/", status_code=status.HTTP_302_FOUND) + + # Normalize user info structure + request.session["user_info"] = { + "id": str(user_info.get("id")), + "name": user_info.get("name") or user_info.get("login", ""), + "email": email, + "picture": user_info.get("avatar_url", ""), + "provider": "github", + } + request.session["github_token"] = token + request.session["token_validated_at"] = time.time() + + return RedirectResponse(url="/", status_code=status.HTTP_302_FOUND) + + except AuthlibBaseError as e: + logging.error("GitHub OAuth error: %s", e) + _clear_auth_session(request.session) + return RedirectResponse(url="/", status_code=status.HTTP_302_FOUND) + + +@auth_router.get("/login/github/callback", response_class=RedirectResponse) +async def github_callback_compat(request: Request) -> RedirectResponse: + qs = f"?{request.url.query}" if request.url.query else "" + return RedirectResponse(url=f"/login/github/authorized{qs}", status_code=status.HTTP_307_TEMPORARY_REDIRECT) + + +@auth_router.get("/logout", response_class=RedirectResponse) +async def logout(request: Request) -> RedirectResponse: + """Handle user logout and revoke tokens for Google (actively) and GitHub (locally).""" + google_token = request.session.get("google_token") + github_token = request.session.get("github_token") + + # ---- Revoke Google tokens ---- + if google_token: + tokens_to_revoke = [] + if access_token := google_token.get("access_token"): + tokens_to_revoke.append(access_token) + if refresh_token := google_token.get("refresh_token"): + tokens_to_revoke.append(refresh_token) + + if tokens_to_revoke: + try: + async with httpx.AsyncClient() as client: + for token in tokens_to_revoke: + resp = await client.post( + "https://oauth2.googleapis.com/revoke", + params={"token": token}, + headers={"content-type": "application/x-www-form-urlencoded"}, + ) + if resp.status_code != 200: + logging.warning( + "Google token revoke failed (%s): %s", + resp.status_code, + resp.text, + ) + else: + logging.info("Successfully revoked Google token") + except Exception as e: + logging.error("Error revoking Google tokens: %s", e) + + # ---- Handle GitHub tokens ---- + if github_token: + logging.info("GitHub token found, clearing from session (no remote revoke available).") + # GitHub logout is local only unless we call the App management API + + # ---- Clear session auth keys ---- + for key in ["user_info", "google_token", "github_token", "token_validated_at"]: + request.session.pop(key, None) + + return RedirectResponse(url="/", status_code=status.HTTP_302_FOUND) + +# ---- Hook for app factory ---- +def init_auth(app): + """Initialize OAuth and sessions for the app.""" + config = Config(".env") + from authlib.integrations.starlette_client import OAuth + oauth = OAuth(config) + + google_client_id = os.getenv("GOOGLE_CLIENT_ID") + google_client_secret = os.getenv("GOOGLE_CLIENT_SECRET") + if not google_client_id or not google_client_secret: + logging.warning("Google OAuth env vars not set; login will fail until configured.") + + oauth.register( + name="google", + client_id=google_client_id, + client_secret=google_client_secret, + server_metadata_url="https://accounts.google.com/.well-known/openid-configuration", + client_kwargs={"scope": "openid email profile"}, + ) + + github_client_id = os.getenv("GITHUB_CLIENT_ID") + github_client_secret = os.getenv("GITHUB_CLIENT_SECRET") + if not github_client_id or not github_client_secret: + logging.warning("GitHub OAuth env vars not set; login will fail until configured.") + + oauth.register( + name="github", + client_id=github_client_id, + client_secret=github_client_secret, + access_token_url="https://github.com/login/oauth/access_token", + authorize_url="https://github.com/login/oauth/authorize", + api_base_url="https://api.github.com/", + client_kwargs={"scope": "user:email"}, + ) + + app.state.oauth = oauth diff --git a/api/routes/database.py b/api/routes/database.py index aad51c3e..cd4bcac9 100644 --- a/api/routes/database.py +++ b/api/routes/database.py @@ -1,30 +1,37 @@ """Database connection routes for the text2sql API.""" import logging -from flask import Blueprint, jsonify, request, g +from typing import Dict, Any + +from fastapi import APIRouter, Request, HTTPException, status +from fastapi.responses import JSONResponse +from pydantic import BaseModel from api.auth.user_management import token_required from api.loaders.postgres_loader import PostgresLoader from api.loaders.mysql_loader import MySQLLoader -database_bp = Blueprint("database", __name__) +database_router = APIRouter() + + +class DatabaseConnectionRequest(BaseModel): + url: str -@database_bp.route("/database", methods=["POST"]) +@database_router.post("/database") @token_required -def connect_database(): +async def connect_database(request: Request, db_request: DatabaseConnectionRequest): """ Accepts a JSON payload with a database URL and attempts to connect. Supports both PostgreSQL and MySQL databases. Returns success or error message. """ - data = request.get_json() - url = data.get("url") if data else None + url = db_request.url if not url: - return jsonify({"success": False, "error": "No URL provided"}), 400 + raise HTTPException(status_code=400, detail="No URL provided") # Validate URL format if not isinstance(url, str) or len(url.strip()) == 0: - return jsonify({"success": False, "error": "Invalid URL format"}), 400 + raise HTTPException(status_code=400, detail="Invalid URL format") try: success = False @@ -34,31 +41,33 @@ def connect_database(): if url.startswith("postgres://") or url.startswith("postgresql://"): try: # Attempt to connect/load using the PostgreSQL loader - success, result = PostgresLoader.load(g.user_id, url) + success, result = PostgresLoader.load(request.state.user_id, url) except (ValueError, ConnectionError) as e: logging.error("PostgreSQL connection error: %s", str(e)) - return jsonify({"success": False, "error": "Failed to connect to PostgreSQL database"}), 500 + raise HTTPException(status_code=500, detail="Failed to connect to PostgreSQL database") # Check for MySQL URL elif url.startswith("mysql://"): try: # Attempt to connect/load using the MySQL loader - success, result = MySQLLoader.load(g.user_id, url) + success, result = MySQLLoader.load(request.state.user_id, url) except (ValueError, ConnectionError) as e: logging.error("MySQL connection error: %s", str(e)) - return jsonify({"success": False, "error": "Failed to connect to MySQL database"}), 500 + raise HTTPException(status_code=500, detail="Failed to connect to MySQL database") else: - return jsonify({"success": False, "error": "Invalid database URL. Supported formats: postgresql:// or mysql://"}), 400 + raise HTTPException(status_code=400, detail="Invalid database URL. Supported formats: postgresql:// or mysql://") if success: - return jsonify({"success": True, - "message": "Database connected successfully"}), 200 + return JSONResponse(content={ + "success": True, + "message": "Database connected successfully" + }) # Don't return detailed error messages to prevent information exposure logging.error("Database loader failed: %s", result) - return jsonify({"success": False, "error": "Failed to load database schema"}), 400 + raise HTTPException(status_code=400, detail="Failed to load database schema") except (ValueError, TypeError) as e: logging.error("Unexpected error in database connection: %s", str(e)) - return jsonify({"success": False, "error": "Internal server error"}), 500 + raise HTTPException(status_code=500, detail="Internal server error") diff --git a/api/routes/graphs.py b/api/routes/graphs.py index eaf761a8..9a05787e 100644 --- a/api/routes/graphs.py +++ b/api/routes/graphs.py @@ -2,10 +2,14 @@ import json import logging +import time from concurrent.futures import ThreadPoolExecutor from concurrent.futures import TimeoutError as FuturesTimeoutError +from typing import Dict, Any -from flask import Blueprint, jsonify, request, Response, stream_with_context, g +from fastapi import APIRouter, Request, HTTPException, status, UploadFile, File, Form +from fastapi.responses import JSONResponse, StreamingResponse +from pydantic import BaseModel from api.agents import AnalysisAgent, RelevancyAgent, ResponseFormatterAgent from api.auth.user_management import token_required @@ -20,7 +24,24 @@ # Use the same delimiter as in the JavaScript MESSAGE_DELIMITER = "|||FALKORDB_MESSAGE_BOUNDARY|||" -graphs_bp = Blueprint("graphs", __name__, url_prefix="/graphs") +graphs_router = APIRouter() + + +class GraphData(BaseModel): + database: str + + +class ChatRequest(BaseModel): + chat: list + result: list = None + instructions: str = None + + +class ConfirmRequest(BaseModel): + sql_query: str + confirmation: str = "" + chat: list = [] + def get_database_type_and_loader(db_url: str): """ @@ -55,23 +76,23 @@ def sanitize_log_input(value: str) -> str: return str(value) return value.replace('\n', ' ').replace('\r', ' ') -@graphs_bp.route("") +@graphs_router.get("") @token_required -def list_graphs(): +async def list_graphs(request: Request): """ This route is used to list all the graphs that are available in the database. """ - user_id = g.user_id + user_id = request.state.user_id user_graphs = db.list_graphs() # Only include graphs that start with user_id + '_', and strip the prefix filtered_graphs = [graph[len(f"{user_id}_"):] for graph in user_graphs if graph.startswith(f"{user_id}_")] - return jsonify(filtered_graphs) + return JSONResponse(content=filtered_graphs) -@graphs_bp.route("//data", methods=["GET"]) +@graphs_router.get("/{graph_id}/data") @token_required -def get_graph_data(graph_id: str): +async def get_graph_data(request: Request, graph_id: str): """Return all nodes and edges for the specified graph (namespaced to the user). This endpoint returns a JSON object with two keys: `nodes` and `edges`. @@ -79,16 +100,16 @@ def get_graph_data(graph_id: str): Edges contain source and target node names (or internal ids), type and props. """ if not graph_id or not isinstance(graph_id, str): - return jsonify({"error": "Invalid graph_id"}), 400 + return JSONResponse(content={"error": "Invalid graph_id"}, status_code=400) graph_id = graph_id.strip()[:200] - namespaced = g.user_id + "_" + graph_id + namespaced = request.state.user_id + "_" + graph_id try: graph = db.select_graph(namespaced) except Exception as e: logging.error("Failed to select graph %s: %s", sanitize_log_input(namespaced), e) - return jsonify({"error": "Graph not found or database error"}), 404 + return JSONResponse(content={"error": "Graph not found or database error"}, status_code=404) # Build table nodes with columns and table-to-table links (foreign keys) tables_query = """ @@ -109,7 +130,7 @@ def get_graph_data(graph_id: str): links_res = graph.query(links_query).result_set except Exception as e: logging.error("Error querying graph data for %s: %s", sanitize_log_input(namespaced), e) - return jsonify({"error": "Failed to read graph data"}), 500 + return JSONResponse(content={"error": "Failed to read graph data"}, status_code=500) nodes = [] for row in tables_res: @@ -165,12 +186,12 @@ def get_graph_data(graph_id: str): seen.add(key) links.append({"source": source, "target": target}) - return jsonify({"nodes": nodes, "links": links}) + return JSONResponse(content={"nodes": nodes, "links": links}) -@graphs_bp.route("", methods=["POST"]) +@graphs_router.post("") @token_required -def load_graph(): +async def load_graph(request: Request, data: GraphData = None, file: UploadFile = File(None)): """ This route is used to load the graph data into the database. It expects either: @@ -178,112 +199,91 @@ def load_graph(): - A File upload (multipart/form-data) - An XML payload (application/xml or text/xml) """ - content_type = request.content_type success, result = False, "Invalid content type" graph_id = "" # ✅ Handle JSON Payload - if content_type.startswith("application/json"): - data = request.get_json() - if not data or "database" not in data: - return jsonify({"error": "Invalid JSON data"}), 400 - - graph_id = g.user_id + "_" + data["database"] - success, result = JSONLoader.load(graph_id, data) - - # # ✅ Handle XML Payload - # elif content_type.startswith("application/xml") or content_type.startswith("text/xml"): - # xml_data = request.data - # graph_id = "" - # success, result = ODataLoader.load(graph_id, xml_data) - - # # ✅ Handle CSV Payload - # elif content_type.startswith("text/csv"): - # csv_data = request.data - # graph_id = "" - # success, result = CSVLoader.load(graph_id, csv_data) - - # ✅ Handle File Upload (FormData with JSON/XML) - elif content_type.startswith("multipart/form-data"): - if "file" not in request.files: - return jsonify({"error": "No file uploaded"}), 400 - - file = request.files["file"] - if file.filename == "": - return jsonify({"error": "Empty file"}), 400 + if data: + if not hasattr(data, 'database') or not data.database: + raise HTTPException(status_code=400, detail="Invalid JSON data") + + graph_id = request.state.user_id + "_" + data.database + success, result = JSONLoader.load(graph_id, data.dict()) + + # ✅ Handle File Upload + elif file: + content = await file.read() + filename = file.filename # ✅ Check if file is JSON - if file.filename.endswith(".json"): + if filename.endswith(".json"): try: - data = json.load(file) - graph_id = g.user_id + "_" + data.get("database", "") + data = json.loads(content.decode("utf-8")) + graph_id = request.state.user_id + "_" + data.get("database", "") success, result = JSONLoader.load(graph_id, data) except json.JSONDecodeError: - return jsonify({"error": "Invalid JSON file"}), 400 + raise HTTPException(status_code=400, detail="Invalid JSON file") # ✅ Check if file is XML - elif file.filename.endswith(".xml"): - xml_data = file.read().decode("utf-8") # Convert bytes to string - graph_id = g.user_id + "_" + file.filename.replace(".xml", "") + elif filename.endswith(".xml"): + xml_data = content.decode("utf-8") + graph_id = request.state.user_id + "_" + filename.replace(".xml", "") success, result = ODataLoader.load(graph_id, xml_data) # ✅ Check if file is csv - elif file.filename.endswith(".csv"): - csv_data = file.read().decode("utf-8") # Convert bytes to string - graph_id = g.user_id + "_" + file.filename.replace(".csv", "") + elif filename.endswith(".csv"): + csv_data = content.decode("utf-8") + graph_id = request.state.user_id + "_" + filename.replace(".csv", "") success, result = CSVLoader.load(graph_id, csv_data) else: - return jsonify({"error": "Unsupported file type"}), 415 + raise HTTPException(status_code=415, detail="Unsupported file type") else: - return jsonify({"error": "Unsupported Content-Type"}), 415 + raise HTTPException(status_code=415, detail="Unsupported Content-Type") # ✅ Return the final response if success: - return jsonify({"message": "Graph loaded successfully", "graph_id": graph_id}) + return JSONResponse(content={"message": "Graph loaded successfully", "graph_id": graph_id}) # Log detailed error but return generic message to user logging.error("Graph loading failed: %s", str(result)[:100]) - return jsonify({"error": "Failed to load graph data"}), 400 + raise HTTPException(status_code=400, detail="Failed to load graph data") -@graphs_bp.route("/", methods=["POST"]) +@graphs_router.post("/{graph_id}") @token_required -def query_graph(graph_id: str): +async def query_graph(request: Request, graph_id: str, chat_data: ChatRequest): """ text2sql """ # Input validation if not graph_id or not isinstance(graph_id, str): - return jsonify({"error": "Invalid graph_id"}), 400 + raise HTTPException(status_code=400, detail="Invalid graph_id") # Sanitize graph_id to prevent injection graph_id = graph_id.strip()[:100] # Limit length and strip whitespace if not graph_id: - return jsonify({"error": "Invalid graph_id"}), 400 + raise HTTPException(status_code=400, detail="Invalid graph_id") - graph_id = g.user_id + "_" + graph_id - request_data = request.get_json() + graph_id = request.state.user_id + "_" + graph_id - if not request_data: - return jsonify({"error": "No JSON data provided"}), 400 - - queries_history = request_data.get("chat") - result_history = request_data.get("result") - instructions = request_data.get("instructions") + queries_history = chat_data.chat if hasattr(chat_data, 'chat') else None + result_history = chat_data.result if hasattr(chat_data, 'result') else None + instructions = chat_data.instructions if hasattr(chat_data, 'instructions') else None if not queries_history or not isinstance(queries_history, list): - return jsonify({"error": "Invalid or missing chat history"}), 400 + raise HTTPException(status_code=400, detail="Invalid or missing chat history") if len(queries_history) == 0: - return jsonify({"error": "Empty chat history"}), 400 + raise HTTPException(status_code=400, detail="Empty chat history") logging.info("User Query: %s", sanitize_query(queries_history[-1])) # Create a generator function for streaming - def generate(): + async def generate(): agent_rel = RelevancyAgent(queries_history, result_history) agent_an = AnalysisAgent(queries_history, result_history) + step1_start = time.perf_counter() step = {"type": "reasoning_step", "message": "Step 1: Analyzing user query and generating SQL..."} @@ -303,8 +303,10 @@ def generate(): logging.info("Calling to relevancy agent with query: %s", sanitize_query(queries_history[-1])) - + rel_start = time.perf_counter() answer_rel = agent_rel.get_answer(queries_history[-1], db_description) + rel_elapsed = time.perf_counter() - rel_start + logging.info("Relevancy check took %.2f seconds", rel_elapsed) if answer_rel["status"] != "On-topic": step = { "type": "followup_questions", @@ -312,12 +314,21 @@ def generate(): } logging.info("SQL Fail reason: %s", answer_rel["reason"]) yield json.dumps(step) + MESSAGE_DELIMITER + # Total time for the pre-analysis phase + step1_elapsed = time.perf_counter() - step1_start + logging.info("Step 1 (relevancy + prep) took %.2f seconds", step1_elapsed) else: # Use a thread pool to enforce timeout with ThreadPoolExecutor(max_workers=1) as executor: + find_start = time.perf_counter() future = executor.submit(find, graph_id, queries_history, db_description) try: _, result, _ = future.result(timeout=120) + find_elapsed = time.perf_counter() - find_start + logging.info("Finding relevant tables took %.2f seconds", find_elapsed) + # Total time for the pre-analysis phase + step1_elapsed = time.perf_counter() - step1_start + logging.info("Step 1 (relevancy + table finding) took %.2f seconds", step1_elapsed) except FuturesTimeoutError: yield json.dumps( { @@ -337,9 +348,12 @@ def generate(): logging.info("Calling to analysis agent with query: %s", sanitize_query(queries_history[-1])) + analysis_start = time.perf_counter() answer_an = agent_an.get_analysis( queries_history[-1], result, db_description, instructions ) + analysis_elapsed = time.perf_counter() - analysis_start + logging.info("SQL generation took %.2f seconds", analysis_elapsed) logging.info("SQL Result: %s", answer_an['sql_query']) yield json.dumps( @@ -487,23 +501,23 @@ def generate(): {"type": "error", "message": "Error executing SQL query"} ) + MESSAGE_DELIMITER - return Response(stream_with_context(generate()), content_type="application/json") + return StreamingResponse(generate(), media_type="application/json") -@graphs_bp.route("//confirm", methods=["POST"]) +@graphs_router.post("/{graph_id}/confirm") @token_required -def confirm_destructive_operation(graph_id: str): +async def confirm_destructive_operation(request: Request, graph_id: str, confirm_data: ConfirmRequest): """ Handle user confirmation for destructive SQL operations """ - graph_id = g.user_id + "_" + graph_id.strip() - request_data = request.get_json() - confirmation = request_data.get("confirmation", "").strip().upper() - sql_query = request_data.get("sql_query", "") - queries_history = request_data.get("chat", []) + graph_id = request.state.user_id + "_" + graph_id.strip() + + confirmation = confirm_data.confirmation.strip().upper() if hasattr(confirm_data, 'confirmation') else "" + sql_query = confirm_data.sql_query if hasattr(confirm_data, 'sql_query') else "" + queries_history = confirm_data.chat if hasattr(confirm_data, 'chat') else [] if not sql_query: - return jsonify({"error": "No SQL query provided"}), 400 + raise HTTPException(status_code=400, detail="No SQL query provided") # Create a generator function for streaming the confirmation response def generate_confirmation(): @@ -603,56 +617,56 @@ def generate_confirmation(): } ) + MESSAGE_DELIMITER - return Response(stream_with_context(generate_confirmation()), content_type="application/json") + return StreamingResponse(generate_confirmation(), media_type="application/json") -@graphs_bp.route("//refresh", methods=["POST"]) +@graphs_router.post("/{graph_id}/refresh") @token_required -def refresh_graph_schema(graph_id: str): +async def refresh_graph_schema(request: Request, graph_id: str): """ Manually refresh the graph schema from the database. This endpoint allows users to manually trigger a schema refresh if they suspect the graph is out of sync with the database. """ - graph_id = g.user_id + "_" + graph_id.strip() + graph_id = request.state.user_id + "_" + graph_id.strip() try: # Get database connection details _, db_url = get_db_description(graph_id) if not db_url or db_url == "No URL available for this database.": - return jsonify({ + return JSONResponse({ "success": False, "error": "No database URL found for this graph" - }), 400 + }, status_code=400) # Determine database type and get appropriate loader db_type, loader_class = get_database_type_and_loader(db_url) if not loader_class: - return jsonify({ + return JSONResponse({ "success": False, "error": "Unable to determine database type" - }), 400 + }, status_code=400) # Perform schema refresh using the appropriate loader success, message = loader_class.refresh_graph_schema(graph_id, db_url) if success: - return jsonify({ + return JSONResponse({ "success": True, "message": f"Graph schema refreshed successfully using {db_type}" - }), 200 + }) logging.error("Schema refresh failed for graph %s: %s", graph_id, message) - return jsonify({ + return JSONResponse({ "success": False, "error": "Failed to refresh schema" - }), 500 + }, status_code=500) except Exception as e: logging.error("Error in manual schema refresh: %s", e) - return jsonify({ + return JSONResponse({ "success": False, "error": "Error refreshing schema" - }), 500 + }, status_code=500) diff --git a/app/package-lock.json b/app/package-lock.json index 5e18cc05..c24dedb7 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -8,14 +8,34 @@ "name": "queryweaver-app", "version": "0.0.1", "devDependencies": { - "esbuild": "^0.18.0", - "typescript": "^5.0.0" + "@typescript-eslint/eslint-plugin": "^8.40.0", + "@typescript-eslint/parser": "^8.40.0", + "esbuild": "^0.25.9", + "eslint": "^9.33.0", + "typescript": "^5.9.2" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.9.tgz", + "integrity": "sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" } }, "node_modules/@esbuild/android-arm": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", - "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.9.tgz", + "integrity": "sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==", "cpu": [ "arm" ], @@ -26,13 +46,13 @@ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", - "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.9.tgz", + "integrity": "sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==", "cpu": [ "arm64" ], @@ -43,13 +63,13 @@ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", - "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.9.tgz", + "integrity": "sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==", "cpu": [ "x64" ], @@ -60,13 +80,13 @@ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", - "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.9.tgz", + "integrity": "sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==", "cpu": [ "arm64" ], @@ -77,13 +97,13 @@ "darwin" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", - "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.9.tgz", + "integrity": "sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==", "cpu": [ "x64" ], @@ -94,13 +114,13 @@ "darwin" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", - "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.9.tgz", + "integrity": "sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==", "cpu": [ "arm64" ], @@ -111,13 +131,13 @@ "freebsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", - "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.9.tgz", + "integrity": "sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==", "cpu": [ "x64" ], @@ -128,13 +148,13 @@ "freebsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-arm": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", - "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.9.tgz", + "integrity": "sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==", "cpu": [ "arm" ], @@ -145,13 +165,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", - "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.9.tgz", + "integrity": "sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==", "cpu": [ "arm64" ], @@ -162,13 +182,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", - "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.9.tgz", + "integrity": "sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==", "cpu": [ "ia32" ], @@ -179,13 +199,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", - "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.9.tgz", + "integrity": "sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==", "cpu": [ "loong64" ], @@ -196,13 +216,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", - "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.9.tgz", + "integrity": "sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==", "cpu": [ "mips64el" ], @@ -213,13 +233,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", - "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.9.tgz", + "integrity": "sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==", "cpu": [ "ppc64" ], @@ -230,13 +250,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", - "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.9.tgz", + "integrity": "sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==", "cpu": [ "riscv64" ], @@ -247,13 +267,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", - "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.9.tgz", + "integrity": "sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==", "cpu": [ "s390x" ], @@ -264,13 +284,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", - "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.9.tgz", + "integrity": "sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==", "cpu": [ "x64" ], @@ -281,13 +301,30 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.9.tgz", + "integrity": "sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", - "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.9.tgz", + "integrity": "sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==", "cpu": [ "x64" ], @@ -298,13 +335,30 @@ "netbsd" ], "engines": { - "node": ">=12" + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.9.tgz", + "integrity": "sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", - "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.9.tgz", + "integrity": "sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==", "cpu": [ "x64" ], @@ -315,13 +369,30 @@ "openbsd" ], "engines": { - "node": ">=12" + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.9.tgz", + "integrity": "sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", - "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.9.tgz", + "integrity": "sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==", "cpu": [ "x64" ], @@ -332,13 +403,13 @@ "sunos" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", - "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.9.tgz", + "integrity": "sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==", "cpu": [ "arm64" ], @@ -349,13 +420,13 @@ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", - "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.9.tgz", + "integrity": "sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==", "cpu": [ "ia32" ], @@ -366,13 +437,13 @@ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", - "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.9.tgz", + "integrity": "sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==", "cpu": [ "x64" ], @@ -383,59 +454,1729 @@ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/esbuild": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", - "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", + "node_modules/@eslint-community/eslint-utils": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", + "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", "dev": true, - "hasInstallScript": true, "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" + "dependencies": { + "eslint-visitor-keys": "^3.4.3" }, "engines": { - "node": ">=12" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, - "optionalDependencies": { - "@esbuild/android-arm": "0.18.20", - "@esbuild/android-arm64": "0.18.20", - "@esbuild/android-x64": "0.18.20", - "@esbuild/darwin-arm64": "0.18.20", - "@esbuild/darwin-x64": "0.18.20", - "@esbuild/freebsd-arm64": "0.18.20", - "@esbuild/freebsd-x64": "0.18.20", - "@esbuild/linux-arm": "0.18.20", - "@esbuild/linux-arm64": "0.18.20", - "@esbuild/linux-ia32": "0.18.20", - "@esbuild/linux-loong64": "0.18.20", - "@esbuild/linux-mips64el": "0.18.20", - "@esbuild/linux-ppc64": "0.18.20", - "@esbuild/linux-riscv64": "0.18.20", - "@esbuild/linux-s390x": "0.18.20", - "@esbuild/linux-x64": "0.18.20", - "@esbuild/netbsd-x64": "0.18.20", - "@esbuild/openbsd-x64": "0.18.20", - "@esbuild/sunos-x64": "0.18.20", - "@esbuild/win32-arm64": "0.18.20", - "@esbuild/win32-ia32": "0.18.20", - "@esbuild/win32-x64": "0.18.20" + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, - "node_modules/typescript": { - "version": "5.9.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", - "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", + "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", "dev": true, "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" + "dependencies": { + "@eslint/object-schema": "^2.1.6", + "debug": "^4.3.1", + "minimatch": "^3.1.2" }, "engines": { - "node": ">=14.17" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-array/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.1.tgz", + "integrity": "sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.2.tgz", + "integrity": "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==", + "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.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "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.0", + "minimatch": "^3.1.2", + "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/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/@eslint/eslintrc/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/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "9.33.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.33.0.tgz", + "integrity": "sha512-5K1/mKhWaMfreBGJTwval43JJmkip0RmM+3+IuqupeSKNC/Th2Kc7ucaq5ovTSra/OOKB9c58CGSz3QMVbWt0A==", + "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.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.5.tgz", + "integrity": "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.15.2", + "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.6", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", + "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.3.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "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/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "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/@typescript-eslint/eslint-plugin": { + "version": "8.40.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.40.0.tgz", + "integrity": "sha512-w/EboPlBwnmOBtRbiOvzjD+wdiZdgFeo17lkltrtn7X37vagKKWJABvyfsJXTlHe6XBzugmYgd4A4nW+k8Mixw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.40.0", + "@typescript-eslint/type-utils": "8.40.0", + "@typescript-eslint/utils": "8.40.0", + "@typescript-eslint/visitor-keys": "8.40.0", + "graphemer": "^1.4.0", + "ignore": "^7.0.0", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.40.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.40.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.40.0.tgz", + "integrity": "sha512-jCNyAuXx8dr5KJMkecGmZ8KI61KBUhkCob+SD+C+I5+Y1FWI2Y3QmY4/cxMCC5WAsZqoEtEETVhUiUMIGCf6Bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.40.0", + "@typescript-eslint/types": "8.40.0", + "@typescript-eslint/typescript-estree": "8.40.0", + "@typescript-eslint/visitor-keys": "8.40.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.40.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.40.0.tgz", + "integrity": "sha512-/A89vz7Wf5DEXsGVvcGdYKbVM9F7DyFXj52lNYUDS1L9yJfqjW/fIp5PgMuEJL/KeqVTe2QSbXAGUZljDUpArw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.40.0", + "@typescript-eslint/types": "^8.40.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.40.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.40.0.tgz", + "integrity": "sha512-y9ObStCcdCiZKzwqsE8CcpyuVMwRouJbbSrNuThDpv16dFAj429IkM6LNb1dZ2m7hK5fHyzNcErZf7CEeKXR4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.40.0", + "@typescript-eslint/visitor-keys": "8.40.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.40.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.40.0.tgz", + "integrity": "sha512-jtMytmUaG9d/9kqSl/W3E3xaWESo4hFDxAIHGVW/WKKtQhesnRIJSAJO6XckluuJ6KDB5woD1EiqknriCtAmcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.40.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.40.0.tgz", + "integrity": "sha512-eE60cK4KzAc6ZrzlJnflXdrMqOBaugeukWICO2rB0KNvwdIMaEaYiywwHMzA1qFpTxrLhN9Lp4E/00EgWcD3Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.40.0", + "@typescript-eslint/typescript-estree": "8.40.0", + "@typescript-eslint/utils": "8.40.0", + "debug": "^4.3.4", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.40.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.40.0.tgz", + "integrity": "sha512-ETdbFlgbAmXHyFPwqUIYrfc12ArvpBhEVgGAxVYSwli26dn8Ko+lIo4Su9vI9ykTZdJn+vJprs/0eZU0YMAEQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.40.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.40.0.tgz", + "integrity": "sha512-k1z9+GJReVVOkc1WfVKs1vBrR5MIKKbdAjDTPvIK3L8De6KbFfPFt6BKpdkdk7rZS2GtC/m6yI5MYX+UsuvVYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.40.0", + "@typescript-eslint/tsconfig-utils": "8.40.0", + "@typescript-eslint/types": "8.40.0", + "@typescript-eslint/visitor-keys": "8.40.0", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.40.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.40.0.tgz", + "integrity": "sha512-Cgzi2MXSZyAUOY+BFwGs17s7ad/7L+gKt6Y8rAVVWS+7o6wrjeFN4nVfTpbE25MNcxyJ+iYUXflbs2xR9h4UBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.40.0", + "@typescript-eslint/types": "8.40.0", + "@typescript-eslint/typescript-estree": "8.40.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.40.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.40.0.tgz", + "integrity": "sha512-8CZ47QwalyRjsypfwnbI3hKy5gJDPmrkLjkgMxhi0+DZZ2QNx2naS6/hWoVYUHU7LU2zleF68V9miaVZvhFfTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.40.0", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/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/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "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/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/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/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/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/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "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/esbuild": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.9.tgz", + "integrity": "sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.9", + "@esbuild/android-arm": "0.25.9", + "@esbuild/android-arm64": "0.25.9", + "@esbuild/android-x64": "0.25.9", + "@esbuild/darwin-arm64": "0.25.9", + "@esbuild/darwin-x64": "0.25.9", + "@esbuild/freebsd-arm64": "0.25.9", + "@esbuild/freebsd-x64": "0.25.9", + "@esbuild/linux-arm": "0.25.9", + "@esbuild/linux-arm64": "0.25.9", + "@esbuild/linux-ia32": "0.25.9", + "@esbuild/linux-loong64": "0.25.9", + "@esbuild/linux-mips64el": "0.25.9", + "@esbuild/linux-ppc64": "0.25.9", + "@esbuild/linux-riscv64": "0.25.9", + "@esbuild/linux-s390x": "0.25.9", + "@esbuild/linux-x64": "0.25.9", + "@esbuild/netbsd-arm64": "0.25.9", + "@esbuild/netbsd-x64": "0.25.9", + "@esbuild/openbsd-arm64": "0.25.9", + "@esbuild/openbsd-x64": "0.25.9", + "@esbuild/openharmony-arm64": "0.25.9", + "@esbuild/sunos-x64": "0.25.9", + "@esbuild/win32-arm64": "0.25.9", + "@esbuild/win32-ia32": "0.25.9", + "@esbuild/win32-x64": "0.25.9" + } + }, + "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.33.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.33.0.tgz", + "integrity": "sha512-TS9bTNIryDzStCpJN93aC5VRSW3uTx9sClUn4B87pwiCaJh220otoI0X8mJKr+VcPtniMdN8GKjlwgWGUv5ZKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.0", + "@eslint/config-helpers": "^0.3.1", + "@eslint/core": "^0.15.2", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.33.0", + "@eslint/plugin-kit": "^0.3.5", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "ajv": "^6.12.4", + "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.2", + "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-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": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.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/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/eslint/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/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "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/espree/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/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "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-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "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/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-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.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "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": "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/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "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/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/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/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "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/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "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/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/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-api-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "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/typescript": { + "version": "5.9.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", + "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "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/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/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } } } diff --git a/app/package.json b/app/package.json index 4025d74b..0c39ce12 100644 --- a/app/package.json +++ b/app/package.json @@ -9,10 +9,10 @@ "lint": "eslint --ext .ts ./ts --max-warnings=0 || true" }, "devDependencies": { - "esbuild": "^0.18.0", - "typescript": "^5.0.0", - "eslint": "^8.0.0", - "@typescript-eslint/parser": "^5.0.0", - "@typescript-eslint/eslint-plugin": "^5.0.0" + "esbuild": "^0.25.9", + "typescript": "^5.9.2", + "eslint": "^9.33.0", + "@typescript-eslint/parser": "^8.40.0", + "@typescript-eslint/eslint-plugin": "^8.40.0" } } diff --git a/app/public/css/landing.css b/app/public/css/landing.css index 58f252a3..16f1188c 100644 --- a/app/public/css/landing.css +++ b/app/public/css/landing.css @@ -1,61 +1,189 @@ -.landing-container{ - max-width:1200px; - margin:3rem auto; - display:grid; - grid-template-columns:1fr 500px; - gap:2.5rem; - align-items:center; - padding:0 1rem; -} - -.hero-left{padding:1rem 0} -.hero-title{ +.landing-container { + max-width: 1200px; + margin: 3rem auto; + display: grid; + grid-template-columns: 1fr 500px; + gap: 2.5rem; + align-items: center; + padding: 0 1rem; +} + +.hero-left { + padding: 1rem 0; +} + +.hero-title { font-family: 'Inter', system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial; - font-weight:800; - font-size:4rem; - line-height:1.02; - margin:0 0 1rem 0; - color:var(--text-primary); + font-weight: 800; + font-size: 4rem; + line-height: 1.02; + margin: 0 0 1rem 0; + color: var(--text-primary); +} + +.hero-sub { + color: var(--text-secondary); + font-size: 1.05rem; + max-width: 44rem; + margin-bottom: 1.5rem; } -.hero-sub{ - color:var(--text-secondary); - font-size:1.05rem; - max-width:44rem; - margin-bottom:1.5rem; + +.hero-ctas { + display: flex; + gap: 1rem; + align-items: center; } -.hero-ctas{display:flex;gap:1rem;align-items:center} -.btn-pill{display:inline-block;padding:0.9rem 1.25rem;border-radius:999px;background:var(--falkor-primary);color:#fff;text-decoration:none;box-shadow:0 8px 20px rgba(91,107,192,0.14);} -.btn-ghost{background:transparent;color:var(--text-primary);text-decoration:none;padding:0.9rem 1.25rem} -.demo-card{background:var(--falkor-secondary);border-radius:12px;box-shadow:0 16px 30px rgba(11,18,32,0.06);padding:1rem;border:1px solid var(--border-color)} +.btn-pill { + display: inline-block; + padding: 0.9rem 1.25rem; + border-radius: 999px; + background: var(--falkor-primary); + color: #fff; + text-decoration: none; + box-shadow: 0 8px 20px rgba(91, 107, 192, 0.14); +} + +.btn-ghost { + display: inline-block; + padding: 0.9rem 1.25rem; + border-radius: 999px; + background: transparent; + color: var(--text-primary); + text-decoration: none; + border: 1px solid var(--falkor-border-tertiary); + box-shadow: 0 6px 14px rgba(11, 18, 32, 0.06); + transition: background 0.12s ease, border-color 0.12s ease, box-shadow 0.12s ease; +} + +.btn-ghost:hover { + background: rgba(255, 255, 255, 0.02); + border-color: var(--border-color); + box-shadow: 0 10px 24px rgba(11, 18, 32, 0.08); +} + +.btn-ghost:focus { + outline: none; + box-shadow: 0 0 0 3px rgba(91, 107, 192, 0.08); +} + +.demo-card { + background: var(--falkor-secondary); + border-radius: 12px; + box-shadow: 0 16px 30px rgba(11, 18, 32, 0.06); + padding: 1rem; + border: 1px solid var(--border-color); +} /* Use a neutral themed surface for the inner area so it adapts to light/dark */ -.demo-inner{background:var(--bg-tertiary);border-radius:8px;padding:1rem;border:1px solid var(--falkor-border-tertiary)} -.demo-label{font-size:0.9rem;color:var(--text-secondary);margin-bottom:0.5rem} +.demo-inner { + background: var(--bg-tertiary); + border-radius: 8px; + padding: 1rem; + border: 1px solid var(--falkor-border-tertiary); +} + +.demo-label { + font-size: 0.9rem; + color: var(--text-secondary); + margin-bottom: 0.5rem; +} + +.demo-sql-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-top: 1rem; +} + +.demo-success { + display: flex; + align-items: center; + gap: 0.5rem; + color: #10B981; + font-size: 0.9rem; + font-weight: 600; +} + +.demo-success svg { + flex-shrink: 0; +} /* Use the theme's secondary surface for the white/black boxes so text contrast is correct in both themes */ -.demo-question{background:var(--falkor-secondary);border-radius:6px;padding:0.75rem 1rem;border:1px solid var(--falkor-border-tertiary);color:var(--text-primary);height:120px;white-space:pre-wrap;font-family:monospace;font-size:0.95rem;overflow:auto;line-height:1.3} -.demo-sql{background:var(--falkor-secondary);border-radius:6px;padding:0.75rem 1rem;border:1px solid var(--falkor-border-tertiary);color:var(--text-primary);margin-top:0.8rem;font-size:0.9rem;overflow:auto;height:200px;line-height:1.25} -.demo-sql.typing{position:relative} -.demo-sql.typing::after{ +.demo-question { + background: var(--falkor-secondary); + border-radius: 6px; + padding: 0.75rem 1rem; + border: 1px solid var(--falkor-border-tertiary); + color: var(--text-primary); + height: 120px; + white-space: pre-wrap; + font-family: monospace; + font-size: 0.95rem; + overflow: auto; + line-height: 1.3; +} + +.demo-sql { + background: var(--falkor-secondary); + border-radius: 6px; + padding: 0.75rem 1rem; + border: 1px solid var(--falkor-border-tertiary); + color: var(--text-primary); + margin-top: 0.5rem; + font-size: 0.9rem; + overflow: auto; + height: 200px; + line-height: 1.25; +} + +.demo-sql.typing { + position: relative; +} + +.demo-sql.typing::after { content: ''; - display:inline-block; - width:10px; - height:1.1em; - background:var(--falkor-primary); - margin-left:6px; - vertical-align:bottom; + display: inline-block; + width: 10px; + height: 1.1em; + background: var(--falkor-primary); + margin-left: 6px; + vertical-align: bottom; animation: blink-caret 1s steps(1) infinite; } -@keyframes blink-caret{ - 0%, 50% { opacity: 1 } - 51%, 100% { opacity: 0 } +@keyframes blink-caret { + 0%, 50% { + opacity: 1; + } + + 51%, 100% { + opacity: 0; + } } -.demo-cta{margin-top:1rem;text-align:center} -.demo-cta .btn-full{display:inline-block;width:100%;padding:0.75rem;border-radius:8px;background:#e7f1ff;color:var(--falkor-primary);text-decoration:none} -@media (max-width:900px){ - .landing-container{grid-template-columns:1fr;gap:1.25rem} - .hero-title{font-size:2.4rem} +.demo-cta { + margin-top: 1rem; + text-align: center; +} + +.demo-cta .btn-full { + display: inline-block; + width: 100%; + padding: 0.75rem; + border-radius: 8px; + background: #e7f1ff; + color: var(--falkor-primary); + text-decoration: none; +} + +@media (max-width: 900px) { + .landing-container { + grid-template-columns: 1fr; + gap: 1.25rem; + } + + .hero-title { + font-size: 2.4rem; + } } diff --git a/app/templates/base.j2 b/app/templates/base.j2 index c4a47c51..d1b578ac 100644 --- a/app/templates/base.j2 +++ b/app/templates/base.j2 @@ -4,8 +4,8 @@ {% block title %}Chatbot Interface{% endblock %} - - + + {% block extra_css %}{% endblock %} {% if google_tag_manager_id %} @@ -34,7 +34,7 @@ {% block scripts %} diff --git a/app/templates/components/chat_header.j2 b/app/templates/components/chat_header.j2 index edbed257..1e10a2ae 100644 --- a/app/templates/components/chat_header.j2 +++ b/app/templates/components/chat_header.j2 @@ -1,6 +1,6 @@ {# Chat header with logo, title, and action buttons #}
- +

Natural Language to SQL Generator

diff --git a/app/templates/components/scripts.j2 b/app/templates/components/scripts.j2 index a9c7e273..34438852 100644 --- a/app/templates/components/scripts.j2 +++ b/app/templates/components/scripts.j2 @@ -14,4 +14,4 @@ // Keep the default '-' if fetch fails }); - + diff --git a/app/templates/landing.j2 b/app/templates/landing.j2 index 1e2929f6..c7779620 100644 --- a/app/templates/landing.j2 +++ b/app/templates/landing.j2 @@ -3,7 +3,7 @@ {% block title %}QueryWeaver — Natural language to SQL{% endblock %} {% block extra_css %} - + {% endblock %} {% block content %} @@ -16,7 +16,7 @@ @@ -26,18 +26,17 @@
Your Business Question:
Which customers from Germany who bought 'Chai' also have an open support ticket in Salesforce?
-
Generated SQL:
-
-SELECT c.companyName
-FROM customers c
-JOIN orders o ON c.customerID = o.customerID
-JOIN order_details od ON o.orderID = od.orderID
-JOIN products p ON od.productID = p.productID
-JOIN salesforce_tickets st ON c.customerID = st.customerID
-WHERE c.country = 'Germany'
-  AND p.productName = 'Chai'
-  AND st.status = 'Open';
-      
+
+
Generated SQL:
+ +
+

 
       
@@ -63,6 +62,7 @@ WHERE c.country = 'Germany' let idx = 0; const qEl = document.querySelector('.demo-question'); const sEl = document.querySelector('.demo-sql'); + const successEl = document.querySelector('.demo-success'); const btn = document.getElementById('demo-next'); let typingTimer = null; @@ -75,10 +75,13 @@ WHERE c.country = 'Germany' sEl.classList.remove('typing'); sEl.textContent = ex.sql; } + if(successEl) successEl.style.display = 'flex'; } function typeSql(text){ if(!sEl) return; + // hide success while typing + if(successEl) successEl.style.display = 'none'; // stop any previous typing if(typingTimer) { clearInterval(typingTimer); @@ -94,6 +97,8 @@ WHERE c.country = 'Germany' clearInterval(typingTimer); typingTimer = null; sEl.classList.remove('typing'); + // show success when typing completes + if(successEl) successEl.style.display = 'flex'; } }, typingSpeed); } @@ -108,8 +113,10 @@ WHERE c.country = 'Germany' }); } - // initial render: show full first example without typing - renderFull(0); + // initial render: use typing animation for first example too + const firstExample = examples[0]; + if(qEl) qEl.textContent = firstExample.q; + typeSql(firstExample.sql); })();
diff --git a/app/ts/app.ts b/app/ts/app.ts index 1c1160fd..bb1c1a8a 100644 --- a/app/ts/app.ts +++ b/app/ts/app.ts @@ -2,10 +2,10 @@ * Main application entry point (TypeScript) */ -import { DOM } from './modules/config.js'; -import { initChat } from './modules/messages.js'; -import { sendMessage, pauseRequest } from './modules/chat.js'; -import { loadGraphs, handleFileUpload, onGraphChange } from './modules/graphs.js'; +import { DOM } from './modules/config'; +import { initChat } from './modules/messages'; +import { sendMessage, pauseRequest } from './modules/chat'; +import { loadGraphs, handleFileUpload, onGraphChange } from './modules/graphs'; import { toggleContainer, showResetConfirmation, @@ -16,9 +16,9 @@ import { setupToolbar, handleWindowResize, setupCustomDropdown -} from './modules/ui.js'; -import { setupAuthenticationModal, setupDatabaseModal } from './modules/modals.js'; -import { showGraph } from './modules/schema.js'; +} from './modules/ui'; +import { setupAuthenticationModal, setupDatabaseModal } from './modules/modals'; +import { showGraph } from './modules/schema'; async function loadAndShowGraph(selected: string | undefined) { if (!selected) return; diff --git a/app/ts/modules/chat.ts b/app/ts/modules/chat.ts index c6b9a831..a9cd41b4 100644 --- a/app/ts/modules/chat.ts +++ b/app/ts/modules/chat.ts @@ -2,8 +2,8 @@ * Chat API and messaging functionality (TypeScript) */ -import { DOM, state, MESSAGE_DELIMITER } from './config.js'; -import { addMessage, removeLoadingMessage, moveLoadingMessageToBottom } from './messages.js'; +import { DOM, state, MESSAGE_DELIMITER } from './config'; +import { addMessage, removeLoadingMessage, moveLoadingMessageToBottom } from './messages'; export async function sendMessage() { const message = (DOM.messageInput?.value || '').trim(); diff --git a/app/ts/modules/graphs.ts b/app/ts/modules/graphs.ts index e79ea758..4eb2cbe8 100644 --- a/app/ts/modules/graphs.ts +++ b/app/ts/modules/graphs.ts @@ -2,8 +2,8 @@ * Graph loading and management functionality (TypeScript) */ -import { DOM } from './config.js'; -import { addMessage, initChat } from './messages.js'; +import { DOM } from './config'; +import { addMessage, initChat } from './messages'; export function loadGraphs() { const isAuthenticated = (window as any).isAuthenticated !== undefined ? (window as any).isAuthenticated : false; diff --git a/app/ts/modules/messages.ts b/app/ts/modules/messages.ts index 192484ad..6f26c5a0 100644 --- a/app/ts/modules/messages.ts +++ b/app/ts/modules/messages.ts @@ -2,7 +2,7 @@ * Message handling and UI functions (TypeScript) */ -import { DOM, state } from './config.js'; +import { DOM, state } from './config'; export function addMessage( message: string, diff --git a/app/ts/modules/ui.ts b/app/ts/modules/ui.ts index 7f7d7f77..d2729d8f 100644 --- a/app/ts/modules/ui.ts +++ b/app/ts/modules/ui.ts @@ -2,7 +2,7 @@ * UI components and interactions (TypeScript) */ -import { DOM } from './config.js'; +import { DOM } from './config'; export function toggleContainer(container: HTMLElement, onOpen?: () => void) { const isMobile = window.innerWidth <= 768; @@ -45,7 +45,7 @@ export function hideResetConfirmation() { export function handleResetConfirmation() { hideResetConfirmation(); - import('./messages.js').then(({ initChat }) => { + import('./messages').then(({ initChat }) => { initChat(); }); } diff --git a/tests/conftest.py b/tests/conftest.py index 7f05aaf6..a4d97da8 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -8,17 +8,17 @@ @pytest.fixture(scope="session") -def flask_app(): - """Start the Flask application for testing.""" +def fastapi_app(): + """Start the FastAPI application for testing.""" import os # Get the project root directory (parent of tests directory) current_dir = os.path.dirname(os.path.abspath(__file__)) project_root = os.path.dirname(current_dir) - # Start the Flask app using pipenv + # Start the FastAPI app using pipenv process = subprocess.Popen([ - "pipenv", "run", "flask", "--app", "api.index", "run", + "pipenv", "run", "uvicorn", "api.index:app", "--host", "localhost", "--port", "5000" ], cwd=project_root) @@ -33,7 +33,7 @@ def flask_app(): time.sleep(1) else: process.terminate() - raise RuntimeError("Flask app failed to start") + raise RuntimeError("FastAPI app failed to start") yield "http://localhost:5000" @@ -43,9 +43,9 @@ def flask_app(): @pytest.fixture -def app_url(flask_app): +def app_url(fastapi_app): """Provide the base URL for the application.""" - return flask_app + return fastapi_app @pytest.fixture diff --git a/tests/e2e/pages/home_page.py b/tests/e2e/pages/home_page.py index 83476346..8b71ab48 100644 --- a/tests/e2e/pages/home_page.py +++ b/tests/e2e/pages/home_page.py @@ -18,7 +18,7 @@ class HomePage(BasePage): def navigate_to_home(self): """Navigate to the home page.""" - self.navigate_to("/") + self.navigate_to("/chat") self.wait_for_page_load() def is_authenticated(self): diff --git a/tests/e2e/test_api_endpoints.py b/tests/e2e/test_api_endpoints.py index f653ffd3..4a338640 100644 --- a/tests/e2e/test_api_endpoints.py +++ b/tests/e2e/test_api_endpoints.py @@ -27,7 +27,7 @@ def test_static_files(self, app_url): # Test CSS files (if any) response = requests.get(f"{app_url}/static/css/", timeout=10) - assert response.status_code in [405] # Various acceptable responses + assert response.status_code in [403] # Various acceptable responses def test_login_endpoints(self, app_url): """Test login endpoints.""" diff --git a/tests/test_mysql_loader.py b/tests/test_mysql_loader.py index 36001f31..895c374f 100644 --- a/tests/test_mysql_loader.py +++ b/tests/test_mysql_loader.py @@ -101,7 +101,7 @@ def test_is_schema_modifying_query(self): assert MySQLLoader.is_schema_modifying_query("")[0] is False assert MySQLLoader.is_schema_modifying_query(None)[0] is False - @patch('mysql.connector.connect') + @patch('pymysql.connect') def test_connection_error(self, mock_connect): """Test handling of MySQL connection errors.""" # Mock connection failure @@ -112,7 +112,7 @@ def test_connection_error(self, mock_connect): assert success is False assert "Error loading MySQL schema" in message - @patch('mysql.connector.connect') + @patch('pymysql.connect') @patch('api.loaders.mysql_loader.load_to_graph') def test_successful_load(self, mock_load_to_graph, mock_connect): """Test successful MySQL schema loading.""" diff --git a/tests/test_simple_integration.py b/tests/test_simple_integration.py index b2a0cec3..cc103435 100644 --- a/tests/test_simple_integration.py +++ b/tests/test_simple_integration.py @@ -8,7 +8,7 @@ class TestSimpleIntegration: """Simple integration tests using requests.""" def test_app_starts_successfully(self, app_url): - """Test that the Flask application starts and responds.""" + """Test that the FastAPI application starts and responds.""" response = requests.get(app_url, timeout=10) assert response.status_code == 200 @@ -28,4 +28,4 @@ def test_static_files_accessible(self, app_url): # Try to access static directory response = requests.get(f"{app_url}/static/", timeout=10) # Should either return content or various error codes, but app should respond - assert response.status_code in [405] + assert response.status_code in [403]