diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 89a5f907..94891b53 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -30,11 +30,17 @@ jobs: uses: actions/setup-python@v6 with: python-version: ${{ env.PYTHON_VERSION }} + cache: 'pip' + cache-dependency-path: Pipfile.lock # Setup Node.js - uses: actions/setup-node@v6 with: node-version: ${{ env.NODE_VERSION }} + cache: 'npm' + cache-dependency-path: | + package-lock.json + app/package-lock.json # Install pipenv - name: Install pipenv @@ -48,6 +54,14 @@ jobs: - name: Install root dependencies run: npm ci + # Cache Playwright browsers + - name: Cache Playwright browsers + id: playwright-cache + uses: actions/cache@v4 + with: + path: ~/.cache/ms-playwright + key: playwright-browsers-${{ runner.os }}-${{ hashFiles('package-lock.json') }} + # Install Node dependencies (frontend) - name: Install frontend dependencies run: npm ci @@ -61,13 +75,8 @@ jobs: # Start Docker Compose services (test databases) - name: Start test databases run: | - docker compose -f e2e/docker-compose.test.yml up -d - # Wait for databases to be healthy - echo "Waiting for databases to be ready..." - sleep 20 + docker compose -f e2e/docker-compose.test.yml up -d --wait --wait-timeout 120 docker ps -a - # Verify all containers are running - docker compose -f e2e/docker-compose.test.yml ps # Start FalkorDB (Redis graph database) - name: Start FalkorDB @@ -133,9 +142,14 @@ jobs: AZURE_API_BASE: ${{ secrets.AZURE_API_BASE }} AZURE_API_VERSION: ${{ secrets.AZURE_API_VERSION }} - # Install Playwright browsers + # Install Playwright browsers (Chromium only in CI) - name: Install Playwright Browsers - run: npx playwright install --with-deps chromium firefox + if: steps.playwright-cache.outputs.cache-hit != 'true' + run: npx playwright install --with-deps chromium + + - name: Install Playwright system deps + if: steps.playwright-cache.outputs.cache-hit == 'true' + run: npx playwright install-deps chromium # Create auth directory for storage state - name: Create auth directory @@ -148,7 +162,7 @@ jobs: CI: true # Upload test results on failure - - uses: actions/upload-artifact@v6 + - uses: actions/upload-artifact@v7 if: failure() with: name: playwright-report @@ -156,7 +170,7 @@ jobs: retention-days: 30 # Upload test screenshots on failure - - uses: actions/upload-artifact@v6 + - uses: actions/upload-artifact@v7 if: failure() with: name: test-results diff --git a/Pipfile b/Pipfile index 020cb5ca..c1c7fa77 100644 --- a/Pipfile +++ b/Pipfile @@ -4,9 +4,9 @@ verify_ssl = true name = "pypi" [packages] -fastapi = "~=0.131.0" -uvicorn = "~=0.40.0" -litellm = "~=1.80.9" +fastapi = "~=0.135.0" +uvicorn = "~=0.41.0" +litellm = "~=1.82.0" falkordb = "~=1.6.0" psycopg2-binary = "~=2.9.11" pymysql = "~=1.1.0" @@ -22,7 +22,7 @@ fastmcp = ">=2.13.1" [dev-packages] pytest = "~=8.4.2" pylint = "~=4.0.3" -playwright = "~=1.57.0" +playwright = "~=1.58.0" pytest-playwright = "~=0.7.1" pytest-asyncio = "~=1.2.0" diff --git a/Pipfile.lock b/Pipfile.lock index 36252143..a01f66f2 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "87444999c27f9e20ce232a6e0ed25a50d5e6af1a3b102e97aa29fac060e6685b" + "sha256": "1fba6f108fd74d27ba11d829c89ee35b72233b52c5efefd802e877194ff152e8" }, "pipfile-spec": 6, "requires": { @@ -251,11 +251,11 @@ }, "certifi": { "hashes": [ - "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", - "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120" + "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", + "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7" ], "markers": "python_version >= '3.7'", - "version": "==2026.1.4" + "version": "==2026.2.25" }, "cffi": { "hashes": [ @@ -531,11 +531,11 @@ }, "cyclopts": { "hashes": [ - "sha256:ad001986ec403ca1dc1ed20375c439d62ac796295ea32b451dfe25d6696bc71a", - "sha256:eed4d6c76d4391aa796d8fcaabd50e5aad7793261792beb19285f62c5c456c8b" + "sha256:0a891cb55bfd79a3cdce024db8987b33316aba11071e5258c21ac12a640ba9f2", + "sha256:483c4704b953ea6da742e8de15972f405d2e748d19a848a4d61595e8e5360ee5" ], "markers": "python_version >= '3.10'", - "version": "==4.5.4" + "version": "==4.6.0" }, "diskcache": { "hashes": [ @@ -604,21 +604,21 @@ }, "fastapi": { "hashes": [ - "sha256:6531155e52bee2899a932c746c9a8250f210e3c3303a5f7b9f8a808bfe0548ff", - "sha256:ed0e53decccf4459de78837ce1b867cd04fa9ce4579497b842579755d20b405a" + "sha256:31e2ddc78d6406c6f7d5d7b9996a057985e2600fbe7e9ba6ace8205d48dff688", + "sha256:bd37903acf014d1284bda027096e460814dca9699f9dacfe11c275749d949f4d" ], "index": "pypi", "markers": "python_version >= '3.10'", - "version": "==0.131.0" + "version": "==0.135.0" }, "fastmcp": { "hashes": [ - "sha256:71de15ffa4e54baebb78d7031c4c9a042a1ab8d1c0b44a9961b75d65809b67e8", - "sha256:ba463ae51e357fba2bafe513cc97f0a06c9f31220e6584990b7d8bcbf69f0516" + "sha256:6bd73b4a3bab773ee6932df5249dcbcd78ed18365ed0aeeb97bb42702a7198d7", + "sha256:f513d80d4b30b54749fe8950116b1aab843f3c293f5cb971fc8665cb48dbb028" ], "index": "pypi", "markers": "python_version >= '3.10'", - "version": "==3.0.1" + "version": "==3.0.2" }, "fastuuid": { "hashes": [ @@ -706,11 +706,11 @@ }, "filelock": { "hashes": [ - "sha256:011a5644dc937c22699943ebbfc46e969cdde3e171470a6e40b9533e5a72affa", - "sha256:426e9a4660391f7f8a810d71b0555bce9008b0a1cc342ab1f6947d37639e002d" + "sha256:5ccf8069f7948f494968fc0713c10e5c182a9c9d9eef3a636307a20c2490f047", + "sha256:8f00faf3abf9dc730a1ffe9c354ae5c04e079ab7d3a683b7c32da5dd05f26af3" ], "markers": "python_version >= '3.10'", - "version": "==3.24.3" + "version": "==3.25.0" }, "frozenlist": { "hashes": [ @@ -861,73 +861,6 @@ "markers": "python_version >= '3.10' and python_version < '4'", "ref": "6e3c54ae2891e10a24d698a2f0733d65d148edf4" }, - "grpcio": { - "hashes": [ - "sha256:02b82dcd2fa580f5e82b4cf62ecde1b3c7cc9ba27b946421200706a6e5acaf85", - "sha256:07eb016ea7444a22bef465cce045512756956433f54450aeaa0b443b8563b9ca", - "sha256:09fbd4bcaadb6d8604ed1504b0bdf7ac18e48467e83a9d930a70a7fefa27e862", - "sha256:0fa9943d4c7f4a14a9a876153a4e8ee2bb20a410b65c09f31510b2a42271f41b", - "sha256:13937b28986f45fee342806b07c6344db785ad74a549ebcb00c659142973556f", - "sha256:15f6e636d1152667ddb4022b37534c161c8477274edb26a0b65b215dd0a81e97", - "sha256:1a56bf3ee99af5cf32d469de91bf5de79bdac2e18082b495fc1063ea33f4f2d0", - "sha256:263307118791bc350f4642749a9c8c2d13fec496228ab11070973e568c256bfd", - "sha256:27b5cb669603efb7883a882275db88b6b5d6b6c9f0267d5846ba8699b7ace338", - "sha256:27c625532d33ace45d57e775edf1982e183ff8641c72e4e91ef7ba667a149d72", - "sha256:2b7ad2981550ce999e25ce3f10c8863f718a352a2fd655068d29ea3fd37b4907", - "sha256:2c473b54ef1618f4fb85e82ff4994de18143b74efc088b91b5a935a3a45042ba", - "sha256:34b6cb16f4b67eeb5206250dc5b4d5e8e3db939535e58efc330e4c61341554bd", - "sha256:36aeff5ba8aaf70ceb2cbf6cbba9ad6beef715ad744841f3e0cd977ec02e5966", - "sha256:389b77484959bdaad6a2b7dda44d7d1228381dd669a03f5660392aa0e9385b22", - "sha256:39d21fd30d38a5afb93f0e2e71e2ec2bd894605fb75d41d5a40060c2f98f8d11", - "sha256:39da1680d260c0c619c3b5fa2dc47480ca24d5704c7a548098bca7de7f5dd17f", - "sha256:3a8aa79bc6e004394c0abefd4b034c14affda7b66480085d87f5fbadf43b593b", - "sha256:409bfe22220889b9906739910a0ee4c197a967c21b8dd14b4b06dd477f8819ce", - "sha256:41e4605c923e0e9a84a2718e4948a53a530172bfaf1a6d1ded16ef9c5849fca2", - "sha256:4393bef64cf26dc07cd6f18eaa5170ae4eebaafd4418e7e3a59ca9526a6fa30b", - "sha256:43b930cf4f9c4a2262bb3e5d5bc40df426a72538b4f98e46f158b7eb112d2d70", - "sha256:4b8d7fda614cf2af0f73bbb042f3b7fee2ecd4aea69ec98dbd903590a1083529", - "sha256:4d50329b081c223d444751076bb5b389d4f06c2b32d51b31a1e98172e6cecfb9", - "sha256:5380268ab8513445740f1f77bd966d13043d07e2793487e61fd5b5d0935071eb", - "sha256:5572c5dd1e43dbb452b466be9794f77e3502bdb6aa6a1a7feca72c98c5085ca7", - "sha256:559f58b6823e1abc38f82e157800aff649146f8906f7998c356cd48ae274d512", - "sha256:5ce1855e8cfc217cdf6bcfe0cf046d7cf81ddcc3e6894d6cfd075f87a2d8f460", - "sha256:656a5bd142caeb8b1efe1fe0b4434ecc7781f44c97cfc7927f6608627cf178c0", - "sha256:716a544969660ed609164aff27b2effd3ff84e54ac81aa4ce77b1607ca917d22", - "sha256:75fa92c47d048d696f12b81a775316fca68385ffc6e6cb1ed1d76c8562579f74", - "sha256:7e836778c13ff70edada16567e8da0c431e8818eaae85b80d11c1ba5782eccbb", - "sha256:849cc62eb989bc3be5629d4f3acef79be0d0ff15622201ed251a86d17fef6494", - "sha256:86edb3966778fa05bfdb333688fde5dc9079f9e2a9aa6a5c42e9564b7656ba04", - "sha256:888ceb7821acd925b1c90f0cdceaed1386e69cfe25e496e0771f6c35a156132f", - "sha256:8942bdfc143b467c264b048862090c4ba9a0223c52ae28c9ae97754361372e42", - "sha256:8991c2add0d8505178ff6c3ae54bd9386279e712be82fa3733c54067aae9eda1", - "sha256:8e1fcb419da5811deb47b7749b8049f7c62b993ba17822e3c7231e3e0ba65b79", - "sha256:8f27683ca68359bd3f0eb4925824d71e538f84338b3ae337ead2ae43977d7541", - "sha256:917047c19cd120b40aab9a4b8a22e9ce3562f4a1343c0d62b3cd2d5199da3d67", - "sha256:99550e344482e3c21950c034f74668fccf8a546d50c1ecb4f717543bbdc071ba", - "sha256:9a00992d6fafe19d648b9ccb4952200c50d8e36d0cce8cf026c56ed3fdc28465", - "sha256:9dee66d142f4a8cca36b5b98a38f006419138c3c89e72071747f8fca415a6d8f", - "sha256:a40515b69ac50792f9b8ead260f194ba2bb3285375b6c40c7ff938f14c3df17d", - "sha256:a6afd191551fd72e632367dfb083e33cd185bf9ead565f2476bba8ab864ae496", - "sha256:b071dccac245c32cd6b1dd96b722283b855881ca0bf1c685cf843185f5d5d51e", - "sha256:b2acd83186305c0802dbc4d81ed0ec2f3e8658d7fde97cfba2f78d7372f05b89", - "sha256:b5d5881d72a09b8336a8f874784a8eeffacde44a7bc1a148bce5a0243a265ef0", - "sha256:ca6aebae928383e971d5eace4f1a217fd7aadaf18d5ddd3163d80354105e9068", - "sha256:cd26048d066b51f39fe9206e2bcc2cea869a5e5b2d13c8d523f4179193047ebd", - "sha256:d101fe49b1e0fb4a7aa36ed0c3821a0f67a5956ef572745452d2cd790d723a3f", - "sha256:d6fb962947e4fe321eeef3be1ba5ba49d32dea9233c825fcbade8e858c14aaf4", - "sha256:db681513a1bdd879c0b24a5a6a70398da5eaaba0e077a306410dc6008426847a", - "sha256:e2a6b33d1050dce2c6f563c5caf7f7cbeebf7fba8cde37ffe3803d50526900d1", - "sha256:e49e720cd6b092504ec7bb2f60eb459aaaf4ce0e5fe20521c201b179e93b5d5d", - "sha256:e840405a3f1249509892be2399f668c59b9d492068a2cf326d661a8c79e5e747", - "sha256:ebeec1383aed86530a5f39646984e92d6596c050629982ac54eeb4e2f6ead668", - "sha256:f81816faa426da461e9a597a178832a351d6f1078102590a4b32c77d251b71eb", - "sha256:f8759a1347f3b4f03d9a9d4ce8f9f31ad5e5d0144ba06ccfb1ffaeb0ba4c1e20", - "sha256:ff7de398bb3528d44d17e6913a7cfe639e3b15c65595a71155322df16978c5e1", - "sha256:ffbb760df1cd49e0989f9826b2fd48930700db6846ac171eaff404f3cfbe5c28" - ], - "markers": "python_version >= '3.9'", - "version": "==1.78.1" - }, "h11": { "hashes": [ "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", @@ -938,31 +871,34 @@ }, "hf-xet": { "hashes": [ - "sha256:10bfab528b968c70e062607f663e21e34e2bba349e8038db546646875495179e", - "sha256:210d577732b519ac6ede149d2f2f34049d44e8622bf14eb3d63bbcd2d4b332dc", - "sha256:27df617a076420d8845bea087f59303da8be17ed7ec0cd7ee3b9b9f579dff0e4", - "sha256:293a7a3787e5c95d7be1857358a9130694a9c6021de3f27fa233f37267174382", - "sha256:29c8fc913a529ec0a91867ce3d119ac1aac966e098cf49501800c870328cc090", - "sha256:2a212e842647b02eb6a911187dc878e79c4aa0aa397e88dd3b26761676e8c1f8", - "sha256:30e06daccb3a7d4c065f34fc26c14c74f4653069bb2b194e7f18f17cbe9939c0", - "sha256:3651fd5bfe0281951b988c0facbe726aa5e347b103a675f49a3fa8144c7968fd", - "sha256:46740d4ac024a7ca9b22bebf77460ff43332868b661186a8e46c227fdae01848", - "sha256:4c1428c9ae73ec0939410ec73023c4f842927f39db09b063b9482dac5a3bb737", - "sha256:66e159cbfcfbb29f920db2c09ed8b660eb894640d284f102ada929b6e3dc410a", - "sha256:6de1fc44f58f6dd937956c8d304d8c2dea264c80680bcfa61ca4a15e7b76780f", - "sha256:7d40b18769bb9a8bc82a9ede575ce1a44c75eb80e7375a01d76259089529b5dc", - "sha256:9c91d5ae931510107f148874e9e2de8a16052b6f1b3ca3c1b12f15ccb491390f", - "sha256:a55558084c16b09b5ed32ab9ed38421e2d87cf3f1f89815764d1177081b99865", - "sha256:a8c27070ca547293b6890c4bf389f713f80e8c478631432962bb7f4bc0bd7d7f", - "sha256:b70218dd548e9840224df5638fdc94bd033552963cfa97f9170829381179c813", - "sha256:cd3a6027d59cfb60177c12d6424e31f4b5ff13d8e3a1247b3a584bf8977e6df5", - "sha256:ceeefcd1b7aed4956ae8499e2199607765fbd1c60510752003b6cc0b8413b649", - "sha256:d06fa97c8562fb3ee7a378dd9b51e343bc5bc8190254202c9771029152f5e08c", - "sha256:e6584a52253f72c9f52f9e549d5895ca7a471608495c4ecaa6cc73dba2b24d69", - "sha256:f182f264ed2acd566c514e45da9f2119110e48a87a327ca271027904c70c5832" + "sha256:06b724a361f670ae557836e57801b82c75b534812e351a87a2c739f77d1e0635", + "sha256:06cdbde243c85f39a63b28e9034321399c507bcd5e7befdd17ed2ccc06dfe14e", + "sha256:1c88fbd90ad0d27c46b77a445f0a436ebaa94e14965c581123b68b1c52f5fd30", + "sha256:211f30098512d95e85ad03ae63bd7dd2c4df476558a5095d09f9e38e78cbf674", + "sha256:305f5489d7241a47e0458ef49334be02411d1d0f480846363c1c8084ed9916f7", + "sha256:3155a02e083aa21fd733a7485c7c36025e49d5975c8d6bda0453d224dd0b0ac4", + "sha256:31612ba0629046e425ba50375685a2586e11fb9144270ebabd75878c3eaf6378", + "sha256:335a8f36c55fd35a92d0062f4e9201b4015057e62747b7e7001ffb203c0ee1d2", + "sha256:35b855024ca37f2dd113ac1c08993e997fbe167b9d61f9ef66d3d4f84015e508", + "sha256:433c77c9f4e132b562f37d66c9b22c05b5479f243a1f06a120c1c06ce8b1502a", + "sha256:4a6817c41de7c48ed9270da0b02849347e089c5ece9a0e72ae4f4b3a57617f82", + "sha256:4bc995d6c41992831f762096020dc14a65fdf3963f86ffed580b596d04de32e3", + "sha256:7c2a054a97c44e136b1f7f5a78f12b3efffdf2eed3abc6746fc5ea4b39511633", + "sha256:83d8ec273136171431833a6957e8f3af496bee227a0fe47c7b8b39c106d1749a", + "sha256:91b1dc03c31cbf733d35dc03df7c5353686233d86af045e716f1e0ea4a2673cf", + "sha256:9298b47cce6037b7045ae41482e703c471ce36b52e73e49f71226d2e8e5685a1", + "sha256:959083c89dee30f7d6f890b36cdadda823386c4de63b1a30384a75bfd2ae995d", + "sha256:a85d3d43743174393afe27835bde0cd146e652b5fcfdbcd624602daef2ef3259", + "sha256:c1980abfb68ecf6c1c7983379ed7b1e2b49a1aaf1a5aca9acc7d48e5e2e0a961", + "sha256:c1ae4d3a716afc774e66922f3cac8206bfa707db13f6a7e62dfff74bfc95c9a8", + "sha256:c34e2c7aefad15792d57067c1c89b2b02c1bbaeabd7f8456ae3d07b4bbaf4094", + "sha256:cfa760888633b08c01b398d212ce7e8c0d7adac6c86e4b20dfb2397d8acd78ee", + "sha256:d6dbdf231efac0b9b39adcf12a07f0c030498f9212a18e8c50224d0e84ab803d", + "sha256:e130ee08984783d12717444e538587fa2119385e5bd8fc2bb9f930419b73a7af", + "sha256:f93b7595f1d8fefddfede775c18b5c9256757824f7f6832930b49858483cd56f" ], "markers": "python_version >= '3.8'", - "version": "==1.2.0" + "version": "==1.3.2" }, "httpcore": { "hashes": [ @@ -990,11 +926,11 @@ }, "huggingface-hub": { "hashes": [ - "sha256:9931d075fb7a79af5abc487106414ec5fba2c0ae86104c0c62fd6cae38873d18", - "sha256:b41131ec35e631e7383ab26d6146b8d8972abc8b6309b963b306fbcca87f5ed5" + "sha256:c9c0b3ab95a777fc91666111f3b3ede71c0cdced3614c553a64e98920585c4ee", + "sha256:f281838db29265880fb543de7a23b0f81d3504675de82044307ea3c6c62f799d" ], "markers": "python_full_version >= '3.9.0'", - "version": "==1.4.1" + "version": "==1.5.0" }, "idna": { "hashes": [ @@ -1189,11 +1125,11 @@ }, "jsonschema-path": { "hashes": [ - "sha256:727d8714158c41327908677e6119f9db9d5e0f486d4cc79ca4b4016eee2f33e8", - "sha256:ffca3bd37f66364ae3afeaa2804d6078a9ab3b9359ade4dd9923aabbbd475e71" + "sha256:5f5ff183150030ea24bb51cf1ddac9bf5dbf030272e2792a7ffe8262f7eea2a5", + "sha256:9c3d88e727cc4f1a88e51dbbed4211dbcd815d27799d2685efd904435c3d39e7" ], "markers": "python_version >= '3.10' and python_full_version < '4.0.0'", - "version": "==0.4.1" + "version": "==0.4.2" }, "jsonschema-specifications": { "hashes": [ @@ -1213,12 +1149,12 @@ }, "litellm": { "hashes": [ - "sha256:514ae407e488bccbe0c33a280ed88495d6f079c1dbfe745eb374d898c94b0ac3", - "sha256:52b23a21910a16820e6ea4b982dc81d27c993c1054ce148c56b251e9b89d89df" + "sha256:5496b5d4532cccdc7a095c21cbac4042f7662021c57bc1d17be4e39838929e80", + "sha256:d388f52447daccbcaafa19a3e68d17b75f1374b5bf2cde680d65e1cd86e50d22" ], "index": "pypi", "markers": "python_version >= '3.9' and python_version < '4.0'", - "version": "==1.80.17" + "version": "==1.82.0" }, "markdown-it-py": { "hashes": [ @@ -1587,11 +1523,11 @@ }, "openai": { "hashes": [ - "sha256:0bc1c775e5b1536c294eded39ee08f8407656537ccc71b1004104fe1602e267c", - "sha256:81b48ce4b8bbb2cc3af02047ceb19561f7b1dc0d4e52d1de7f02abfd15aa59b7" + "sha256:1e5769f540dbd01cb33bc4716a23e67b9d695161a734aff9c5f925e2bf99a673", + "sha256:fed30480d7d6c884303287bde864980a4b137b60553ffbcf9ab4a233b7a73d94" ], "markers": "python_version >= '3.9'", - "version": "==2.21.0" + "version": "==2.24.0" }, "openapi-pydantic": { "hashes": [ @@ -2055,11 +1991,11 @@ }, "python-dotenv": { "hashes": [ - "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", - "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61" + "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", + "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3" ], - "markers": "python_version >= '3.9'", - "version": "==1.2.1" + "markers": "python_version >= '3.10'", + "version": "==1.2.2" }, "python-multipart": { "hashes": [ @@ -2174,123 +2110,123 @@ }, "regex": { "hashes": [ - "sha256:00ec994d7824bf01cd6c7d14c7a6a04d9aeaf7c42a2bc22d2359d715634d539b", - "sha256:015088b8558502f1f0bccd58754835aa154a7a5b0bd9d4c9b7b96ff4ae9ba876", - "sha256:02b9e1b8a7ebe2807cd7bbdf662510c8e43053a23262b9f46ad4fc2dfc9d204e", - "sha256:03d191a9bcf94d31af56d2575210cb0d0c6a054dbcad2ea9e00aa4c42903b919", - "sha256:03d706fbe7dfec503c8c3cb76f9352b3e3b53b623672aa49f18a251a6c71b8e6", - "sha256:0782bd983f19ac7594039c9277cd6f75c89598c1d72f417e4d30d874105eb0c7", - "sha256:0c10869d18abb759a3317c757746cc913d6324ce128b8bcec99350df10419f18", - "sha256:0d0e72703c60d68b18b27cde7cdb65ed2570ae29fb37231aa3076bfb6b1d1c13", - "sha256:0d9f81806abdca3234c3dd582b8a97492e93de3602c8772013cb4affa12d1668", - "sha256:11c138febb40546ff9e026dbbc41dc9fb8b29e61013fa5848ccfe045f5b23b83", - "sha256:127ea69273485348a126ebbf3d6052604d3c7da284f797bba781f364c0947d47", - "sha256:12e86a01594031abf892686fcb309b041bf3de3d13d99eb7e2b02a8f3c687df1", - "sha256:17648e1a88e72d88641b12635e70e6c71c5136ba14edba29bf8fc6834005a265", - "sha256:196553ba2a2f47904e5dc272d948a746352e2644005627467e055be19d73b39e", - "sha256:1e7a08622f7d51d7a068f7e4052a38739c412a3e74f55817073d2e2418149619", - "sha256:2905ff4a97fad42f2d0834d8b1ea3c2f856ec209837e458d71a061a7d05f9f01", - "sha256:294c0fb2e87c6bcc5f577c8f609210f5700b993151913352ed6c6af42f30f95f", - "sha256:2c1693ca6f444d554aa246b592355b5cec030ace5a2729eae1b04ab6e853e768", - "sha256:2cb00aabd96b345d56a8c2bc328c8d6c4d29935061e05078bf1f02302e12abf5", - "sha256:2f914ae8c804c8a8a562fe216100bc156bfb51338c1f8d55fe32cf407774359a", - "sha256:2fedd459c791da24914ecc474feecd94cf7845efb262ac3134fe27cbd7eda799", - "sha256:311fcccb76af31be4c588d5a17f8f1a059ae8f4b097192896ebffc95612f223a", - "sha256:31a5f561eb111d6aae14202e7043fb0b406d3c8dddbbb9e60851725c9b38ab1d", - "sha256:31aefac2506967b7dd69af2c58eca3cc8b086d4110b66d6ac6e9026f0ee5b697", - "sha256:38d88c6ed4a09ed61403dbdf515d969ccba34669af3961ceb7311ecd0cef504a", - "sha256:3a039474986e7a314ace6efb9ce52f5da2bdb80ac4955358723d350ec85c32ad", - "sha256:3aa0944f1dc6e92f91f3b306ba7f851e1009398c84bfd370633182ee4fc26a64", - "sha256:4071209fd4376ab5ceec72ad3507e9d3517c59e38a889079b98916477a871868", - "sha256:4146dc576ea99634ae9c15587d0c43273b4023a10702998edf0fa68ccb60237a", - "sha256:4192464fe3e6cb0ef6751f7d3b16f886d8270d359ed1590dd555539d364f0ff7", - "sha256:43cdde87006271be6963896ed816733b10967baaf0e271d529c82e93da66675b", - "sha256:4584a3ee5f257b71e4b693cc9be3a5104249399f4116fe518c3f79b0c6fc7083", - "sha256:46e69a4bf552e30e74a8aa73f473c87efcb7f6e8c8ece60d9fd7bf13d5c86f02", - "sha256:49cef7bb2a491f91a8869c7cdd90babf0a417047ab0bf923cd038ed2eab2ccb8", - "sha256:4a02faea614e7fdd6ba8b3bec6c8e79529d356b100381cec76e638f45d12ca04", - "sha256:50f1ee9488dd7a9fda850ec7c68cad7a32fa49fd19733f5403a3f92b451dcf73", - "sha256:516ee067c6c721d0d0bfb80a2004edbd060fffd07e456d4e1669e38fe82f922e", - "sha256:52136f5b71f095cb74b736cc3a1b578030dada2e361ef2f07ca582240b703946", - "sha256:5390b130cce14a7d1db226a3896273b7b35be10af35e69f1cca843b6e5d2bb2d", - "sha256:59a7a5216485a1896c5800e9feb8ff9213e11967b482633b6195d7da11450013", - "sha256:5a8f28dd32a4ce9c41758d43b5b9115c1c497b4b1f50c457602c1d571fa98ce1", - "sha256:5b81ff4f9cad99f90c807a00c5882fbcda86d8b3edd94e709fb531fc52cb3d25", - "sha256:5df947cabab4b643d4791af5e28aecf6bf62e6160e525651a12eba3d03755e6b", - "sha256:5e3a31e94d10e52a896adaa3adf3621bd526ad2b45b8c2d23d1bbe74c7423007", - "sha256:5e56c669535ac59cbf96ca1ece0ef26cb66809990cda4fa45e1e32c3b146599e", - "sha256:5ec1d7c080832fdd4e150c6f5621fe674c70c63b3ae5a4454cebd7796263b175", - "sha256:6380f29ff212ec922b6efb56100c089251940e0526a0d05aa7c2d9b571ddf2fe", - "sha256:64128549b600987e0f335c2365879895f860a9161f283b14207c800a6ed623d3", - "sha256:654dc41a5ba9b8cc8432b3f1aa8906d8b45f3e9502442a07c2f27f6c63f85db5", - "sha256:655f553a1fa3ab8a7fd570eca793408b8d26a80bfd89ed24d116baaf13a38969", - "sha256:66e6a43225ff1064f8926adbafe0922b370d381c3330edaf9891cade52daa790", - "sha256:676c4e6847a83a1d5732b4ed553881ad36f0a8133627bb695a89ecf3571499d3", - "sha256:6bc25d7e15f80c9dc7853cbb490b91c1ec7310808b09d56bd278fe03d776f4f6", - "sha256:6c8fb3b19652e425ff24169dad3ee07f99afa7996caa9dfbb3a9106cd726f49a", - "sha256:6fb8cb09b10e38f3ae17cc6dc04a1df77762bd0351b6ba9041438e7cc85ec310", - "sha256:7187fdee1be0896c1499a991e9bf7c78e4b56b7863e7405d7bb687888ac10c4b", - "sha256:74ff212aa61532246bb3036b3dfea62233414b0154b8bc3676975da78383cac3", - "sha256:75472631eee7898e16a8a20998d15106cb31cfde21cdf96ab40b432a7082af06", - "sha256:77cfd6b5e7c4e8bf7a39d243ea05882acf5e3c7002b0ef4756de6606893b0ecd", - "sha256:78af1e499cab704131f6f4e2f155b7f54ce396ca2acb6ef21a49507e4752e0be", - "sha256:79014115e6fdf18fd9b32e291d58181bf42d4298642beaa13fd73e69810e4cb6", - "sha256:790dbf87b0361606cb0d79b393c3e8f4436a14ee56568a7463014565d97da02a", - "sha256:80caaa1ddcc942ec7be18427354f9d58a79cee82dea2a6b3d4fd83302e1240d7", - "sha256:80d31c3f1fe7e4c6cd1831cd4478a0609903044dfcdc4660abfe6fb307add7f0", - "sha256:82336faeecac33297cd42857c3b36f12b91810e3fdd276befdd128f73a2b43fa", - "sha256:8457c1bc10ee9b29cdfd897ccda41dce6bde0e9abd514bcfef7bcd05e254d411", - "sha256:8497421099b981f67c99eba4154cf0dfd8e47159431427a11cfb6487f7791d9e", - "sha256:8abe671cf0f15c26b1ad389bf4043b068ce7d3b1c5d9313e12895f57d6738555", - "sha256:8dbff048c042beef60aa1848961384572c5afb9e8b290b0f1203a5c42cf5af65", - "sha256:8df08decd339e8b3f6a2eb5c05c687fe9d963ae91f352bc57beb05f5b2ac6879", - "sha256:8e6e77cd92216eb489e21e5652a11b186afe9bdefca8a2db739fd6b205a9e0a4", - "sha256:8edda06079bd770f7f0cf7f3bba1a0b447b96b4a543c91fe0c142d034c166161", - "sha256:93b16a18cadb938f0f2306267161d57eb33081a861cee9ffcd71e60941eb5dfc", - "sha256:93d881cab5afdc41a005dba1524a40947d6f7a525057aa64aaf16065cf62faa9", - "sha256:965d59792f5037d9138da6fed50ba943162160443b43d4895b182551805aff9c", - "sha256:997862c619994c4a356cb7c3592502cbd50c2ab98da5f61c5c871f10f22de7e5", - "sha256:9cbc69eae834afbf634f7c902fc72ff3e993f1c699156dd1af1adab5d06b7fe7", - "sha256:9dadc10d1c2bbb1326e572a226d2ec56474ab8aab26fdb8cf19419b372c349a9", - "sha256:9e6693b8567a59459b5dda19104c4a4dbbd4a1c78833eacc758796f2cfef1854", - "sha256:9fff45852160960f29e184ec8a5be5ab4063cfd0b168d439d1fc4ac3744bf29e", - "sha256:a032bc01a4bc73fc3cadba793fce28eb420da39338f47910c59ffcc11a5ba5ef", - "sha256:a09ae430e94c049dc6957f6baa35ee3418a3a77f3c12b6e02883bd80a2b679b0", - "sha256:a178df8ec03011153fbcd2c70cb961bc98cbbd9694b28f706c318bee8927c3db", - "sha256:ab780092b1424d13200aa5a62996e95f65ee3db8509be366437439cdc0af1a9f", - "sha256:b5100acb20648d9efd3f4e7e91f51187f95f22a741dcd719548a6cf4e1b34b3f", - "sha256:b9ab8dec42afefa6314ea9b31b188259ffdd93f433d77cad454cd0b8d235ce1c", - "sha256:bcf57d30659996ee5c7937999874504c11b5a068edc9515e6a59221cc2744dd1", - "sha256:c0761d7ae8d65773e01515ebb0b304df1bf37a0a79546caad9cbe79a42c12af7", - "sha256:c0924c64b082d4512b923ac016d6e1dcf647a3560b8a4c7e55cbbd13656cb4ed", - "sha256:c13228fbecb03eadbfd8f521732c5fda09ef761af02e920a3148e18ad0e09968", - "sha256:c1665138776e4ac1aa75146669236f7a8a696433ec4e525abf092ca9189247cc", - "sha256:c227f2922153ee42bbeb355fd6d009f8c81d9d7bdd666e2276ce41f53ed9a743", - "sha256:c7e121a918bbee3f12ac300ce0a0d2f2c979cf208fb071ed8df5a6323281915c", - "sha256:ccaaf9b907ea6b4223d5cbf5fa5dff5f33dc66f4907a25b967b8a81339a6e332", - "sha256:cce8027010d1ffa3eb89a0b19621cdc78ae548ea2b49fea1f7bfb3ea77064c2b", - "sha256:cdc0a80f679353bd68450d2a42996090c30b2e15ca90ded6156c31f1a3b63f3b", - "sha256:d00c95a2b6bfeb3ea1cb68d1751b1dfce2b05adc2a72c488d77a780db06ab867", - "sha256:d792b84709021945597e05656aac059526df4e0c9ef60a0eaebb306f8fafcaa8", - "sha256:d793c5b4d2b4c668524cd1651404cfc798d40694c759aec997e196fe9729ec60", - "sha256:d89f85a5ccc0cec125c24be75610d433d65295827ebaf0d884cbe56df82d4774", - "sha256:d96162140bb819814428800934c7b71b7bffe81fb6da2d6abc1dcca31741eca3", - "sha256:db5fd91eec71e7b08de10011a2223d0faa20448d4e1380b9daa179fa7bf58906", - "sha256:db970bcce4d63b37b3f9eb8c893f0db980bbf1d404a1d8d2b17aa8189de92c53", - "sha256:dbb240c81cfed5d4a67cb86d7676d9f7ec9c3f186310bec37d8a1415210e111e", - "sha256:e561dd47a85d2660d3d3af4e6cb2da825cf20f121e577147963f875b83d32786", - "sha256:e581f75d5c0b15669139ca1c2d3e23a65bb90e3c06ba9d9ea194c377c726a904", - "sha256:e689fed279cbe797a6b570bd18ff535b284d057202692c73420cb93cca41aa32", - "sha256:ea8dfc99689240e61fb21b5fc2828f68b90abf7777d057b62d3166b7c1543c4c", - "sha256:eb20c11aa4c3793c9ad04c19a972078cdadb261b8429380364be28e867a843f2", - "sha256:ec661807ffc14c8d14bb0b8c1bb3d5906e476bc96f98b565b709d03962ee4dd4", - "sha256:f374366ed35673ea81b86a8859c457d4fae6ba092b71024857e9e237410c7404", - "sha256:f5a37a17d110f9d5357a43aa7e3507cb077bf3143d1c549a45c4649e90e40a70", - "sha256:f9417fd853fcd00b7d55167e692966dd12d95ba1a88bf08a62002ccd85030790", - "sha256:fdbade8acba71bb45057c2b72f477f0b527c4895f9c83e6cfc30d4a006c21726" + "sha256:00945d007fd74a9084d2ab79b695b595c6b7ba3698972fadd43e23230c6979c1", + "sha256:00f2b8d9615aa165fdff0a13f1a92049bfad555ee91e20d246a51aa0b556c60a", + "sha256:01d65fd24206c8e1e97e2e31b286c59009636c022eb5d003f52760b0f42155d4", + "sha256:02473c954af35dd2defeb07e44182f5705b30ea3f351a7cbffa9177beb14da5d", + "sha256:03a83cc26aa2acda6b8b9dfe748cf9e84cbd390c424a1de34fdcef58961a297a", + "sha256:09500be324f49b470d907b3ef8af9afe857f5cca486f853853f7945ddbf75911", + "sha256:0b1d2b07614d95fa2bf8a63fd1e98bd8fa2b4848dc91b1efbc8ba219fdd73952", + "sha256:0d25a10811de831c2baa6aef3c0be91622f44dd8d31dd12e69f6398efb15e48b", + "sha256:0d5bef2031cbf38757a0b0bc4298bb4824b6332d28edc16b39247228fbdbad97", + "sha256:10d28e19bd4888e4abf43bd3925f3c134c52fdf7259219003588a42e24c2aa25", + "sha256:180e08a435a0319e6a4821c3468da18dc7001987e1c17ae1335488dfe7518dd8", + "sha256:195237dc327858a7721bf8b0bbbef797554bc13563c3591e91cd0767bacbe359", + "sha256:19a9c9e0a8f24f39d575a6a854d516b48ffe4cbdcb9de55cb0570a032556ecff", + "sha256:1c2c95e1a2b0f89d01e821ff4de1be4b5d73d1f4b0bf679fa27c1ad8d2327f1a", + "sha256:1d367257cd86c1cbb97ea94e77b373a0bbc2224976e247f173d19e8f18b4afa7", + "sha256:1e496956106fd59ba6322a8ea17141a27c5040e5ee8f9433ae92d4e5204462a0", + "sha256:1f8b17be5c27a684ea6759983c13506bd77bfc7c0347dff41b18ce5ddd2ee09a", + "sha256:2234059cfe33d9813a3677ef7667999caea9eeaa83fef98eb6ce15c6cf9e0215", + "sha256:25b6eb660c5cf4b8c3407a1ed462abba26a926cc9965e164268a3267bcc06a43", + "sha256:2954379dd20752e82d22accf3ff465311cbb2bac6c1f92c4afd400e1757f7451", + "sha256:2afa673660928d0b63d84353c6c08a8a476ddfc4a47e11742949d182e6863ce8", + "sha256:2b2b23587b26496ff5fd40df4278becdf386813ec00dc3533fa43a4cf0e2ad3c", + "sha256:2fb950ac1d88e6b6a9414381f403797b236f9fa17e1eee07683af72b1634207b", + "sha256:3935174fa4d9f70525a4367aaff3cb8bc0548129d114260c29d9dfa4a5b41692", + "sha256:39bb5727650b9a0275c6a6690f9bb3fe693a7e6cc5c3155b1240aedf8926423e", + "sha256:3b24bd7e9d85dc7c6a8bd2aa14ecd234274a0248335a02adeb25448aecdd420d", + "sha256:4390c365fd2d45278f45afd4673cb90f7285f5701607e3ad4274df08e36140ae", + "sha256:481df4623fa4969c8b11f3433ed7d5e3dc9cec0f008356c3212b3933fb77e3d8", + "sha256:4f5c0b182ad4269e7381b7c27fdb0408399881f7a92a4624fd5487f2971dfc11", + "sha256:50c2fc924749543e0eacc93ada6aeeb3ea5f6715825624baa0dccaec771668ae", + "sha256:511f7419f7afab475fd4d639d4aedfc54205bcb0800066753ef68a59f0f330b5", + "sha256:516604edd17b1c2c3e579cf4e9b25a53bf8fa6e7cedddf1127804d3e0140ca64", + "sha256:52b017b35ac2214d0db5f4f90e303634dc44e4aba4bd6235a27f97ecbe5b0472", + "sha256:5a932ea8ad5d0430351ff9c76c8db34db0d9f53c1d78f06022a21f4e290c5c18", + "sha256:5cdcc17d935c8f9d3f4db5c2ebe2640c332e3822ad5d23c2f8e0228e6947943a", + "sha256:5d10303dd18cedfd4d095543998404df656088240bcfd3cd20a8f95b861f74bd", + "sha256:5e68192bb3a1d6fb2836da24aa494e413ea65853a21505e142e5b1064a595f3d", + "sha256:64e7c6ad614573e0640f271e811a408d79a9e1fe62a46adb602f598df42a818d", + "sha256:6591f281cb44dc13de9585b552cec6fc6cf47fb2fe7a48892295ee9bc4a612f9", + "sha256:69fc560ccbf08a09dc9b52ab69cacfae51e0ed80dc5693078bdc97db2f91ae96", + "sha256:6d63a07e5ec8ce7184452cb00c41c37b49e67dc4f73b2955b5b8e782ea970784", + "sha256:6db7bfae0f8a2793ff1f7021468ea55e2699d0790eb58ee6ab36ae43aa00bc5b", + "sha256:71a911098be38c859ceb3f9a9ce43f4ed9f4c6720ad8684a066ea246b76ad9ff", + "sha256:73cdcdbba8028167ea81490c7f45280113e41db2c7afb65a276f4711fa3bcbff", + "sha256:78454178c7df31372ea737996fb7f36b3c2c92cccc641d251e072478afb4babc", + "sha256:7900157786428a79615a8264dac1f12c9b02957c473c8110c6b1f972dcecaddf", + "sha256:7ab218076eb0944549e7fe74cf0e2b83a82edb27e81cc87411f76240865e04d5", + "sha256:7c1b34dfa72f826f535b20712afa9bb3ba580020e834f3c69866c5bddbf10098", + "sha256:851fa70df44325e1e4cdb79c5e676e91a78147b1b543db2aec8734d2add30ec2", + "sha256:864cdd1a2ef5716b0ab468af40139e62ede1b3a53386b375ec0786bb6783fc05", + "sha256:8710d61737b0c0ce6836b1da7109f20d495e49b3809f30e27e9560be67a257bf", + "sha256:9036b400b20e4858d56d117108d7813ed07bb7803e3eed766675862131135ca6", + "sha256:9185cc63359862a6e80fe97f696e04b0ad9a11c4ac0a4a927f979f611bfe3768", + "sha256:948c12ef30ecedb128903c2c2678b339746eb7c689c5c21957c4a23950c96d15", + "sha256:94d63db12e45a9b9f064bfe4800cefefc7e5f182052e4c1b774d46a40ab1d9bb", + "sha256:96f6269a2882fbb0ee76967116b83679dc628e68eaea44e90884b8d53d833881", + "sha256:97054c55db06ab020342cc0d35d6f62a465fa7662871190175f1ad6c655c028f", + "sha256:98adf340100cbe6fbaf8e6dc75e28f2c191b1be50ffefe292fb0e6f6eefdb0d8", + "sha256:99985a2c277dcb9ccb63f937451af5d65177af1efdeb8173ac55b61095a0a05c", + "sha256:9b65d33a17101569f86d9c5966a8b1d7fbf8afdda5a8aa219301b0a80f58cf7d", + "sha256:9dd450db6458387167e033cfa80887a34c99c81d26da1bf8b0b41bf8c9cac88e", + "sha256:a25c7701e4f7a70021db9aaf4a4a0a67033c6318752146e03d1b94d32006217e", + "sha256:a448af01e3d8031c89c5d902040b124a5e921a25c4e5e07a861ca591ce429341", + "sha256:a5dac14d0872eeb35260a8e30bac07ddf22adc1e3a0635b52b02e180d17c9c7e", + "sha256:a729e47d418ea11d03469f321aaf67cdee8954cde3ff2cf8403ab87951ad10f2", + "sha256:aaffaecffcd2479ce87aa1e74076c221700b7c804e48e98e62500ee748f0f550", + "sha256:b059e71ec363968671693a78c5053bd9cb2fe410f9b8e4657e88377ebd603a2e", + "sha256:b387a0d092dac157fb026d737dde35ff3e49ef27f285343e7c6401851239df27", + "sha256:b389c61aa28a79c2e0527ac36da579869c2e235a5b208a12c5b5318cda2501d8", + "sha256:b42f7466e32bf15a961cf09f35fa6323cc72e64d3d2c990b10de1274a5da0a59", + "sha256:b49eb78048c6354f49e91e4b77da21257fecb92256b6d599ae44403cab30b05b", + "sha256:b5acd4b6a95f37c3c3828e5d053a7d4edaedb85de551db0153754924cb7c83e3", + "sha256:b8b3f1be1738feadc69f62daa250c933e85c6f34fa378f54a7ff43807c1b9117", + "sha256:b8cf76f1a29f0e99dcfd7aef1551a9827588aae5a737fe31442021165f1920dc", + "sha256:ba55c50f408fb5c346a3a02d2ce0ebc839784e24f7c9684fde328ff063c3cdea", + "sha256:bba2b18d70eeb7b79950f12f633beeecd923f7c9ad6f6bae28e59b4cb3ab046b", + "sha256:bbb882061f742eb5d46f2f1bd5304055be0a66b783576de3d7eef1bed4778a6e", + "sha256:bcb399ed84eabf4282587ba151f2732ad8168e66f1d3f85b1d038868fe547703", + "sha256:bd477d5f79920338107f04aa645f094032d9e3030cc55be581df3d1ef61aa318", + "sha256:bec23c11cbbf09a4df32fe50d57cbdd777bc442269b6e39a1775654f1c95dee2", + "sha256:c0b5ccbb8ffb433939d248707d4a8b31993cb76ab1a0187ca886bf50e96df952", + "sha256:c15af43c72a7fb0c97cbc66fa36a43546eddc5c06a662b64a0cbf30d6ac40944", + "sha256:c7815afb0ca45456613fdaf60ea9c993715511c8d53a83bc468305cbc0ee23c7", + "sha256:cb3b1db8ff6c7b8bf838ab05583ea15230cb2f678e569ab0e3a24d1e8320940b", + "sha256:d0b02e8b7e5874b48ae0f077ecca61c1a6a9f9895e9c6dfb191b55b242862033", + "sha256:d6b08a06976ff4fb0d83077022fde3eca06c55432bb997d8c0495b9a4e9872f4", + "sha256:d6cfe798d8da41bb1862ed6e0cba14003d387c3c0c4a5d45591076ae9f0ce2f8", + "sha256:d8511a01d0e4ee1992eb3ba19e09bc1866fe03f05129c3aec3fdc4cbc77aad3f", + "sha256:dc8ed8c3f41c27acb83f7b6a9eb727a73fc6663441890c5cb3426a5f6a91ce7d", + "sha256:dd8847c4978bc3c7e6c826fb745f5570e518b8459ac2892151ce6627c7bc00d5", + "sha256:de0cf053139f96219ccfabb4a8dd2d217c8c82cb206c91d9f109f3f552d6b43d", + "sha256:dee50f1be42222f89767b64b283283ef963189da0dda4a515aa54a5563c62dec", + "sha256:e1e7b24cb3ae9953a560c563045d1ba56ee4749fbd05cf21ba571069bd7be81b", + "sha256:e59bc8f30414d283ae8ee1617b13d8112e7135cb92830f0ec3688cb29152585a", + "sha256:e61eea47230eba62a31f3e8a0e3164d0f37ef9f40529fb2c79361bc6b53d2a92", + "sha256:e621fb7c8dc147419b28e1702f58a0177ff8308a76fa295c71f3e7827849f5d9", + "sha256:e71dcecaa113eebcc96622c17692672c2d104b1d71ddf7adeda90da7ddeb26fc", + "sha256:e7ce83654d1ab701cb619285a18a8e5a889c1216d746ddc710c914ca5fd71022", + "sha256:e8c8cb2deba42f5ec1ede46374e990f8adc5e6456a57ac1a261b19be6f28e4e6", + "sha256:ec0c608b7a7465ffadb344ed7c987ff2f11ee03f6a130b569aa74d8a70e8333c", + "sha256:ec6f5674c5dc836994f50f1186dd1fafde4be0666aae201ae2fcc3d29d8adf27", + "sha256:edb1b1b3a5576c56f08ac46f108c40333f222ebfd5cf63afdfa3aab0791ebe5b", + "sha256:ef77bdde9c9eba3f7fa5b58084b29bbcc74bcf55fdbeaa67c102a35b5bd7e7cc", + "sha256:f2791948f7c70bb9335a9102df45e93d428f4b8128020d85920223925d73b9e1", + "sha256:f467cb602f03fbd1ab1908f68b53c649ce393fde056628dc8c7e634dab6bfc07", + "sha256:f8ed9a5d4612df9d4de15878f0bc6aa7a268afbe5af21a3fdd97fa19516e978c", + "sha256:fa539be029844c0ce1114762d2952ab6cfdd7c7c9bd72e0db26b94c3c36dcc5a", + "sha256:fb1c4ff62277d87a7335f2c1ea4e0387b8f2b3ad88a64efd9943906aafad4f33", + "sha256:fb4db2f17e6484904f986c5a657cec85574c76b5c5e61c7aae9ffa1bc6224f95", + "sha256:fb66e5245db9652abd7196ace599b04d9c0e4aa7c8f0e2803938377835780081", + "sha256:fc48c500838be6882b32748f60a15229d2dea96e59ef341eaa96ec83538f498d", + "sha256:fcf26c3c6d0da98fada8ae4ef0aa1c3405a431c0a77eb17306d38a89b02adcd7", + "sha256:fd0ce43e71d825b7c0661f9c54d4d74bd97c56c3fd102a8985bcfea48236bacb", + "sha256:fd63453f10d29097cc3dc62d070746523973fb5aa1c66d25f8558bebd47fed61" ], "markers": "python_version >= '3.10'", - "version": "==2026.2.19" + "version": "==2026.2.28" }, "requests": { "hashes": [ @@ -2636,12 +2572,12 @@ }, "uvicorn": { "hashes": [ - "sha256:839676675e87e73694518b5574fd0f24c9d97b46bea16df7b8c05ea1a51071ea", - "sha256:c6c8f55bc8bf13eb6fa9ff87ad62308bbbc33d0b67f84293151efe87e0d5f2ee" + "sha256:09d11cf7008da33113824ee5a1c6422d89fbc2ff476540d69a34c87fab8b571a", + "sha256:29e35b1d2c36a04b9e180d4007ede3bcb32a85fbdfd6c6aeb3f26839de088187" ], "index": "pypi", "markers": "python_version >= '3.10'", - "version": "==0.40.0" + "version": "==0.41.0" }, "watchfiles": { "hashes": [ @@ -2827,139 +2763,137 @@ }, "yarl": { "hashes": [ - "sha256:01e73b85a5434f89fc4fe27dcda2aff08ddf35e4d47bbbea3bdcd25321af538a", - "sha256:029866bde8d7b0878b9c160e72305bbf0a7342bcd20b9999381704ae03308dc8", - "sha256:078278b9b0b11568937d9509b589ee83ef98ed6d561dfe2020e24a9fd08eaa2b", - "sha256:078a8aefd263f4d4f923a9677b942b445a2be970ca24548a8102689a3a8ab8da", - "sha256:07a524d84df0c10f41e3ee918846e1974aba4ec017f990dc735aad487a0bdfdf", - "sha256:088e4e08f033db4be2ccd1f34cf29fe994772fb54cfe004bbf54db320af56890", - "sha256:0b5bcc1a9c4839e7e30b7b30dd47fe5e7e44fb7054ec29b5bb8d526aa1041093", - "sha256:0cf71bf877efeac18b38d3930594c0948c82b64547c1cf420ba48722fe5509f6", - "sha256:0d6e6885777af0f110b0e5d7e5dda8b704efed3894da26220b7f3d887b839a79", - "sha256:0dd9a702591ca2e543631c2a017e4a547e38a5c0f29eece37d9097e04a7ac683", - "sha256:10619d9fdee46d20edc49d3479e2f8269d0779f1b031e6f7c2aa1c76be04b7ed", - "sha256:131a085a53bfe839a477c0845acf21efc77457ba2bcf5899618136d64f3303a2", - "sha256:1380560bdba02b6b6c90de54133c81c9f2a453dee9912fe58c1dcced1edb7cff", - "sha256:139718f35149ff544caba20fce6e8a2f71f1e39b92c700d8438a0b1d2a631a02", - "sha256:14291620375b1060613f4aab9ebf21850058b6b1b438f386cc814813d901c60b", - "sha256:1834bb90991cc2999f10f97f5f01317f99b143284766d197e43cd5b45eb18d03", - "sha256:1ab72135b1f2db3fed3997d7e7dc1b80573c67138023852b6efb336a5eae6511", - "sha256:1e7ce67c34138a058fd092f67d07a72b8e31ff0c9236e751957465a24b28910c", - "sha256:1e8fbaa7cec507aa24ea27a01456e8dd4b6fab829059b69844bd348f2d467124", - "sha256:22965c2af250d20c873cdbee8ff958fb809940aeb2e74ba5f20aaf6b7ac8c70c", - "sha256:22b029f2881599e2f1b06f8f1db2ee63bd309e2293ba2d566e008ba12778b8da", - "sha256:243dda95d901c733f5b59214d28b0120893d91777cb8aa043e6ef059d3cddfe2", - "sha256:2ca6fd72a8cd803be290d42f2dec5cdcd5299eeb93c2d929bf060ad9efaf5de0", - "sha256:2e4e1f6f0b4da23e61188676e3ed027ef0baa833a2e633c29ff8530800edccba", - "sha256:31f0b53913220599446872d757257be5898019c85e7971599065bc55065dc99d", - "sha256:334b8721303e61b00019474cc103bdac3d7b1f65e91f0bfedeec2d56dfe74b53", - "sha256:33e32a0dd0c8205efa8e83d04fc9f19313772b78522d1bdc7d9aed706bfd6138", - "sha256:34b36c2c57124530884d89d50ed2c1478697ad7473efd59cfd479945c95650e4", - "sha256:3aa27acb6de7a23785d81557577491f6c38a5209a254d1191519d07d8fe51748", - "sha256:3b06bcadaac49c70f4c88af4ffcfbe3dc155aab3163e75777818092478bcbbe7", - "sha256:3b7c88eeef021579d600e50363e0b6ee4f7f6f728cd3486b9d0f3ee7b946398d", - "sha256:3e2daa88dc91870215961e96a039ec73e4937da13cf77ce17f9cad0c18df3503", - "sha256:3ea66b1c11c9150f1372f69afb6b8116f2dd7286f38e14ea71a44eee9ec51b9d", - "sha256:42188e6a615c1a75bcaa6e150c3fe8f3e8680471a6b10150c5f7e83f47cc34d2", - "sha256:433885ab5431bc3d3d4f2f9bd15bfa1614c522b0f1405d62c4f926ccd69d04fa", - "sha256:437840083abe022c978470b942ff832c3940b2ad3734d424b7eaffcd07f76737", - "sha256:4398557cbf484207df000309235979c79c4356518fd5c99158c7d38203c4da4f", - "sha256:45c2842ff0e0d1b35a6bf1cd6c690939dacb617a70827f715232b2e0494d55d1", - "sha256:47743b82b76d89a1d20b83e60d5c20314cbd5ba2befc9cda8f28300c4a08ed4d", - "sha256:4792b262d585ff0dff6bcb787f8492e40698443ec982a3568c2096433660c694", - "sha256:47d8a5c446df1c4db9d21b49619ffdba90e77c89ec6e283f453856c74b50b9e3", - "sha256:47fdb18187e2a4e18fda2c25c05d8251a9e4a521edaed757fef033e7d8498d9a", - "sha256:4c52a6e78aef5cf47a98ef8e934755abf53953379b7d53e68b15ff4420e6683d", - "sha256:4dcc74149ccc8bba31ce1944acee24813e93cfdee2acda3c172df844948ddf7b", - "sha256:50678a3b71c751d58d7908edc96d332af328839eea883bb554a43f539101277a", - "sha256:51af598701f5299012b8416486b40fceef8c26fc87dc6d7d1f6fc30609ea0aa6", - "sha256:594fcab1032e2d2cc3321bb2e51271e7cd2b516c7d9aee780ece81b07ff8244b", - "sha256:595697f68bd1f0c1c159fcb97b661fc9c3f5db46498043555d04805430e79bea", - "sha256:59c189e3e99a59cf8d83cbb31d4db02d66cda5a1a4374e8a012b51255341abf5", - "sha256:5a3bf7f62a289fa90f1990422dc8dff5a458469ea71d1624585ec3a4c8d6960f", - "sha256:5c401e05ad47a75869c3ab3e35137f8468b846770587e70d71e11de797d113df", - "sha256:5cdac20da754f3a723cceea5b3448e1a2074866406adeb4ef35b469d089adb8f", - "sha256:5d0fcda9608875f7d052eff120c7a5da474a6796fe4d83e152e0e4d42f6d1a9b", - "sha256:5dbeefd6ca588b33576a01b0ad58aa934bc1b41ef89dee505bf2932b22ddffba", - "sha256:62441e55958977b8167b2709c164c91a6363e25da322d87ae6dd9c6019ceecf9", - "sha256:663e1cadaddae26be034a6ab6072449a8426ddb03d500f43daf952b74553bba0", - "sha256:669930400e375570189492dc8d8341301578e8493aec04aebc20d4717f899dd6", - "sha256:68986a61557d37bb90d3051a45b91fa3d5c516d177dfc6dd6f2f436a07ff2b6b", - "sha256:6944b2dc72c4d7f7052683487e3677456050ff77fcf5e6204e98caf785ad1967", - "sha256:6a635ea45ba4ea8238463b4f7d0e721bad669f80878b7bfd1f89266e2ae63da2", - "sha256:6c5010a52015e7c70f86eb967db0f37f3c8bd503a695a49f8d45700144667708", - "sha256:6dcbb0829c671f305be48a7227918cfcd11276c2d637a8033a99a02b67bf9eda", - "sha256:70dfd4f241c04bd9239d53b17f11e6ab672b9f1420364af63e8531198e3f5fe8", - "sha256:719ae08b6972befcba4310e49edb1161a88cdd331e3a694b84466bd938a6ab10", - "sha256:75976c6945d85dbb9ee6308cd7ff7b1fb9409380c82d6119bd778d8fcfe2931c", - "sha256:7861058d0582b847bc4e3a4a4c46828a410bca738673f35a29ba3ca5db0b473b", - "sha256:792a2af6d58177ef7c19cbf0097aba92ca1b9cb3ffdd9c7470e156c8f9b5e028", - "sha256:8009b3173bcd637be650922ac455946197d858b3630b6d8787aa9e5c4564533e", - "sha256:80ddf7a5f8c86cb3eb4bc9028b07bbbf1f08a96c5c0bc1244be5e8fefcb94147", - "sha256:8218f4e98d3c10d683584cb40f0424f4b9fd6e95610232dd75e13743b070ee33", - "sha256:84fc3ec96fce86ce5aa305eb4aa9358279d1aa644b71fab7b8ed33fe3ba1a7ca", - "sha256:852863707010316c973162e703bddabec35e8757e67fcb8ad58829de1ebc8590", - "sha256:8884d8b332a5e9b88e23f60bb166890009429391864c685e17bd73a9eda9105c", - "sha256:8dee9c25c74997f6a750cd317b8ca63545169c098faee42c84aa5e506c819b53", - "sha256:939fe60db294c786f6b7c2d2e121576628468f65453d86b0fe36cb52f987bd74", - "sha256:99b6fc1d55782461b78221e95fc357b47ad98b041e8e20f47c1411d0aacddc60", - "sha256:9d7672ecf7557476642c88497c2f8d8542f8e36596e928e9bcba0e42e1e7d71f", - "sha256:9f6d73c1436b934e3f01df1e1b21ff765cd1d28c77dfb9ace207f746d4610ee1", - "sha256:9fb17ea16e972c63d25d4a97f016d235c78dd2344820eb35bc034bc32012ee27", - "sha256:a49370e8f711daec68d09b821a34e1167792ee2d24d405cbc2387be4f158b520", - "sha256:a4fcfc8eb2c34148c118dfa02e6427ca278bfd0f3df7c5f99e33d2c0e81eae3e", - "sha256:a899cbd98dce6f5d8de1aad31cb712ec0a530abc0a86bd6edaa47c1090138467", - "sha256:a9b1ba5610a4e20f655258d5a1fdc7ebe3d837bb0e45b581398b99eb98b1f5ca", - "sha256:af74f05666a5e531289cb1cc9c883d1de2088b8e5b4de48004e5ca8a830ac859", - "sha256:b0748275abb8c1e1e09301ee3cf90c8a99678a4e92e4373705f2a2570d581273", - "sha256:b266bd01fedeffeeac01a79ae181719ff848a5a13ce10075adbefc8f1daee70e", - "sha256:b4f15793aa49793ec8d1c708ab7f9eded1aa72edc5174cae703651555ed1b601", - "sha256:b580e71cac3f8113d3135888770903eaf2f507e9421e5697d6ee6d8cd1c7f054", - "sha256:b6a6f620cfe13ccec221fa312139135166e47ae169f8253f72a0abc0dae94376", - "sha256:b790b39c7e9a4192dc2e201a282109ed2985a1ddbd5ac08dc56d0e121400a8f7", - "sha256:b85b982afde6df99ecc996990d4ad7ccbdbb70e2a4ba4de0aecde5922ba98a0b", - "sha256:b8a0588521a26bf92a57a1705b77b8b59044cdceccac7151bd8d229e66b8dedb", - "sha256:ba440ae430c00eee41509353628600212112cd5018d5def7e9b05ea7ac34eb65", - "sha256:bca03b91c323036913993ff5c738d0842fc9c60c4648e5c8d98331526df89784", - "sha256:bebf8557577d4401ba8bd9ff33906f1376c877aa78d1fe216ad01b4d6745af71", - "sha256:bec03d0d388060058f5d291a813f21c011041938a441c593374da6077fe21b1b", - "sha256:bf4a21e58b9cde0e401e683ebd00f6ed30a06d14e93f7c8fd059f8b6e8f87b6a", - "sha256:c0232bce2170103ec23c454e54a57008a9a72b5d1c3105dc2496750da8cfa47c", - "sha256:c4647674b6150d2cae088fc07de2738a84b8bcedebef29802cf0b0a82ab6face", - "sha256:c7044802eec4524fde550afc28edda0dd5784c4c45f0be151a2d3ba017daca7d", - "sha256:c7bd6683587567e5a49ee6e336e0612bec8329be1b7d4c8af5687dcdeb67ee1e", - "sha256:ca1f59c4e1ab6e72f0a23c13fca5430f889634166be85dbf1013683e49e3278e", - "sha256:cb95a9b1adaa48e41815a55ae740cfda005758104049a640a398120bf02515ca", - "sha256:cfebc0ac8333520d2d0423cbbe43ae43c8838862ddb898f5ca68565e395516e9", - "sha256:d332fc2e3c94dad927f2112395772a4e4fedbcf8f80efc21ed7cdfae4d574fdb", - "sha256:d3e32536234a95f513bd374e93d717cf6b2231a791758de6c509e3653f234c95", - "sha256:d5372ca1df0f91a86b047d1277c2aaf1edb32d78bbcefffc81b40ffd18f027ed", - "sha256:d77e1b2c6d04711478cb1c4ab90db07f1609ccf06a287d5607fcd90dc9863acf", - "sha256:d947071e6ebcf2e2bee8fce76e10faca8f7a14808ca36a910263acaacef08eca", - "sha256:dd7afd3f8b0bfb4e0d9fc3c31bfe8a4ec7debe124cfd90619305def3c8ca8cd2", - "sha256:de6b9a04c606978fdfe72666fa216ffcf2d1a9f6a381058d4378f8d7b1e5de62", - "sha256:e1651bf8e0398574646744c1885a41198eba53dc8a9312b954073f845c90a8df", - "sha256:e1b329cb8146d7b736677a2440e422eadd775d1806a81db2d4cded80a48efc1a", - "sha256:e1b51bebd221006d3d2f95fbe124b22b247136647ae5dcc8c7acafba66e5ee67", - "sha256:e340382d1afa5d32b892b3ff062436d592ec3d692aeea3bef3a5cfe11bbf8c6f", - "sha256:e4b582bab49ac33c8deb97e058cd67c2c50dac0dd134874106d9c774fd272529", - "sha256:e51ac5435758ba97ad69617e13233da53908beccc6cfcd6c34bbed8dcbede486", - "sha256:e5542339dcf2747135c5c85f68680353d5cb9ffd741c0f2e8d832d054d41f35a", - "sha256:e6438cc8f23a9c1478633d216b16104a586b9761db62bfacb6425bac0a36679e", - "sha256:e81fda2fb4a07eda1a2252b216aa0df23ebcd4d584894e9612e80999a78fd95b", - "sha256:ea70f61a47f3cc93bdf8b2f368ed359ef02a01ca6393916bc8ff877427181e74", - "sha256:ebd4549b108d732dba1d4ace67614b9545b21ece30937a63a65dd34efa19732d", - "sha256:efb07073be061c8f79d03d04139a80ba33cbd390ca8f0297aae9cce6411e4c6b", - "sha256:f0d97c18dfd9a9af4490631905a3f131a8e4c9e80a39353919e2cfed8f00aedc", - "sha256:f1e09112a2c31ffe8d80be1b0988fa6a18c5d5cad92a9ffbb1c04c91bfe52ad2", - "sha256:f3d7a87a78d46a2e3d5b72587ac14b4c16952dd0887dbb051451eceac774411e", - "sha256:f4afb5c34f2c6fecdcc182dfcfc6af6cccf1aa923eed4d6a12e9d96904e1a0d8", - "sha256:f6d2cb59377d99718913ad9a151030d6f83ef420a2b8f521d94609ecc106ee82", - "sha256:f87ac53513d22240c7d59203f25cc3beac1e574c6cd681bbfd321987b69f95fd", - "sha256:ff86011bd159a9d2dfc89c34cfd8aff12875980e3bd6a39ff097887520e60249" + "sha256:03214408cfa590df47728b84c679ae4ef00be2428e11630277be0727eba2d7cc", + "sha256:041b1a4cefacf65840b4e295c6985f334ba83c30607441ae3cf206a0eed1a2e4", + "sha256:0793e2bd0cf14234983bbb371591e6bea9e876ddf6896cdcc93450996b0b5c85", + "sha256:0e1fdaa14ef51366d7757b45bde294e95f6c8c049194e793eedb8387c86d5993", + "sha256:0e40111274f340d32ebcc0a5668d54d2b552a6cca84c9475859d364b380e3222", + "sha256:115136c4a426f9da976187d238e84139ff6b51a20839aa6e3720cd1026d768de", + "sha256:13a563739ae600a631c36ce096615fe307f131344588b0bc0daec108cdb47b25", + "sha256:16c6994ac35c3e74fb0ae93323bf8b9c2a9088d55946109489667c510a7d010e", + "sha256:170e26584b060879e29fac213e4228ef063f39128723807a312e5c7fec28eff2", + "sha256:17235362f580149742739cc3828b80e24029d08cbb9c4bda0242c7b5bc610a8e", + "sha256:1932b6b8bba8d0160a9d1078aae5838a66039e8832d41d2992daa9a3a08f7860", + "sha256:1b6b572edd95b4fa8df75de10b04bc81acc87c1c7d16bcdd2035b09d30acc957", + "sha256:1c3a3598a832590c5a3ce56ab5576361b5688c12cb1d39429cf5dba30b510760", + "sha256:1c57676bdedc94cd3bc37724cf6f8cd2779f02f6aba48de45feca073e714fe52", + "sha256:1dc702e42d0684f42d6519c8d581e49c96cefaaab16691f03566d30658ee8788", + "sha256:21d1b7305a71a15b4794b5ff22e8eef96ff4a6d7f9657155e5aa419444b28912", + "sha256:23f371bd662cf44a7630d4d113101eafc0cfa7518a2760d20760b26021454719", + "sha256:2569b67d616eab450d262ca7cb9f9e19d2f718c70a8b88712859359d0ab17035", + "sha256:263cd4f47159c09b8b685890af949195b51d1aa82ba451c5847ca9bc6413c220", + "sha256:2803ed8b21ca47a43da80a6fd1ed3019d30061f7061daa35ac54f63933409412", + "sha256:2a6940a074fb3c48356ed0158a3ca5699c955ee4185b4d7d619be3c327143e05", + "sha256:2e27c8841126e017dd2a054a95771569e6070b9ee1b133366d8b31beb5018a41", + "sha256:31c9921eb8bd12633b41ad27686bbb0b1a2a9b8452bfdf221e34f311e9942ed4", + "sha256:34b6cf500e61c90f305094911f9acc9c86da1a05a7a3f5be9f68817043f486e4", + "sha256:3650dc2480f94f7116c364096bc84b1d602f44224ef7d5c7208425915c0475dd", + "sha256:389871e65468400d6283c0308e791a640b5ab5c83bcee02a2f51295f95e09748", + "sha256:39004f0ad156da43e86aa71f44e033de68a44e5a31fc53507b36dd253970054a", + "sha256:394906945aa8b19fc14a61cf69743a868bb8c465efe85eee687109cc540b98f4", + "sha256:3ceb13c5c858d01321b5d9bb65e4cf37a92169ea470b70fec6f236b2c9dd7e34", + "sha256:411225bae281f114067578891bc75534cfb3d92a3b4dfef7a6ca78ba354e6069", + "sha256:44bb7bef4ea409384e3f8bc36c063d77ea1b8d4a5b2706956c0d6695f07dcc25", + "sha256:4503053d296bc6e4cbd1fad61cf3b6e33b939886c4f249ba7c78b602214fabe2", + "sha256:4764a6a7588561a9aef92f65bda2c4fb58fe7c675c0883862e6df97559de0bfb", + "sha256:4966242ec68afc74c122f8459abd597afd7d8a60dc93d695c1334c5fd25f762f", + "sha256:4a42e651629dafb64fd5b0286a3580613702b5809ad3f24934ea87595804f2c5", + "sha256:4a59ba56f340334766f3a4442e0efd0af895fae9e2b204741ef885c446b3a1a8", + "sha256:4c41e021bc6d7affb3364dc1e1e5fa9582b470f283748784bd6ea0558f87f42c", + "sha256:5023346c4ee7992febc0068e7593de5fa2bf611848c08404b35ebbb76b1b0512", + "sha256:50f9d8d531dfb767c565f348f33dd5139a6c43f5cbdf3f67da40d54241df93f6", + "sha256:51430653db848d258336cfa0244427b17d12db63d42603a55f0d4546f50f25b5", + "sha256:531ef597132086b6cf96faa7c6c1dcd0361dd5f1694e5cc30375907b9b7d3ea9", + "sha256:53ad387048f6f09a8969631e4de3f1bf70c50e93545d64af4f751b2498755072", + "sha256:53b1ea6ca88ebd4420379c330aea57e258408dd0df9af0992e5de2078dc9f5d5", + "sha256:575aa4405a656e61a540f4a80eaa5260f2a38fff7bfdc4b5f611840d76e9e277", + "sha256:578110dd426f0d209d1509244e6d4a3f1a3e9077655d98c5f22583d63252a08a", + "sha256:5ec2f42d41ccbd5df0270d7df31618a8ee267bfa50997f5d720ddba86c4a83a6", + "sha256:5ee586fb17ff8f90c91cf73c6108a434b02d69925f44f5f8e0d7f2f260607eae", + "sha256:5f10fd85e4b75967468af655228fbfd212bdf66db1c0d135065ce288982eda26", + "sha256:609d3614d78d74ebe35f54953c5bbd2ac647a7ddb9c30a5d877580f5e86b22f2", + "sha256:62694e275c93d54f7ccedcfef57d42761b2aad5234b6be1f3e3026cae4001cd4", + "sha256:63e92247f383c85ab00dd0091e8c3fa331a96e865459f5ee80353c70a4a42d70", + "sha256:682bae25f0a0dd23a056739f23a134db9f52a63e2afd6bfb37ddc76292bbd723", + "sha256:6b41389c19b07c760c7e427a3462e8ab83c4bb087d127f0e854c706ce1b9215c", + "sha256:6e87a6e8735b44816e7db0b2fbc9686932df473c826b0d9743148432e10bb9b9", + "sha256:6f0fd84de0c957b2d280143522c4f91a73aada1923caee763e24a2b3fda9f8a5", + "sha256:70efd20be968c76ece7baa8dafe04c5be06abc57f754d6f36f3741f7aa7a208e", + "sha256:71d006bee8397a4a89f469b8deb22469fe7508132d3c17fa6ed871e79832691c", + "sha256:73309162a6a571d4cbd3b6a1dcc703c7311843ae0d1578df6f09be4e98df38d4", + "sha256:75e3026ab649bf48f9a10c0134512638725b521340293f202a69b567518d94e0", + "sha256:76855800ac56f878847a09ce6dba727c93ca2d89c9e9d63002d26b916810b0a2", + "sha256:7c6b9461a2a8b47c65eef63bb1c76a4f1c119618ffa99ea79bc5bb1e46c5821b", + "sha256:803a3c3ce4acc62eaf01eaca1208dcf0783025ef27572c3336502b9c232005e7", + "sha256:80e6d33a3d42a7549b409f199857b4fb54e2103fc44fb87605b6663b7a7ff750", + "sha256:8419ebd326430d1cbb7efb5292330a2cf39114e82df5cc3d83c9a0d5ebeaf2f2", + "sha256:85610b4f27f69984932a7abbe52703688de3724d9f72bceb1cca667deff27474", + "sha256:85e9beda1f591bc73e77ea1c51965c68e98dafd0fec72cdd745f77d727466716", + "sha256:877b0738624280e34c55680d6054a307aa94f7d52fa0e3034a9cc6e790871da7", + "sha256:88f9fb0116fbfcefcab70f85cf4b74a2b6ce5d199c41345296f49d974ddb4123", + "sha256:8c4fe09e0780c6c3bf2b7d4af02ee2394439d11a523bbcf095cf4747c2932007", + "sha256:93a784271881035ab4406a172edb0faecb6e7d00f4b53dc2f55919d6c9688595", + "sha256:94f8575fbdf81749008d980c17796097e645574a3b8c28ee313931068dad14fe", + "sha256:95451e6ce06c3e104556d73b559f5da6c34a069b6b62946d3ad66afcd51642ea", + "sha256:99c8a9ed30f4164bc4c14b37a90208836cbf50d4ce2a57c71d0f52c7fb4f7598", + "sha256:9a18d6f9359e45722c064c97464ec883eb0e0366d33eda61cb19a244bf222679", + "sha256:9cbf44c5cb4a7633d078788e1b56387e3d3cf2b8139a3be38040b22d6c3221c8", + "sha256:9ee33b875f0b390564c1fb7bc528abf18c8ee6073b201c6ae8524aca778e2d83", + "sha256:a0e317df055958a0c1e79e5d2aa5a5eaa4a6d05a20d4b0c9c3f48918139c9fc6", + "sha256:a2df6afe50dea8ae15fa34c9f824a3ee958d785fd5d089063d960bae1daa0a3f", + "sha256:a31de1613658308efdb21ada98cbc86a97c181aa050ba22a808120bb5be3ab94", + "sha256:a3d2bff8f37f8d0f96c7ec554d16945050d54462d6e95414babaa18bfafc7f51", + "sha256:a41bcf68efd19073376eb8cf948b8d9be0af26256403e512bb18f3966f1f9120", + "sha256:a82836cab5f197a0514235aaf7ffccdc886ccdaa2324bc0aafdd4ae898103039", + "sha256:a8d00f29b42f534cc8aa3931cfe773b13b23e561e10d2b26f27a8d309b0e82a1", + "sha256:aafe5dcfda86c8af00386d7781d4c2181b5011b7be3f2add5e99899ea925df05", + "sha256:ab5f043cb8a2d71c981c09c510da013bc79fd661f5c60139f00dd3c3cc4f2ffb", + "sha256:ac09d42f48f80c9ee1635b2fcaa819496a44502737660d3c0f2ade7526d29144", + "sha256:aecfed0b41aa72b7881712c65cf764e39ce2ec352324f5e0837c7048d9e6daaa", + "sha256:b2c6b50c7b0464165472b56b42d4c76a7b864597007d9c085e8b63e185cf4a7a", + "sha256:b35d13d549077713e4414f927cdc388d62e543987c572baee613bf82f11a4b99", + "sha256:b39cb32a6582750b6cc77bfb3c49c0f8760dc18dc96ec9fb55fbb0f04e08b928", + "sha256:b5405bb8f0e783a988172993cfc627e4d9d00432d6bbac65a923041edacf997d", + "sha256:baaf55442359053c7d62f6f8413a62adba3205119bcb6f49594894d8be47e5e3", + "sha256:bd654fad46d8d9e823afbb4f87c79160b5a374ed1ff5bde24e542e6ba8f41434", + "sha256:be61f6fff406ca40e3b1d84716fde398fc08bc63dd96d15f3a14230a0973ed86", + "sha256:bf49a3ae946a87083ef3a34c8f677ae4243f5b824bfc4c69672e72b3d6719d46", + "sha256:c4a80f77dc1acaaa61f0934176fccca7096d9b1ff08c8ba9cddf5ae034a24319", + "sha256:c75eb09e8d55bceb4367e83496ff8ef2bc7ea6960efb38e978e8073ea59ecb67", + "sha256:c7f8dc16c498ff06497c015642333219871effba93e4a2e8604a06264aca5c5c", + "sha256:c8aa34a5c864db1087d911a0b902d60d203ea3607d91f615acd3f3108ac32169", + "sha256:cbb0fef01f0c6b38cb0f39b1f78fc90b807e0e3c86a7ff3ce74ad77ce5c7880c", + "sha256:cde9a2ecd91668bcb7f077c4966d8ceddb60af01b52e6e3e2680e4cf00ad1a59", + "sha256:cff6d44cb13d39db2663a22b22305d10855efa0fa8015ddeacc40bc59b9d8107", + "sha256:d1009abedb49ae95b136a8904a3f71b342f849ffeced2d3747bf29caeda218c4", + "sha256:d38c1e8231722c4ce40d7593f28d92b5fc72f3e9774fe73d7e800ec32299f63a", + "sha256:d53834e23c015ee83a99377db6e5e37d8484f333edb03bd15b4bc312cc7254fb", + "sha256:d7504f2b476d21653e4d143f44a175f7f751cd41233525312696c76aa3dbb23f", + "sha256:dbf507e9ef5688bada447a24d68b4b58dd389ba93b7afc065a2ba892bea54769", + "sha256:dc52310451fc7c629e13c4e061cbe2dd01684d91f2f8ee2821b083c58bd72432", + "sha256:dd00607bffbf30250fe108065f07453ec124dbf223420f57f5e749b04295e090", + "sha256:dda608c88cf709b1d406bdfcd84d8d63cff7c9e577a403c6108ce8ce9dcc8764", + "sha256:debe9c4f41c32990771be5c22b56f810659f9ddf3d63f67abfdcaa2c6c9c5c1d", + "sha256:e09fd068c2e169a7070d83d3bde728a4d48de0549f975290be3c108c02e499b4", + "sha256:e0fd068364a6759bc794459f0a735ab151d11304346332489c7972bacbe9e72b", + "sha256:e4c53f8347cd4200f0d70a48ad059cabaf24f5adc6ba08622a23423bc7efa10d", + "sha256:e5723c01a56c5028c807c701aa66722916d2747ad737a046853f6c46f4875543", + "sha256:e7b0460976dc75cb87ad9cc1f9899a4b97751e7d4e77ab840fc9b6d377b8fd24", + "sha256:e9d9a4d06d3481eab79803beb4d9bd6f6a8e781ec078ac70d7ef2dcc29d1bea5", + "sha256:ead11956716a940c1abc816b7df3fa2b84d06eaed8832ca32f5c5e058c65506b", + "sha256:ed5f69ce7be7902e5c70ea19eb72d20abf7d725ab5d49777d696e32d4fc1811d", + "sha256:f2af5c81a1f124609d5f33507082fc3f739959d4719b56877ab1ee7e7b3d602b", + "sha256:f40e782d49630ad384db66d4d8b73ff4f1b8955dc12e26b09a3e3af064b3b9d6", + "sha256:f514f6474e04179d3d33175ed3f3e31434d3130d42ec153540d5b157deefd735", + "sha256:f69f57305656a4852f2a7203efc661d8c042e6cc67f7acd97d8667fb448a426e", + "sha256:fb1e8b8d66c278b21d13b0a7ca22c41dd757a7c209c6b12c313e445c31dd3b28", + "sha256:fb4948814a2a98e3912505f09c9e7493b1506226afb1f881825368d6fb776ee3", + "sha256:fda207c815b253e34f7e1909840fd14299567b1c0eb4908f8c2ce01a41265401", + "sha256:fe8f8f5e70e6dbdfca9882cd9deaac058729bcf323cf7a58660901e55c9c94f6", + "sha256:fffc45637bcd6538de8b85f51e3df3223e4ad89bccbfca0481c08c7fc8b7ed7d" ], - "markers": "python_version >= '3.9'", - "version": "==1.22.0" + "markers": "python_version >= '3.10'", + "version": "==1.23.0" }, "zipp": { "hashes": [ @@ -2981,11 +2915,11 @@ }, "certifi": { "hashes": [ - "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", - "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120" + "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", + "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7" ], "markers": "python_version >= '3.7'", - "version": "==2026.1.4" + "version": "==2026.2.25" }, "charset-normalizer": { "hashes": [ @@ -3223,18 +3157,18 @@ }, "playwright": { "hashes": [ - "sha256:1dd93b265688da46e91ecb0606d36f777f8eadcf7fbef12f6426b20bf0c9137c", - "sha256:284ed5a706b7c389a06caa431b2f0ba9ac4130113c3a779767dda758c2497bb1", - "sha256:38a1bae6c0a07839cdeaddbc0756b3b2b85e476c07945f64ece08f1f956a86f1", - "sha256:5f065f5a133dbc15e6e7c71e7bc04f258195755b1c32a432b792e28338c8335e", - "sha256:6caefb08ed2c6f29d33b8088d05d09376946e49a73be19271c8cd5384b82b14c", - "sha256:9351c1ac3dfd9b3820fe7fc4340d96c0d3736bb68097b9b7a69bd45d25e9370c", - "sha256:99104771abc4eafee48f47dac2369e0015516dc1ce8c409807d2dd440828b9a4", - "sha256:a4a9d65027bce48eeba842408bcc1421502dfd7e41e28d207e94260fa93ca67e" + "sha256:185e0132578733d02802dfddfbbc35f42be23a45ff49ccae5081f25952238117", + "sha256:1e03be090e75a0fabbdaeab65ce17c308c425d879fa48bb1d7986f96bfad0b99", + "sha256:32ffe5c303901a13a0ecab91d1c3f74baf73b84f4bedbb6b935f5bc11cc98e1b", + "sha256:70c763694739d28df71ed578b9c8202bb83e8fe8fb9268c04dd13afe36301f71", + "sha256:8f9999948f1ab541d98812de25e3a8c410776aa516d948807140aff797b4bffa", + "sha256:96e3204aac292ee639edbfdef6298b4be2ea0a55a16b7068df91adac077cc606", + "sha256:a2bf639d0ce33b3ba38de777e08697b0d8f3dc07ab6802e4ac53fb65e3907af8", + "sha256:c95568ba1eda83812598c1dc9be60b4406dffd60b149bc1536180ad108723d6b" ], "index": "pypi", "markers": "python_version >= '3.9'", - "version": "==1.57.0" + "version": "==1.58.0" }, "pluggy": { "hashes": [ diff --git a/api/app_factory.py b/api/app_factory.py index e26509a8..7c93cce9 100644 --- a/api/app_factory.py +++ b/api/app_factory.py @@ -1,7 +1,9 @@ """Application factory for the text2sql FastAPI app.""" +import hmac import logging import os +import secrets from dotenv import load_dotenv from fastapi import FastAPI, Request, HTTPException @@ -54,6 +56,79 @@ async def dispatch(self, request: Request, call_next): return response +def _is_secure_request(request: Request) -> bool: + """Determine if the request is over HTTPS.""" + forwarded_proto = request.headers.get("x-forwarded-proto") + if forwarded_proto: + return forwarded_proto == "https" + return request.url.scheme == "https" + + +class CSRFMiddleware(BaseHTTPMiddleware): # pylint: disable=too-few-public-methods + """Double Submit Cookie CSRF protection. + + Sets a csrf_token cookie (readable by JS) on every response. + State-changing requests must echo the cookie value back + via the X-CSRF-Token header. Bearer-token authenticated + requests and auth/login endpoints are exempt. + """ + + SAFE_METHODS = frozenset({"GET", "HEAD", "OPTIONS", "TRACE"}) + CSRF_COOKIE = "csrf_token" + CSRF_HEADER = "x-csrf-token" + + # Paths exempt from CSRF validation (auth flow endpoints). + # "/mcp" has no trailing slash so it also covers sub-paths like /mcp/sse. + EXEMPT_PREFIXES = ( + "/login/", + "/signup/", + "/mcp", + ) + + async def dispatch(self, request: Request, call_next): + # Validate CSRF for unsafe, non-exempt, non-Bearer requests + if ( + request.method not in self.SAFE_METHODS + and not request.url.path.startswith(self.EXEMPT_PREFIXES) + and not request.headers.get("authorization", "").lower().startswith("bearer ") + ): + cookie_token = request.cookies.get(self.CSRF_COOKIE) + header_token = request.headers.get(self.CSRF_HEADER) + + if ( + not cookie_token + or not header_token + or not hmac.compare_digest(cookie_token, header_token) + ): + response = JSONResponse( + status_code=403, + content={"detail": "CSRF token missing or invalid"}, + ) + self._ensure_csrf_cookie(request, response) + return response + + response = await call_next(request) + self._ensure_csrf_cookie(request, response) + return response + + # Match the session cookie lifetime (14 days in seconds) + CSRF_COOKIE_MAX_AGE = 60 * 60 * 24 * 14 + + def _ensure_csrf_cookie(self, request: Request, response): + """Set the CSRF cookie if it is not already present.""" + if not request.cookies.get(self.CSRF_COOKIE): + token = secrets.token_urlsafe(32) + response.set_cookie( + key=self.CSRF_COOKIE, + value=token, + httponly=False, # JS must read this value + samesite="lax", + secure=_is_secure_request(request), + path="/", + max_age=self.CSRF_COOKIE_MAX_AGE, + ) + + def create_app(): """Create and configure the FastAPI application.""" @@ -192,6 +267,9 @@ def custom_openapi(): # Add security middleware app.add_middleware(SecurityMiddleware) + # Add CSRF middleware (double-submit cookie pattern) + app.add_middleware(CSRFMiddleware) + # Mount static files from the React build (app/dist) # This serves the bundled assets (JS, CSS, images, etc.) dist_path = os.path.join(os.path.dirname(__file__), "../app/dist") @@ -261,7 +339,7 @@ async def handle_oauth_error( # Serve React app for all non-API routes (SPA catch-all) @app.get("/{full_path:path}", include_in_schema=False) - async def serve_react_app(_full_path: str): + async def serve_react_app(full_path: str): # pylint: disable=unused-argument """Serve the React app for all routes not handled by API endpoints.""" # Serve index.html for the React SPA index_path = os.path.join(dist_path, "index.html") diff --git a/app/package-lock.json b/app/package-lock.json index 9d143266..33c4e227 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -1,14 +1,14 @@ { "name": "queryweaver-app", - "version": "0.0.14", + "version": "0.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "queryweaver-app", - "version": "0.0.14", + "version": "0.1.0", "dependencies": { - "@falkordb/canvas": "^0.0.29", + "@falkordb/canvas": "^0.0.41", "@hookform/resolvers": "^3.10.0", "@radix-ui/react-accordion": "^1.2.11", "@radix-ui/react-alert-dialog": "^1.1.14", @@ -37,7 +37,7 @@ "@radix-ui/react-toggle": "^1.1.9", "@radix-ui/react-toggle-group": "^1.1.10", "@radix-ui/react-tooltip": "^1.2.7", - "@tanstack/react-query": "^5.83.0", + "@tanstack/react-query": "^5.90.21", "@types/d3": "^7.4.3", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -48,11 +48,11 @@ "input-otp": "^1.4.2", "lucide-react": "^0.462.0", "next-themes": "^0.3.0", - "preact": "^10.28.2", + "preact": "^10.28.4", "react": "^18.3.1", "react-day-picker": "^8.10.1", "react-dom": "^18.3.1", - "react-hook-form": "^7.61.1", + "react-hook-form": "^7.71.2", "react-resizable-panels": "^2.1.9", "react-router-dom": "^6.30.1", "recharts": "^2.15.4", @@ -69,11 +69,11 @@ "@types/react": "^18.3.23", "@types/react-dom": "^18.3.7", "@vitejs/plugin-react-swc": "^3.11.0", - "autoprefixer": "^10.4.21", + "autoprefixer": "^10.4.27", "eslint": "^9.32.0", "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.20", - "globals": "^15.15.0", + "globals": "^17.3.0", "postcss": "^8.5.6", "tailwindcss": "^3.4.17", "typescript": "^5.8.3", @@ -674,9 +674,9 @@ } }, "node_modules/@falkordb/canvas": { - "version": "0.0.29", - "resolved": "https://registry.npmjs.org/@falkordb/canvas/-/canvas-0.0.29.tgz", - "integrity": "sha512-dDx1H/lOinzQb9rvI5DKDh7+tpwBArkemzZ9iTefH1/iO7BQ0gMr5MsnLxQo0A0Y57RTzKlJRWlpRAwbM4IPag==", + "version": "0.0.41", + "resolved": "https://registry.npmjs.org/@falkordb/canvas/-/canvas-0.0.41.tgz", + "integrity": "sha512-r2DC2Mo9naASr3DDmVKCQVY/S50Hn2bGlmKaFxDRFcn5btiuuvPBqJ3krtC+uz5ZPmad8EoXRRlcQ6KwKJhUZw==", "license": "MIT", "dependencies": { "d3": "^7.9.0", @@ -2346,9 +2346,9 @@ "license": "MIT" }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.55.1.tgz", - "integrity": "sha512-9R0DM/ykwfGIlNu6+2U09ga0WXeZ9MRC2Ter8jnz8415VbuIykVuc6bhdrbORFZANDmTDvq26mJrEVTl8TdnDg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", + "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", "cpu": [ "arm" ], @@ -2360,9 +2360,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.55.1.tgz", - "integrity": "sha512-eFZCb1YUqhTysgW3sj/55du5cG57S7UTNtdMjCW7LwVcj3dTTcowCsC8p7uBdzKsZYa8J7IDE8lhMI+HX1vQvg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", + "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", "cpu": [ "arm64" ], @@ -2374,9 +2374,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.55.1.tgz", - "integrity": "sha512-p3grE2PHcQm2e8PSGZdzIhCKbMCw/xi9XvMPErPhwO17vxtvCN5FEA2mSLgmKlCjHGMQTP6phuQTYWUnKewwGg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", + "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", "cpu": [ "arm64" ], @@ -2388,9 +2388,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.55.1.tgz", - "integrity": "sha512-rDUjG25C9qoTm+e02Esi+aqTKSBYwVTaoS1wxcN47/Luqef57Vgp96xNANwt5npq9GDxsH7kXxNkJVEsWEOEaQ==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", + "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", "cpu": [ "x64" ], @@ -2402,9 +2402,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.55.1.tgz", - "integrity": "sha512-+JiU7Jbp5cdxekIgdte0jfcu5oqw4GCKr6i3PJTlXTCU5H5Fvtkpbs4XJHRmWNXF+hKmn4v7ogI5OQPaupJgOg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", + "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", "cpu": [ "arm64" ], @@ -2416,9 +2416,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.55.1.tgz", - "integrity": "sha512-V5xC1tOVWtLLmr3YUk2f6EJK4qksksOYiz/TCsFHu/R+woubcLWdC9nZQmwjOAbmExBIVKsm1/wKmEy4z4u4Bw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", + "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", "cpu": [ "x64" ], @@ -2430,9 +2430,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.55.1.tgz", - "integrity": "sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", + "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", "cpu": [ "arm" ], @@ -2444,9 +2444,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.55.1.tgz", - "integrity": "sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", + "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", "cpu": [ "arm" ], @@ -2458,9 +2458,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.55.1.tgz", - "integrity": "sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", + "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", "cpu": [ "arm64" ], @@ -2472,9 +2472,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.55.1.tgz", - "integrity": "sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", + "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", "cpu": [ "arm64" ], @@ -2486,9 +2486,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.55.1.tgz", - "integrity": "sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", + "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", "cpu": [ "loong64" ], @@ -2500,9 +2500,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.55.1.tgz", - "integrity": "sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", + "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", "cpu": [ "loong64" ], @@ -2514,9 +2514,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.55.1.tgz", - "integrity": "sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", + "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", "cpu": [ "ppc64" ], @@ -2528,9 +2528,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.55.1.tgz", - "integrity": "sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", + "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", "cpu": [ "ppc64" ], @@ -2542,9 +2542,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.55.1.tgz", - "integrity": "sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", + "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", "cpu": [ "riscv64" ], @@ -2556,9 +2556,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.55.1.tgz", - "integrity": "sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", + "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", "cpu": [ "riscv64" ], @@ -2570,9 +2570,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.55.1.tgz", - "integrity": "sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", + "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", "cpu": [ "s390x" ], @@ -2584,7 +2584,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.55.1", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", "cpu": [ "x64" ], @@ -2596,9 +2598,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.55.1.tgz", - "integrity": "sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", "cpu": [ "x64" ], @@ -2610,9 +2612,9 @@ ] }, "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.55.1.tgz", - "integrity": "sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", + "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", "cpu": [ "x64" ], @@ -2624,9 +2626,9 @@ ] }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.55.1.tgz", - "integrity": "sha512-xzm44KgEP11te3S2HCSyYf5zIzWmx3n8HDCc7EE59+lTcswEWNpvMLfd9uJvVX8LCg9QWG67Xt75AuHn4vgsXw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", + "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", "cpu": [ "arm64" ], @@ -2638,9 +2640,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.55.1.tgz", - "integrity": "sha512-yR6Bl3tMC/gBok5cz/Qi0xYnVbIxGx5Fcf/ca0eB6/6JwOY+SRUcJfI0OpeTpPls7f194as62thCt/2BjxYN8g==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", + "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", "cpu": [ "arm64" ], @@ -2652,9 +2654,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.55.1.tgz", - "integrity": "sha512-3fZBidchE0eY0oFZBnekYCfg+5wAB0mbpCBuofh5mZuzIU/4jIVkbESmd2dOsFNS78b53CYv3OAtwqkZZmU5nA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", + "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", "cpu": [ "ia32" ], @@ -2666,9 +2668,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.55.1.tgz", - "integrity": "sha512-xGGY5pXj69IxKb4yv/POoocPy/qmEGhimy/FoTpTSVju3FYXUQQMFCaZZXJVidsmGxRioZAwpThl/4zX41gRKg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", + "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", "cpu": [ "x64" ], @@ -2680,9 +2682,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.55.1.tgz", - "integrity": "sha512-SPEpaL6DX4rmcXtnhdrQYgzQ5W2uW3SCJch88lB2zImhJRhIIK44fkUrgIV/Q8yUNfw5oyZ5vkeQsZLhCb06lw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", "cpu": [ "x64" ], @@ -2923,7 +2925,9 @@ } }, "node_modules/@tanstack/query-core": { - "version": "5.90.19", + "version": "5.90.20", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.20.tgz", + "integrity": "sha512-OMD2HLpNouXEfZJWcKeVKUgQ5n+n3A2JFmBaScpNDUqSrQSjiveC7dKMe53uJUg1nDG16ttFPz2xfilz6i2uVg==", "license": "MIT", "funding": { "type": "github", @@ -2931,10 +2935,12 @@ } }, "node_modules/@tanstack/react-query": { - "version": "5.90.19", + "version": "5.90.21", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.21.tgz", + "integrity": "sha512-0Lu6y5t+tvlTJMTO7oh5NSpJfpg/5D41LlThfepTixPYkJ0sE2Jj0m0f6yYqujBwIXlId87e234+MxG3D3g7kg==", "license": "MIT", "dependencies": { - "@tanstack/query-core": "5.90.19" + "@tanstack/query-core": "5.90.20" }, "funding": { "type": "github", @@ -3364,11 +3370,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "9.0.5", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "dev": true, "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^2.0.2" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -3519,7 +3527,9 @@ } }, "node_modules/autoprefixer": { - "version": "10.4.23", + "version": "10.4.27", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.27.tgz", + "integrity": "sha512-NP9APE+tO+LuJGn7/9+cohklunJsXWiaWEfV3si4Gi/XHDwVNgkwr1J3RQYFIvPy76GmJ9/bW8vyoU1LcxwKHA==", "dev": true, "funding": [ { @@ -3538,7 +3548,7 @@ "license": "MIT", "dependencies": { "browserslist": "^4.28.1", - "caniuse-lite": "^1.0.30001760", + "caniuse-lite": "^1.0.30001774", "fraction.js": "^5.3.4", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" @@ -3653,7 +3663,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001764", + "version": "1.0.30001775", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001775.tgz", + "integrity": "sha512-s3Qv7Lht9zbVKE9XoTyRG6wVDCKdtOFIjBGg3+Yhn6JaytuNKPIjBMTMIY1AnOH3seL5mvF+x33oGAyK3hVt3A==", "dev": true, "funding": [ { @@ -4677,9 +4689,9 @@ } }, "node_modules/force-graph": { - "version": "1.51.0", - "resolved": "https://registry.npmjs.org/force-graph/-/force-graph-1.51.0.tgz", - "integrity": "sha512-aTnihCmiMA0ItLJLCbrQYS9mzriopW24goFPgUnKAAmAlPogTSmFWqoBPMXzIfPb7bs04Hur5zEI4WYgLW3Sig==", + "version": "1.51.1", + "resolved": "https://registry.npmjs.org/force-graph/-/force-graph-1.51.1.tgz", + "integrity": "sha512-uEEX8iRzgq1IKRISOw6RrB2RLMhcI25xznQYrCTVvxZHZZ+A2jH6qIolYuwavVxAMi64pFp2yZm4KFVdD993cg==", "license": "MIT", "dependencies": { "@tweenjs/tween.js": "18 - 25", @@ -4753,7 +4765,9 @@ } }, "node_modules/globals": { - "version": "15.15.0", + "version": "17.3.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.3.0.tgz", + "integrity": "sha512-yMqGUQVVCkD4tqjOJf3TnrvaaHDMYp4VlUSObbkIiuCPe/ofdMBFIAcBbCSRFWOnos6qRiTVStDwqPLUclaxIw==", "dev": true, "license": "MIT", "engines": { @@ -5052,7 +5066,9 @@ } }, "node_modules/minimatch": { - "version": "3.1.2", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -5389,9 +5405,9 @@ "license": "MIT" }, "node_modules/preact": { - "version": "10.28.3", - "resolved": "https://registry.npmjs.org/preact/-/preact-10.28.3.tgz", - "integrity": "sha512-tCmoRkPQLpBeWzpmbhryairGnhW9tKV6c6gr/w+RhoRoKEJwsjzipwp//1oCpGPOchvSLaAPlpcJi9MwMmoPyA==", + "version": "10.28.4", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.28.4.tgz", + "integrity": "sha512-uKFfOHWuSNpRFVTnljsCluEFq57OKT+0QdOiQo8XWnQ/pSvg7OpX5eNOejELXJMWy+BwM2nobz0FkvzmnpCNsQ==", "license": "MIT", "funding": { "type": "opencollective", @@ -5479,7 +5495,9 @@ } }, "node_modules/react-hook-form": { - "version": "7.71.1", + "version": "7.71.2", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.71.2.tgz", + "integrity": "sha512-1CHvcDYzuRUNOflt4MOq3ZM46AronNJtQ1S7tnX6YN4y72qhgiUItpacZUAQ0TyWYci3yz1X+rXaSxiuEm86PA==", "license": "MIT", "engines": { "node": ">=18.0.0" @@ -5708,7 +5726,9 @@ "license": "Unlicense" }, "node_modules/rollup": { - "version": "4.55.1", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", + "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", "dev": true, "license": "MIT", "dependencies": { @@ -5722,31 +5742,31 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.55.1", - "@rollup/rollup-android-arm64": "4.55.1", - "@rollup/rollup-darwin-arm64": "4.55.1", - "@rollup/rollup-darwin-x64": "4.55.1", - "@rollup/rollup-freebsd-arm64": "4.55.1", - "@rollup/rollup-freebsd-x64": "4.55.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.55.1", - "@rollup/rollup-linux-arm-musleabihf": "4.55.1", - "@rollup/rollup-linux-arm64-gnu": "4.55.1", - "@rollup/rollup-linux-arm64-musl": "4.55.1", - "@rollup/rollup-linux-loong64-gnu": "4.55.1", - "@rollup/rollup-linux-loong64-musl": "4.55.1", - "@rollup/rollup-linux-ppc64-gnu": "4.55.1", - "@rollup/rollup-linux-ppc64-musl": "4.55.1", - "@rollup/rollup-linux-riscv64-gnu": "4.55.1", - "@rollup/rollup-linux-riscv64-musl": "4.55.1", - "@rollup/rollup-linux-s390x-gnu": "4.55.1", - "@rollup/rollup-linux-x64-gnu": "4.55.1", - "@rollup/rollup-linux-x64-musl": "4.55.1", - "@rollup/rollup-openbsd-x64": "4.55.1", - "@rollup/rollup-openharmony-arm64": "4.55.1", - "@rollup/rollup-win32-arm64-msvc": "4.55.1", - "@rollup/rollup-win32-ia32-msvc": "4.55.1", - "@rollup/rollup-win32-x64-gnu": "4.55.1", - "@rollup/rollup-win32-x64-msvc": "4.55.1", + "@rollup/rollup-android-arm-eabi": "4.59.0", + "@rollup/rollup-android-arm64": "4.59.0", + "@rollup/rollup-darwin-arm64": "4.59.0", + "@rollup/rollup-darwin-x64": "4.59.0", + "@rollup/rollup-freebsd-arm64": "4.59.0", + "@rollup/rollup-freebsd-x64": "4.59.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", + "@rollup/rollup-linux-arm-musleabihf": "4.59.0", + "@rollup/rollup-linux-arm64-gnu": "4.59.0", + "@rollup/rollup-linux-arm64-musl": "4.59.0", + "@rollup/rollup-linux-loong64-gnu": "4.59.0", + "@rollup/rollup-linux-loong64-musl": "4.59.0", + "@rollup/rollup-linux-ppc64-gnu": "4.59.0", + "@rollup/rollup-linux-ppc64-musl": "4.59.0", + "@rollup/rollup-linux-riscv64-gnu": "4.59.0", + "@rollup/rollup-linux-riscv64-musl": "4.59.0", + "@rollup/rollup-linux-s390x-gnu": "4.59.0", + "@rollup/rollup-linux-x64-gnu": "4.59.0", + "@rollup/rollup-linux-x64-musl": "4.59.0", + "@rollup/rollup-openbsd-x64": "4.59.0", + "@rollup/rollup-openharmony-arm64": "4.59.0", + "@rollup/rollup-win32-arm64-msvc": "4.59.0", + "@rollup/rollup-win32-ia32-msvc": "4.59.0", + "@rollup/rollup-win32-x64-gnu": "4.59.0", + "@rollup/rollup-win32-x64-msvc": "4.59.0", "fsevents": "~2.3.2" } }, diff --git a/app/package.json b/app/package.json index cd7e42c2..6322ceab 100644 --- a/app/package.json +++ b/app/package.json @@ -1,7 +1,7 @@ { "name": "queryweaver-app", "private": true, - "version": "0.0.14", + "version": "0.1.0", "type": "module", "scripts": { "dev": "vite", @@ -11,7 +11,7 @@ "preview": "vite preview" }, "dependencies": { - "@falkordb/canvas": "^0.0.29", + "@falkordb/canvas": "^0.0.41", "@hookform/resolvers": "^3.10.0", "@radix-ui/react-accordion": "^1.2.11", "@radix-ui/react-alert-dialog": "^1.1.14", @@ -40,7 +40,7 @@ "@radix-ui/react-toggle": "^1.1.9", "@radix-ui/react-toggle-group": "^1.1.10", "@radix-ui/react-tooltip": "^1.2.7", - "@tanstack/react-query": "^5.83.0", + "@tanstack/react-query": "^5.90.21", "@types/d3": "^7.4.3", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -51,11 +51,11 @@ "input-otp": "^1.4.2", "lucide-react": "^0.462.0", "next-themes": "^0.3.0", - "preact": "^10.28.2", + "preact": "^10.28.4", "react": "^18.3.1", "react-day-picker": "^8.10.1", "react-dom": "^18.3.1", - "react-hook-form": "^7.61.1", + "react-hook-form": "^7.71.2", "react-resizable-panels": "^2.1.9", "react-router-dom": "^6.30.1", "recharts": "^2.15.4", @@ -72,11 +72,11 @@ "@types/react": "^18.3.23", "@types/react-dom": "^18.3.7", "@vitejs/plugin-react-swc": "^3.11.0", - "autoprefixer": "^10.4.21", + "autoprefixer": "^10.4.27", "eslint": "^9.32.0", "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.20", - "globals": "^15.15.0", + "globals": "^17.3.0", "postcss": "^8.5.6", "tailwindcss": "^3.4.17", "typescript": "^5.8.3", diff --git a/app/src/components/modals/DatabaseModal.tsx b/app/src/components/modals/DatabaseModal.tsx index 5442cf61..35feae26 100644 --- a/app/src/components/modals/DatabaseModal.tsx +++ b/app/src/components/modals/DatabaseModal.tsx @@ -8,6 +8,7 @@ import { useDatabase } from "@/contexts/DatabaseContext"; import { useToast } from "@/components/ui/use-toast"; import { Loader2, CheckCircle2, XCircle } from "lucide-react"; import { buildApiUrl, API_CONFIG } from "@/config/api"; +import { csrfHeaders } from "@/lib/csrf"; interface DatabaseModalProps { open: boolean; @@ -101,6 +102,7 @@ const DatabaseModal = ({ open, onOpenChange }: DatabaseModalProps) => { method: 'POST', headers: { 'Content-Type': 'application/json', + ...csrfHeaders(), }, body: JSON.stringify({ url: dbUrl }), credentials: 'include', diff --git a/app/src/lib/csrf.ts b/app/src/lib/csrf.ts new file mode 100644 index 00000000..61656c30 --- /dev/null +++ b/app/src/lib/csrf.ts @@ -0,0 +1,38 @@ +/** + * CSRF protection utilities. + * + * The backend sets a `csrf_token` cookie (non-HttpOnly) on every response. + * State-changing requests (POST, PUT, DELETE, PATCH) must echo the cookie + * value back via the `X-CSRF-Token` header. + */ + +const CSRF_COOKIE_NAME = 'csrf_token'; + +/** + * Read the CSRF token from the cookie jar. + */ +export function getCsrfToken(): string { + if (typeof document === 'undefined') { + return ''; + } + const match = document.cookie.match( + new RegExp(`(?:^|;\\s*)${CSRF_COOKIE_NAME}=([^;]*)`) + ); + if (!match) { + console.debug( + `CSRF token cookie "${CSRF_COOKIE_NAME}" not found. ` + + 'State-changing requests may fail with 403 until the cookie is set.' + ); + return ''; + } + return decodeURIComponent(match[1]); +} + +/** + * Return headers object containing the CSRF token. + * Merge this into every state-changing fetch call. + */ +export function csrfHeaders(): Record { + const token = getCsrfToken(); + return token ? { 'X-CSRF-Token': token } : {}; +} diff --git a/app/src/pages/Index.tsx b/app/src/pages/Index.tsx index 7ac8bcd4..0572ee48 100644 --- a/app/src/pages/Index.tsx +++ b/app/src/pages/Index.tsx @@ -15,6 +15,7 @@ import { useAuth } from "@/contexts/AuthContext"; import { useDatabase } from "@/contexts/DatabaseContext"; import { DatabaseService } from "@/services/database"; import { useToast } from "@/components/ui/use-toast"; +import { csrfHeaders } from "@/lib/csrf"; import { DropdownMenu, DropdownMenuContent, @@ -255,6 +256,9 @@ const Index = () => { setIsRefreshingSchema(true); const response = await fetch(`/graphs/${selectedGraph.id}/refresh`, { method: 'POST', + headers: { + ...csrfHeaders(), + }, credentials: 'include', }); diff --git a/app/src/services/auth.ts b/app/src/services/auth.ts index 24a3d787..373c8963 100644 --- a/app/src/services/auth.ts +++ b/app/src/services/auth.ts @@ -1,4 +1,5 @@ import { API_CONFIG, buildApiUrl } from '@/config/api'; +import { csrfHeaders } from '@/lib/csrf'; import type { AuthStatus, User } from '@/types/api'; /** @@ -93,6 +94,9 @@ export class AuthService { await fetch(buildApiUrl(API_CONFIG.ENDPOINTS.LOGOUT), { method: 'POST', credentials: 'include', + headers: { + ...csrfHeaders(), + }, }); } catch (error) { console.error('Failed to logout:', error); diff --git a/app/src/services/chat.ts b/app/src/services/chat.ts index f5a1323b..70c438b2 100644 --- a/app/src/services/chat.ts +++ b/app/src/services/chat.ts @@ -1,4 +1,5 @@ import { API_CONFIG, buildApiUrl } from '@/config/api'; +import { csrfHeaders } from '@/lib/csrf'; import type { ChatRequest, StreamMessage, ConfirmRequest } from '@/types/api'; /** @@ -39,6 +40,7 @@ export class ChatService { method: 'POST', headers: { 'Content-Type': 'application/json', + ...csrfHeaders(), }, body: JSON.stringify({ chat: chatHistory, @@ -169,6 +171,7 @@ export class ChatService { method: 'POST', headers: { 'Content-Type': 'application/json', + ...csrfHeaders(), }, body: JSON.stringify(request), credentials: 'include', diff --git a/app/src/services/database.ts b/app/src/services/database.ts index 1e069df1..a74f4e96 100644 --- a/app/src/services/database.ts +++ b/app/src/services/database.ts @@ -1,4 +1,5 @@ import { API_CONFIG, buildApiUrl } from '@/config/api'; +import { csrfHeaders } from '@/lib/csrf'; import type { Graph, GraphUploadResponse, SchemaUploadRequest } from '@/types/api'; /** @@ -128,6 +129,9 @@ export class DatabaseService { method: 'POST', body: formData, credentials: 'include', + headers: { + ...csrfHeaders(), + }, }); if (!response.ok) { @@ -157,6 +161,9 @@ export class DatabaseService { { method: 'DELETE', credentials: 'include', + headers: { + ...csrfHeaders(), + }, } ); @@ -182,6 +189,7 @@ export class DatabaseService { method: 'POST', headers: { 'Content-Type': 'application/json', + ...csrfHeaders(), }, body: JSON.stringify({ url: config.connectionUrl, @@ -240,6 +248,7 @@ export class DatabaseService { method: 'POST', headers: { 'Content-Type': 'application/json', + ...csrfHeaders(), }, body: JSON.stringify({ url: connectionUrl, @@ -317,6 +326,7 @@ export class DatabaseService { credentials: 'include', headers: { 'Content-Type': 'application/json', + ...csrfHeaders(), }, body: JSON.stringify({ user_rules: userRules }), }); diff --git a/app/src/services/tokens.ts b/app/src/services/tokens.ts index 61b1125e..638a5ec5 100644 --- a/app/src/services/tokens.ts +++ b/app/src/services/tokens.ts @@ -3,6 +3,7 @@ */ import { buildApiUrl } from '@/config/api'; +import { csrfHeaders } from '@/lib/csrf'; export interface Token { token_id: string; @@ -27,6 +28,7 @@ export class TokenService { credentials: 'include', headers: { 'Content-Type': 'application/json', + ...csrfHeaders(), }, signal: controller.signal, }); @@ -93,6 +95,7 @@ export class TokenService { credentials: 'include', headers: { 'Content-Type': 'application/json', + ...csrfHeaders(), }, signal: controller.signal, }); diff --git a/e2e/infra/api/apiRequests.ts b/e2e/infra/api/apiRequests.ts index 512315ab..babed7a5 100644 --- a/e2e/infra/api/apiRequests.ts +++ b/e2e/infra/api/apiRequests.ts @@ -2,6 +2,64 @@ import { APIRequestContext, request } from "@playwright/test" +/** + * Extract the CSRF token from a response's Set-Cookie header. + * The backend sets a `csrf_token` cookie on every response. + */ +function extractCsrfToken(setCookieHeaders: string[]): string | undefined { + for (const header of setCookieHeaders) { + const match = header.match(/csrf_token=([^;]+)/); + if (match) return match[1]; + } + return undefined; +} + +/** + * Per-context CSRF token cache. After the first seed request the token is + * stored and reused for subsequent calls on the same APIRequestContext, + * avoiding an extra GET /auth-status on every state-changing request. + */ +const csrfCache = new WeakMap(); + +/** + * Seed the CSRF cookie on the given request context by making a lightweight + * GET (only on the first call), then return the cached token value. + * + * When the context is initialised from a storageState that already contains + * a csrf_token cookie, the server will NOT set a new one (no Set-Cookie + * header). In that case we fall back to reading the cookie value that is + * already stored in the context. + */ +async function getCsrfToken(baseUrl: string, ctx: APIRequestContext): Promise { + const cached = csrfCache.get(ctx); + if (cached) return cached; + + const seedResp = await ctx.get(`${baseUrl}/auth-status`); + const setCookies = seedResp.headersArray() + .filter(h => h.name.toLowerCase() === 'set-cookie') + .map(h => h.value); + let token = extractCsrfToken(setCookies); + + // If the server didn't set a new cookie, the context may already carry one + // from its storageState – read it directly. + if (!token) { + const state = await ctx.storageState(); + const existing = state.cookies.find(c => c.name === 'csrf_token'); + if (existing) token = existing.value; + } + + if (token) csrfCache.set(ctx, token); + return token; +} + +/** + * Derive the origin (scheme + host + port) from a full URL so we can call + * `getCsrfToken` without requiring callers to pass the base URL separately. + */ +function originOf(url: string): string { + const u = new URL(url); + return u.origin; +} const getRequest = async (url: string, headers?: Record, body?: any, availableRequest?: APIRequestContext) => { const requestOptions = { @@ -15,34 +73,49 @@ const getRequest = async (url: string, headers?: Record, body?: }; const postRequest = async (url: string, body?: any, availableRequest?: APIRequestContext, headers?: Record) => { + const requestContext = availableRequest || (await request.newContext()); + const csrfToken = await getCsrfToken(originOf(url), requestContext); + const requestOptions = { data: body, - headers: headers || undefined, + headers: { + ...(headers || {}), + ...(csrfToken ? { 'X-CSRF-Token': csrfToken } : {}), + }, }; - const requestContext = availableRequest || (await request.newContext()); const response = await requestContext.post(url, requestOptions); return response; }; const deleteRequest = async (url: string, headers?: Record, body?: any, availableRequest?: APIRequestContext) => { + const requestContext = availableRequest || (await request.newContext()); + const csrfToken = await getCsrfToken(originOf(url), requestContext); + const requestOptions = { data: body, - headers: headers || undefined, + headers: { + ...(headers || {}), + ...(csrfToken ? { 'X-CSRF-Token': csrfToken } : {}), + }, }; - const requestContext = availableRequest || (await request.newContext()); const response = await requestContext.delete(url, requestOptions); return response; }; const patchRequest = async (url: string, body?: any, availableRequest?: APIRequestContext, headers?: Record) => { + const requestContext = availableRequest || (await request.newContext()); + const csrfToken = await getCsrfToken(originOf(url), requestContext); + const requestOptions = { data: body, - headers: headers || undefined, + headers: { + ...(headers || {}), + ...(csrfToken ? { 'X-CSRF-Token': csrfToken } : {}), + }, }; - const requestContext = availableRequest || (await request.newContext()); const response = await requestContext.patch(url, requestOptions); return response; }; diff --git a/e2e/logic/api/apiCalls.ts b/e2e/logic/api/apiCalls.ts index a1d00c92..6ffd2279 100644 --- a/e2e/logic/api/apiCalls.ts +++ b/e2e/logic/api/apiCalls.ts @@ -20,6 +20,12 @@ import type { // ==================== AUTHENTICATION ENDPOINTS ==================== export default class ApiCalls { + private defaultRequestContext?: APIRequestContext; + + constructor(requestContext?: APIRequestContext) { + this.defaultRequestContext = requestContext; + } + /** * Check authentication status * GET /auth-status @@ -33,7 +39,7 @@ export default class ApiCalls { `${baseUrl}/auth-status`, undefined, undefined, - requestContext + requestContext || this.defaultRequestContext ); return await response.json(); } catch (error) { @@ -57,7 +63,7 @@ export default class ApiCalls { const response = await postRequest( `${baseUrl}/login/email`, { email, password }, - requestContext + requestContext || this.defaultRequestContext ); const data = await response.json(); @@ -85,7 +91,7 @@ export default class ApiCalls { const response = await postRequest( `${baseUrl}/signup/email`, { firstName, lastName, email, password }, - requestContext + requestContext || this.defaultRequestContext ); return await response.json(); } catch (error) { @@ -107,7 +113,7 @@ export default class ApiCalls { const response = await postRequest( `${baseUrl}/logout`, undefined, - requestContext + requestContext || this.defaultRequestContext ); return await response.json(); } catch (error) { @@ -147,7 +153,8 @@ export default class ApiCalls { const response = await getRequest( `${baseUrl}/graphs`, undefined, - undefined + undefined, + this.defaultRequestContext ); return await response.json(); } catch (error) { @@ -169,7 +176,8 @@ export default class ApiCalls { const response = await getRequest( `${baseUrl}/graphs/${graphId}/data`, undefined, - undefined + undefined, + this.defaultRequestContext ); return await response.json(); } catch (error) { @@ -200,7 +208,7 @@ export default class ApiCalls { const response = await postRequest( `${baseUrl}/graphs`, formData, - undefined, + this.defaultRequestContext, { "Content-Type": "multipart/form-data" } ); return await response.json(); @@ -233,7 +241,7 @@ export default class ApiCalls { const response = await postRequest( `${baseUrl}/graphs/${graphId}`, body, - undefined, + this.defaultRequestContext, { "Content-Type": "application/json" } ); @@ -258,6 +266,14 @@ export default class ApiCalls { .split("|||FALKORDB_MESSAGE_BOUNDARY|||") .filter((msg) => msg.trim()) .map((msg) => JSON.parse(msg.trim())); + // Log error messages to help diagnose CI failures + const errorMessages = messages.filter((m) => m.type === "error"); + if (errorMessages.length > 0) { + console.log( + `[parseStreamingResponse] HTTP status: ${response.status()}, error messages received:`, + JSON.stringify(errorMessages) + ); + } return messages; } catch (error) { throw new Error( @@ -266,6 +282,80 @@ export default class ApiCalls { } } + /** + * Poll getGraphs() until the predicate is satisfied, or until timeout. + * Returns the last observed graph list (for diagnostics even on timeout). + * Can be used to wait for a graph to appear or to disappear. + */ + async waitForGraphs( + predicate: (graphs: GraphsListResponse) => boolean, + timeoutMs: number = 30000, + pollIntervalMs: number = 2000 + ): Promise { + const deadline = Date.now() + timeoutMs; + let lastGraphs: GraphsListResponse = []; + while (Date.now() < deadline) { + try { + lastGraphs = await this.getGraphs(); + if (predicate(lastGraphs)) { + return lastGraphs; + } + } catch (err) { + console.log( + `[waitForGraphs] getGraphs() error: ${(err as Error).message}` + ); + } + const remaining = deadline - Date.now(); + if (remaining <= 0) break; + await new Promise((resolve) => + setTimeout(resolve, Math.min(pollIntervalMs, remaining)) + ); + } + console.log( + `[waitForGraphs] timed out after ${timeoutMs}ms. Last graphs: ${JSON.stringify(lastGraphs)}` + ); + return lastGraphs; + } + + /** + * Connect to external database with retry on transient errors. + * Retries up to `maxAttempts` times if the streaming response final message is not 'final_result'. + */ + async connectDatabaseWithRetry( + connectionUrl: string, + maxAttempts: number = 3, + retryDelayMs: number = 3000 + ): Promise { + let lastMessages: StreamMessage[] = []; + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + try { + const response = await this.connectDatabase(connectionUrl); + const messages = await this.parseStreamingResponse(response); + const finalMessage = messages[messages.length - 1]; + if (finalMessage && finalMessage.type === "final_result") { + return messages; + } + console.log( + `[connectDatabaseWithRetry] attempt ${attempt}/${maxAttempts} did not return final_result.`, + `Last message: ${JSON.stringify(finalMessage)}` + ); + lastMessages = messages; + } catch (err) { + console.error( + `[connectDatabaseWithRetry] attempt ${attempt}/${maxAttempts} threw an error:`, + (err as Error).message + ); + } + if (attempt < maxAttempts) { + await new Promise((resolve) => setTimeout(resolve, retryDelayMs)); + } + } + console.log( + `[connectDatabaseWithRetry] all ${maxAttempts} attempts exhausted. Last messages: ${JSON.stringify(lastMessages)}` + ); + return lastMessages; + } + /** * Confirm destructive SQL operation * POST /graphs/{graph_id}/confirm @@ -287,7 +377,7 @@ export default class ApiCalls { const response = await postRequest( `${baseUrl}/graphs/${graphId}/confirm`, body, - undefined, + this.defaultRequestContext, { "Content-Type": "application/json" } ); @@ -311,7 +401,7 @@ export default class ApiCalls { const response = await postRequest( `${baseUrl}/graphs/${graphId}/refresh`, undefined, - undefined, + this.defaultRequestContext, { "Content-Type": "application/json" } ); @@ -337,7 +427,7 @@ export default class ApiCalls { `${baseUrl}/graphs/${graphId}`, undefined, undefined, - requestContext + requestContext || this.defaultRequestContext ); return await response.json(); } catch (error) { @@ -366,7 +456,7 @@ export default class ApiCalls { const response = await postRequest( `${baseUrl}/database`, body, - undefined, + this.defaultRequestContext, { "Content-Type": "application/json" } ); @@ -389,7 +479,8 @@ export default class ApiCalls { const baseUrl = getBaseUrl(); const response = await postRequest( `${baseUrl}/tokens/generate`, - undefined + undefined, + this.defaultRequestContext ); return await response.json(); } catch (error) { @@ -409,7 +500,8 @@ export default class ApiCalls { const response = await getRequest( `${baseUrl}/tokens/list`, undefined, - undefined + undefined, + this.defaultRequestContext ); return await response.json(); } catch (error) { @@ -433,7 +525,7 @@ export default class ApiCalls { `${baseUrl}/tokens/${tokenId}`, undefined, undefined, - requestContext + requestContext || this.defaultRequestContext ); return await response.json(); } catch (error) { diff --git a/e2e/tests/chat.spec.ts b/e2e/tests/chat.spec.ts index ffbc6ba4..cf10442a 100644 --- a/e2e/tests/chat.spec.ts +++ b/e2e/tests/chat.spec.ts @@ -12,9 +12,9 @@ test.describe('Chat Feature Tests', () => { // Set longer timeout for chat tests (60 seconds) test.setTimeout(60000); - test.beforeEach(async () => { + test.beforeEach(async ({ request }) => { browser = new BrowserWrapper(); - apiCall = new ApiCalls(); + apiCall = new ApiCalls(request); }); test.afterEach(async () => { @@ -42,7 +42,7 @@ test.describe('Chat Feature Tests', () => { }); test('valid query shows SQL, results, and AI response', async () => { - const homePage = await browser.createNewPage(HomePage, getBaseUrl()); + const homePage = await browser.createNewPage(HomePage, getBaseUrl(), 'e2e/.auth/user.json'); await browser.setPageToFullScreen(); // Ensure database is connected (will skip if already connected) @@ -74,7 +74,7 @@ test.describe('Chat Feature Tests', () => { }); test('off-topic query shows AI message without SQL or results', async () => { - const homePage = await browser.createNewPage(HomePage, getBaseUrl()); + const homePage = await browser.createNewPage(HomePage, getBaseUrl(), 'e2e/.auth/user.json'); await browser.setPageToFullScreen(); // Ensure database is connected (will skip if already connected) @@ -105,7 +105,7 @@ test.describe('Chat Feature Tests', () => { }); test('multiple sequential queries maintain conversation history', async () => { - const homePage = await browser.createNewPage(HomePage, getBaseUrl()); + const homePage = await browser.createNewPage(HomePage, getBaseUrl(), 'e2e/.auth/user.json'); await browser.setPageToFullScreen(); // Ensure database is connected (will skip if already connected) @@ -141,7 +141,7 @@ test.describe('Chat Feature Tests', () => { }); test('empty query submission is prevented', async () => { - const homePage = await browser.createNewPage(HomePage, getBaseUrl()); + const homePage = await browser.createNewPage(HomePage, getBaseUrl(), 'e2e/.auth/user.json'); await browser.setPageToFullScreen(); // Ensure database is connected (will skip if already connected) @@ -153,7 +153,7 @@ test.describe('Chat Feature Tests', () => { }); test('rapid query submission is prevented during processing', async () => { - const homePage = await browser.createNewPage(HomePage, getBaseUrl()); + const homePage = await browser.createNewPage(HomePage, getBaseUrl(), 'e2e/.auth/user.json'); await browser.setPageToFullScreen(); // Ensure database is connected (will skip if already connected) @@ -190,7 +190,7 @@ test.describe('Chat Feature Tests', () => { expect(finalMessage2.type).toBe('final_result'); expect(finalMessage2.success).toBeTruthy(); - const homePage = await browser.createNewPage(HomePage, getBaseUrl()); + const homePage = await browser.createNewPage(HomePage, getBaseUrl(), 'e2e/.auth/user.json'); await browser.setPageToFullScreen(); // Wait for page to load databases @@ -224,7 +224,7 @@ test.describe('Chat Feature Tests', () => { }); test('destructive operation shows inline confirmation and executes on confirm', async () => { - const homePage = await browser.createNewPage(HomePage, getBaseUrl()); + const homePage = await browser.createNewPage(HomePage, getBaseUrl(), 'e2e/.auth/user.json'); await browser.setPageToFullScreen(); // Ensure database is connected @@ -266,7 +266,7 @@ test.describe('Chat Feature Tests', () => { }); test('duplicate record shows user-friendly error message', async () => { - const homePage = await browser.createNewPage(HomePage, getBaseUrl()); + const homePage = await browser.createNewPage(HomePage, getBaseUrl(), 'e2e/.auth/user.json'); await browser.setPageToFullScreen(); // Ensure database is connected diff --git a/e2e/tests/database.spec.ts b/e2e/tests/database.spec.ts index d3b91188..73c9ab4c 100644 --- a/e2e/tests/database.spec.ts +++ b/e2e/tests/database.spec.ts @@ -10,9 +10,9 @@ test.describe('Database Connection Tests', () => { let browser: BrowserWrapper; let apiCall: ApiCalls; - test.beforeEach(async () => { + test.beforeEach(async ({ request }) => { browser = new BrowserWrapper(); - apiCall = new ApiCalls(); + apiCall = new ApiCalls(request); }); test.afterEach(async () => { @@ -20,31 +20,39 @@ test.describe('Database Connection Tests', () => { }); test('connect PostgreSQL via API -> verify in UI', async () => { - const homePage = await browser.createNewPage(HomePage, getBaseUrl()); + test.setTimeout(120000); // Allow extra time for schema loading in CI + const homePage = await browser.createNewPage(HomePage, getBaseUrl(), 'e2e/.auth/user.json'); await browser.setPageToFullScreen(); const { postgres: postgresUrl } = getTestDatabases(); - // Connect via API - response is streaming - const response = await apiCall.connectDatabase(postgresUrl); - const messages = await apiCall.parseStreamingResponse(response); + // Connect via API - response is streaming (retry on transient errors) + const messages = await apiCall.connectDatabaseWithRetry(postgresUrl); // Verify final message indicates success + expect(messages.length).toBeGreaterThan(0); const finalMessage = messages[messages.length - 1]; + if (finalMessage.type !== 'final_result') { + console.log(`[PostgreSQL API connect] unexpected final message: ${JSON.stringify(finalMessage)}`); + } expect(finalMessage.type).toBe('final_result'); expect(finalMessage.success).toBeTruthy(); // Get the list of databases to find the connected database - const graphsList = await apiCall.getGraphs(); + const graphsList = await apiCall.waitForGraphs( + (graphs) => graphs.some((id) => id === 'testdb' || id.endsWith('_testdb')), + 30000 + ); expect(graphsList).toBeDefined(); expect(Array.isArray(graphsList)).toBeTruthy(); expect(graphsList.length).toBeGreaterThan(0); + console.log(`[PostgreSQL API connect] graphs after connection: ${JSON.stringify(graphsList)}`); // Find the testdb database (not testdb_delete) - could be 'testdb' or 'userId_testdb' const graphId = graphsList.find(id => id === 'testdb' || id.endsWith('_testdb')); expect(graphId).toBeTruthy(); // Wait for UI to reflect the connection (schema loading completes) - const connectionEstablished = await homePage.waitForDatabaseConnection(); + const connectionEstablished = await homePage.waitForDatabaseConnection(90000); expect(connectionEstablished).toBeTruthy(); // Verify connection appears in UI - check database status badge @@ -64,31 +72,39 @@ test.describe('Database Connection Tests', () => { }); test('connect MySQL via API -> verify in UI', async () => { - const homePage = await browser.createNewPage(HomePage, getBaseUrl()); + test.setTimeout(120000); // Allow extra time for schema loading in CI + const homePage = await browser.createNewPage(HomePage, getBaseUrl(), 'e2e/.auth/user.json'); await browser.setPageToFullScreen(); const { mysql: mysqlUrl } = getTestDatabases(); - // Connect via API - response is streaming - const response = await apiCall.connectDatabase(mysqlUrl); - const messages = await apiCall.parseStreamingResponse(response); + // Connect via API - response is streaming (retry on transient errors) + const messages = await apiCall.connectDatabaseWithRetry(mysqlUrl); // Verify final message indicates success + expect(messages.length).toBeGreaterThan(0); const finalMessage = messages[messages.length - 1]; + if (finalMessage.type !== 'final_result') { + console.log(`[MySQL API connect] unexpected final message: ${JSON.stringify(finalMessage)}`); + } expect(finalMessage.type).toBe('final_result'); expect(finalMessage.success).toBeTruthy(); // Get the list of databases to find the connected database - const graphsList = await apiCall.getGraphs(); + const graphsList = await apiCall.waitForGraphs( + (graphs) => graphs.some((id) => id === 'testdb' || id.endsWith('_testdb')), + 30000 + ); expect(graphsList).toBeDefined(); expect(Array.isArray(graphsList)).toBeTruthy(); expect(graphsList.length).toBeGreaterThan(0); + console.log(`[MySQL API connect] graphs after connection: ${JSON.stringify(graphsList)}`); // Find the testdb database (not testdb_delete) - could be 'testdb' or 'userId_testdb' const graphId = graphsList.find(id => id === 'testdb' || id.endsWith('_testdb')); expect(graphId).toBeTruthy(); // Wait for UI to reflect the connection (schema loading completes) - const connectionEstablished = await homePage.waitForDatabaseConnection(); + const connectionEstablished = await homePage.waitForDatabaseConnection(90000); expect(connectionEstablished).toBeTruthy(); // Verify connection appears in UI - check database status badge @@ -108,7 +124,8 @@ test.describe('Database Connection Tests', () => { }); test('connect PostgreSQL via UI (URL) -> verify via API', async () => { - const homePage = await browser.createNewPage(HomePage, getBaseUrl()); + test.setTimeout(120000); // Allow extra time for schema loading in CI + const homePage = await browser.createNewPage(HomePage, getBaseUrl(), 'e2e/.auth/user.json'); await browser.setPageToFullScreen(); const { postgres: postgresUrl } = getTestDatabases(); @@ -120,17 +137,24 @@ test.describe('Database Connection Tests', () => { await homePage.clickOnDatabaseModalConnect(); // Wait for UI to reflect the connection (schema loading completes) - const connectionEstablished = await homePage.waitForDatabaseConnection(); + const connectionEstablished = await homePage.waitForDatabaseConnection(90000); + if (!connectionEstablished) { + console.log('[PostgreSQL URL connect] waitForDatabaseConnection timed out'); + } expect(connectionEstablished).toBeTruthy(); - // Verify via API - get the list of databases - const graphsList = await apiCall.getGraphs(); + // Verify via API - poll until the expected testdb graph appears + const graphsList = await apiCall.waitForGraphs( + (graphs) => graphs.some((id) => id === 'testdb' || id.endsWith('_testdb')), + 30000 + ); expect(graphsList).toBeDefined(); expect(Array.isArray(graphsList)).toBeTruthy(); expect(graphsList.length).toBeGreaterThan(0); + console.log(`[PostgreSQL URL connect] graphs after connection: ${JSON.stringify(graphsList)}`); // Get the connected database ID - const graphId = graphsList[0]; + const graphId = graphsList.find((id) => id === 'testdb' || id.endsWith('_testdb')); expect(graphId).toBeTruthy(); // Verify connection appears in UI @@ -139,7 +163,8 @@ test.describe('Database Connection Tests', () => { }); test('connect MySQL via UI (URL) -> verify via API', async () => { - const homePage = await browser.createNewPage(HomePage, getBaseUrl()); + test.setTimeout(120000); // Allow extra time for schema loading in CI + const homePage = await browser.createNewPage(HomePage, getBaseUrl(), 'e2e/.auth/user.json'); await browser.setPageToFullScreen(); const { mysql: mysqlUrl } = getTestDatabases(); @@ -151,17 +176,24 @@ test.describe('Database Connection Tests', () => { await homePage.clickOnDatabaseModalConnect(); // Wait for UI to reflect the connection (schema loading completes) - const connectionEstablished = await homePage.waitForDatabaseConnection(); + const connectionEstablished = await homePage.waitForDatabaseConnection(90000); + if (!connectionEstablished) { + console.log('[MySQL URL connect] waitForDatabaseConnection timed out'); + } expect(connectionEstablished).toBeTruthy(); - // Verify via API - get the list of databases - const graphsList = await apiCall.getGraphs(); + // Verify via API - poll until the expected testdb graph appears + const graphsList = await apiCall.waitForGraphs( + (graphs) => graphs.some((id) => id === 'testdb' || id.endsWith('_testdb')), + 30000 + ); expect(graphsList).toBeDefined(); expect(Array.isArray(graphsList)).toBeTruthy(); expect(graphsList.length).toBeGreaterThan(0); + console.log(`[MySQL URL connect] graphs after connection: ${JSON.stringify(graphsList)}`); // Get the connected database ID - const graphId = graphsList[0]; + const graphId = graphsList.find((id) => id === 'testdb' || id.endsWith('_testdb')); expect(graphId).toBeTruthy(); // Verify connection appears in UI @@ -170,7 +202,8 @@ test.describe('Database Connection Tests', () => { }); test('connect PostgreSQL via UI (Manual Entry) -> verify via API', async () => { - const homePage = await browser.createNewPage(HomePage, getBaseUrl()); + test.setTimeout(120000); // Allow extra time for schema loading in CI + const homePage = await browser.createNewPage(HomePage, getBaseUrl(), 'e2e/.auth/user.json'); await browser.setPageToFullScreen(); // Connect via UI using manual entry mode @@ -187,17 +220,24 @@ test.describe('Database Connection Tests', () => { await homePage.clickOnDatabaseModalConnect(); // Wait for UI to reflect the connection (schema loading completes) - const connectionEstablished = await homePage.waitForDatabaseConnection(); + const connectionEstablished = await homePage.waitForDatabaseConnection(90000); + if (!connectionEstablished) { + console.log('[PostgreSQL Manual connect] waitForDatabaseConnection timed out'); + } expect(connectionEstablished).toBeTruthy(); - // Verify via API - get the list of databases - const graphsList = await apiCall.getGraphs(); + // Verify via API - poll until the expected testdb graph appears + const graphsList = await apiCall.waitForGraphs( + (graphs) => graphs.some((id) => id === 'testdb' || id.endsWith('_testdb')), + 30000 + ); expect(graphsList).toBeDefined(); expect(Array.isArray(graphsList)).toBeTruthy(); expect(graphsList.length).toBeGreaterThan(0); + console.log(`[PostgreSQL Manual connect] graphs after connection: ${JSON.stringify(graphsList)}`); // Get the connected database ID - const graphId = graphsList[0]; + const graphId = graphsList.find((id) => id === 'testdb' || id.endsWith('_testdb')); expect(graphId).toBeTruthy(); // Verify connection appears in UI @@ -206,7 +246,8 @@ test.describe('Database Connection Tests', () => { }); test('connect MySQL via UI (Manual Entry) -> verify via API', async () => { - const homePage = await browser.createNewPage(HomePage, getBaseUrl()); + test.setTimeout(120000); // Allow extra time for schema loading in CI + const homePage = await browser.createNewPage(HomePage, getBaseUrl(), 'e2e/.auth/user.json'); await browser.setPageToFullScreen(); // Connect via UI using manual entry mode @@ -223,17 +264,24 @@ test.describe('Database Connection Tests', () => { await homePage.clickOnDatabaseModalConnect(); // Wait for UI to reflect the connection (schema loading completes) - const connectionEstablished = await homePage.waitForDatabaseConnection(); + const connectionEstablished = await homePage.waitForDatabaseConnection(90000); + if (!connectionEstablished) { + console.log('[MySQL Manual connect] waitForDatabaseConnection timed out'); + } expect(connectionEstablished).toBeTruthy(); - // Verify via API - get the list of databases - const graphsList = await apiCall.getGraphs(); + // Verify via API - poll until the expected testdb graph appears + const graphsList = await apiCall.waitForGraphs( + (graphs) => graphs.some((id) => id === 'testdb' || id.endsWith('_testdb')), + 30000 + ); expect(graphsList).toBeDefined(); expect(Array.isArray(graphsList)).toBeTruthy(); expect(graphsList.length).toBeGreaterThan(0); + console.log(`[MySQL Manual connect] graphs after connection: ${JSON.stringify(graphsList)}`); // Get the connected database ID - const graphId = graphsList[0]; + const graphId = graphsList.find((id) => id === 'testdb' || id.endsWith('_testdb')); expect(graphId).toBeTruthy(); // Verify connection appears in UI @@ -242,7 +290,7 @@ test.describe('Database Connection Tests', () => { }); test('invalid connection string -> shows error', async () => { - const homePage = await browser.createNewPage(HomePage, getBaseUrl()); + const homePage = await browser.createNewPage(HomePage, getBaseUrl(), 'e2e/.auth/user.json'); await browser.setPageToFullScreen(); const invalidUrl = 'invalid://connection:string'; @@ -270,38 +318,41 @@ test.describe('Database Connection Tests', () => { // Delete tests run serially to avoid conflicts test.describe.serial('Database Deletion Tests', () => { - test('delete PostgreSQL database via UI -> verify removed via API', async () => { + test('delete PostgreSQL database via UI -> verify removed via API', async () => { + test.setTimeout(180000); // Allow extra time: schema loading + UI interaction // Use the separate postgres delete container on port 5433 const postgresDeleteUrl = 'postgresql://postgres:postgres@localhost:5433/testdb_delete'; - // Connect database via API - const connectResponse = await apiCall.connectDatabase(postgresDeleteUrl); - const connectMessages = await apiCall.parseStreamingResponse(connectResponse); + // Connect database via API (retry on transient errors) + const connectMessages = await apiCall.connectDatabaseWithRetry(postgresDeleteUrl); + expect(connectMessages.length).toBeGreaterThan(0); const finalMessage = connectMessages[connectMessages.length - 1]; + if (finalMessage.type !== 'final_result') { + console.log(`[PostgreSQL delete connect] unexpected final message: ${JSON.stringify(finalMessage)}`); + } expect(finalMessage.type).toBe('final_result'); expect(finalMessage.success).toBeTruthy(); - // Wait a bit for the connection to be fully registered - await new Promise(resolve => setTimeout(resolve, 2000)); - - // Get the graph ID from the API - let graphsList = await apiCall.getGraphs(); + // Poll until the graph appears in the API + let graphsList = await apiCall.waitForGraphs( + (graphs) => graphs.some((id) => id === 'testdb_delete' || id.endsWith('_testdb_delete')), + 30000 + ); expect(graphsList.length).toBeGreaterThan(0); - // Find the graph that contains 'testdb_delete' (could be 'testdb_delete' or 'userId_testdb_delete') - const graphId = graphsList.find(id => id.includes('testdb_delete')); + // Find the graph that is 'testdb_delete' or '{userId}_testdb_delete' + const graphId = graphsList.find(id => id === 'testdb_delete' || id.endsWith('_testdb_delete')); - // If not found, log all graphs for debugging if (!graphId) { - console.log('Available graphs:', graphsList); - console.log('Looking for graph containing: testdb_delete'); + console.log('[PostgreSQL delete] Available graphs:', graphsList); + console.log('[PostgreSQL delete] Looking for graph containing: testdb_delete'); } expect(graphId).toBeTruthy(); const initialCount = graphsList.length; // Create new page and open it - const homePage = await browser.createNewPage(HomePage, getBaseUrl()); + const homePage = await browser.createNewPage(HomePage, getBaseUrl(), 'e2e/.auth/user.json'); await browser.setPageToFullScreen(); // Delete via UI - open dropdown, click delete, confirm @@ -309,62 +360,68 @@ test.describe('Database Connection Tests', () => { await homePage.clickOnDeleteGraph(graphId!); await homePage.clickOnDeleteModalConfirm(); - // Wait for deletion to complete - await homePage.wait(1000); - - // Verify removed from API - graphsList = await apiCall.getGraphs(); + // Wait for deletion to complete - poll until graphId is absent (up to 30s) + graphsList = await apiCall.waitForGraphs( + (graphs) => !graphs.some((id) => id === graphId), + 30000 + ); expect(graphsList.length).toBe(initialCount - 1); expect(graphsList).not.toContain(graphId); - }); - - test('delete MySQL database via UI -> verify removed via API', async () => { - // Use the separate mysql delete container on port 3307 - const mysqlDeleteUrl = 'mysql://root:password@localhost:3307/testdb_delete'; + }); - // Connect database via API - const connectResponse = await apiCall.connectDatabase(mysqlDeleteUrl); - const connectMessages = await apiCall.parseStreamingResponse(connectResponse); - const finalMessage = connectMessages[connectMessages.length - 1]; - expect(finalMessage.type).toBe('final_result'); - expect(finalMessage.success).toBeTruthy(); + test('delete MySQL database via UI -> verify removed via API', async () => { + test.setTimeout(180000); // Allow extra time: schema loading + UI interaction + // Use the separate mysql delete container on port 3307 + const mysqlDeleteUrl = 'mysql://root:password@localhost:3307/testdb_delete'; - // Wait a bit for the connection to be fully registered - await new Promise(resolve => setTimeout(resolve, 2000)); + // Connect database via API (retry on transient errors) + const connectMessages = await apiCall.connectDatabaseWithRetry(mysqlDeleteUrl); + expect(connectMessages.length).toBeGreaterThan(0); + const finalMessage = connectMessages[connectMessages.length - 1]; + if (finalMessage.type !== 'final_result') { + console.log(`[MySQL delete connect] unexpected final message: ${JSON.stringify(finalMessage)}`); + } + expect(finalMessage.type).toBe('final_result'); + expect(finalMessage.success).toBeTruthy(); - // Get the graph ID from the API - find the graph containing testdb_delete - let graphsList = await apiCall.getGraphs(); - expect(graphsList.length).toBeGreaterThan(0); - const graphId = graphsList.find(id => id.includes('testdb_delete')); - - // If not found, log all graphs for debugging - if (!graphId) { - console.log('Available graphs:', graphsList); - console.log('Looking for graph containing: testdb_delete'); - } - - expect(graphId).toBeTruthy(); - const initialCount = graphsList.length; + // Poll until the graph appears in the API + let graphsList = await apiCall.waitForGraphs( + (graphs) => graphs.some((id) => id === 'testdb_delete' || id.endsWith('_testdb_delete')), + 30000 + ); + expect(graphsList.length).toBeGreaterThan(0); + const graphId = graphsList.find(id => id === 'testdb_delete' || id.endsWith('_testdb_delete')); + + if (!graphId) { + console.log('[MySQL delete] Available graphs:', graphsList); + console.log('[MySQL delete] Looking for graph containing: testdb_delete'); + } + + expect(graphId).toBeTruthy(); + const initialCount = graphsList.length; - const homePage = await browser.createNewPage(HomePage, getBaseUrl()); - await browser.setPageToFullScreen(); + const homePage = await browser.createNewPage(HomePage, getBaseUrl(), 'e2e/.auth/user.json'); + await browser.setPageToFullScreen(); - // Wait for UI to reflect the connection (increased timeout for schema loading) - const connectionEstablished = await homePage.waitForDatabaseConnection(60000); - expect(connectionEstablished).toBeTruthy(); + // Wait for UI to reflect the connection (increased timeout for schema loading) + const connectionEstablished = await homePage.waitForDatabaseConnection(90000); + if (!connectionEstablished) { + console.log('[MySQL delete] waitForDatabaseConnection timed out'); + } + expect(connectionEstablished).toBeTruthy(); - // Delete via UI - open dropdown, click delete, confirm - await homePage.clickOnDatabaseSelector(); - await homePage.clickOnDeleteGraph(graphId!); - await homePage.clickOnDeleteModalConfirm(); - - // Wait for deletion to complete - await homePage.wait(1000); - - // Verify removed from API - graphsList = await apiCall.getGraphs(); - expect(graphsList.length).toBe(initialCount - 1); - expect(graphsList).not.toContain(graphId); + // Delete via UI - open dropdown, click delete, confirm + await homePage.clickOnDatabaseSelector(); + await homePage.clickOnDeleteGraph(graphId!); + await homePage.clickOnDeleteModalConfirm(); + + // Wait for deletion to complete - poll until graphId is absent (up to 30s) + graphsList = await apiCall.waitForGraphs( + (graphs) => !graphs.some((id) => id === graphId), + 30000 + ); + expect(graphsList.length).toBe(initialCount - 1); + expect(graphsList).not.toContain(graphId); }); }); }); diff --git a/package-lock.json b/package-lock.json index ded65412..a73d9881 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,5 +1,5 @@ { - "name": "QueryWeaver", + "name": "queryweaver", "lockfileVersion": 3, "requires": true, "packages": { @@ -16,9 +16,9 @@ }, "app": { "name": "queryweaver-app", - "version": "0.0.1", + "version": "0.0.14", "dependencies": { - "@falkordb/canvas": "^0.0.29", + "@falkordb/canvas": "^0.0.41", "@hookform/resolvers": "^3.10.0", "@radix-ui/react-accordion": "^1.2.11", "@radix-ui/react-alert-dialog": "^1.1.14", @@ -47,7 +47,7 @@ "@radix-ui/react-toggle": "^1.1.9", "@radix-ui/react-toggle-group": "^1.1.10", "@radix-ui/react-tooltip": "^1.2.7", - "@tanstack/react-query": "^5.83.0", + "@tanstack/react-query": "^5.90.21", "@types/d3": "^7.4.3", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -58,11 +58,11 @@ "input-otp": "^1.4.2", "lucide-react": "^0.462.0", "next-themes": "^0.3.0", - "preact": "^10.28.2", + "preact": "^10.28.4", "react": "^18.3.1", "react-day-picker": "^8.10.1", "react-dom": "^18.3.1", - "react-hook-form": "^7.61.1", + "react-hook-form": "^7.71.2", "react-resizable-panels": "^2.1.9", "react-router-dom": "^6.30.1", "recharts": "^2.15.4", @@ -79,11 +79,11 @@ "@types/react": "^18.3.23", "@types/react-dom": "^18.3.7", "@vitejs/plugin-react-swc": "^3.11.0", - "autoprefixer": "^10.4.21", + "autoprefixer": "^10.4.27", "eslint": "^9.32.0", "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.20", - "globals": "^15.15.0", + "globals": "^17.3.0", "postcss": "^8.5.6", "tailwindcss": "^3.4.17", "typescript": "^5.8.3", @@ -258,6 +258,10 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "app/node_modules/@falkordb/canvas": { + "resolved": "packages/canvas", + "link": true + }, "app/node_modules/@floating-ui/core": { "version": "1.7.3", "license": "MIT", @@ -1998,28 +2002,6 @@ "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1" } }, - "app/node_modules/@tanstack/query-core": { - "version": "5.90.19", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - } - }, - "app/node_modules/@tanstack/react-query": { - "version": "5.90.19", - "license": "MIT", - "dependencies": { - "@tanstack/query-core": "5.90.19" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - }, - "peerDependencies": { - "react": "^18 || ^19" - } - }, "app/node_modules/@types/d3": { "version": "7.4.3", "license": "MIT", @@ -2229,6 +2211,7 @@ "version": "22.19.7", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -2242,6 +2225,7 @@ "version": "18.3.27", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.2.2" @@ -2251,6 +2235,7 @@ "version": "18.3.7", "devOptional": true, "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^18.0.0" } @@ -2294,6 +2279,7 @@ "version": "8.53.0", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.53.0", "@typescript-eslint/types": "8.53.0", @@ -2501,6 +2487,7 @@ "version": "8.15.0", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2579,54 +2566,11 @@ "node": ">=10" } }, - "app/node_modules/autoprefixer": { - "version": "10.4.23", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/autoprefixer" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "browserslist": "^4.28.1", - "caniuse-lite": "^1.0.30001760", - "fraction.js": "^5.3.4", - "picocolors": "^1.1.1", - "postcss-value-parser": "^4.2.0" - }, - "bin": { - "autoprefixer": "bin/autoprefixer" - }, - "engines": { - "node": "^10 || ^12 || >=14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, "app/node_modules/balanced-match": { "version": "1.0.2", "dev": true, "license": "MIT" }, - "app/node_modules/baseline-browser-mapping": { - "version": "2.9.15", - "dev": true, - "license": "Apache-2.0", - "bin": { - "baseline-browser-mapping": "dist/cli.js" - } - }, "app/node_modules/binary-extensions": { "version": "2.3.0", "license": "MIT", @@ -2656,38 +2600,6 @@ "node": ">=8" } }, - "app/node_modules/browserslist": { - "version": "4.28.1", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "baseline-browser-mapping": "^2.9.0", - "caniuse-lite": "^1.0.30001759", - "electron-to-chromium": "^1.5.263", - "node-releases": "^2.0.27", - "update-browserslist-db": "^1.2.0" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, "app/node_modules/callsites": { "version": "3.1.0", "dev": true, @@ -2703,25 +2615,6 @@ "node": ">= 6" } }, - "app/node_modules/caniuse-lite": { - "version": "1.0.30001764", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" - }, "app/node_modules/chalk": { "version": "4.1.2", "dev": true, @@ -2851,6 +2744,7 @@ "app/node_modules/date-fns": { "version": "3.6.0", "license": "MIT", + "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/kossnocorp" @@ -2901,14 +2795,10 @@ "csstype": "^3.0.2" } }, - "app/node_modules/electron-to-chromium": { - "version": "1.5.267", - "dev": true, - "license": "ISC" - }, "app/node_modules/embla-carousel": { "version": "8.6.0", - "license": "MIT" + "license": "MIT", + "peer": true }, "app/node_modules/embla-carousel-react": { "version": "8.6.0", @@ -2968,14 +2858,6 @@ "@esbuild/win32-x64": "0.27.2" } }, - "app/node_modules/escalade": { - "version": "3.2.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "app/node_modules/escape-string-regexp": { "version": "4.0.0", "dev": true, @@ -2991,6 +2873,7 @@ "version": "9.39.2", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -3254,18 +3137,6 @@ "dev": true, "license": "ISC" }, - "app/node_modules/fraction.js": { - "version": "5.3.4", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/rawify" - } - }, "app/node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -3304,17 +3175,6 @@ "node": ">=10.13.0" } }, - "app/node_modules/globals": { - "version": "15.15.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "app/node_modules/has-flag": { "version": "4.0.0", "dev": true, @@ -3427,14 +3287,11 @@ "app/node_modules/jiti": { "version": "1.21.7", "license": "MIT", + "peer": true, "bin": { "jiti": "bin/jiti.js" } }, - "app/node_modules/js-tokens": { - "version": "4.0.0", - "license": "MIT" - }, "app/node_modules/js-yaml": { "version": "4.1.1", "dev": true, @@ -3518,16 +3375,6 @@ "dev": true, "license": "MIT" }, - "app/node_modules/loose-envify": { - "version": "1.4.0", - "license": "MIT", - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, "app/node_modules/lucide-react": { "version": "0.462.0", "license": "ISC", @@ -3578,22 +3425,6 @@ "thenify-all": "^1.0.0" } }, - "app/node_modules/nanoid": { - "version": "3.3.11", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, "app/node_modules/natural-compare": { "version": "1.4.0", "dev": true, @@ -3607,11 +3438,6 @@ "react-dom": "^16.8 || ^17 || ^18" } }, - "app/node_modules/node-releases": { - "version": "2.0.27", - "dev": true, - "license": "MIT" - }, "app/node_modules/normalize-path": { "version": "3.0.0", "license": "MIT", @@ -3708,10 +3534,6 @@ "version": "1.0.7", "license": "MIT" }, - "app/node_modules/picocolors": { - "version": "1.1.1", - "license": "ISC" - }, "app/node_modules/picomatch": { "version": "2.3.1", "license": "MIT", @@ -3736,32 +3558,6 @@ "node": ">= 6" } }, - "app/node_modules/postcss": { - "version": "8.5.6", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.11", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, "app/node_modules/postcss-import": { "version": "15.1.0", "license": "MIT", @@ -3886,10 +3682,6 @@ "node": ">=4" } }, - "app/node_modules/postcss-value-parser": { - "version": "4.2.0", - "license": "MIT" - }, "app/node_modules/prelude-ls": { "version": "1.2.1", "dev": true, @@ -3937,16 +3729,6 @@ ], "license": "MIT" }, - "app/node_modules/react": { - "version": "18.3.1", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "app/node_modules/react-day-picker": { "version": "8.10.1", "license": "MIT", @@ -3962,6 +3744,7 @@ "app/node_modules/react-dom": { "version": "18.3.1", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -3970,20 +3753,6 @@ "react": "^18.3.1" } }, - "app/node_modules/react-hook-form": { - "version": "7.71.1", - "license": "MIT", - "engines": { - "node": ">=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/react-hook-form" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17 || ^18 || ^19" - } - }, "app/node_modules/react-is": { "version": "18.3.1", "license": "MIT" @@ -4302,13 +4071,6 @@ "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, - "app/node_modules/source-map-js": { - "version": "1.2.1", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, "app/node_modules/strip-json-comments": { "version": "3.1.1", "dev": true, @@ -4379,6 +4141,7 @@ "app/node_modules/tailwindcss": { "version": "3.4.18", "license": "MIT", + "peer": true, "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", @@ -4482,6 +4245,7 @@ "app/node_modules/tinyglobby/node_modules/picomatch": { "version": "4.0.3", "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -4533,6 +4297,7 @@ "version": "5.9.3", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -4568,35 +4333,6 @@ "dev": true, "license": "MIT" }, - "app/node_modules/update-browserslist-db": { - "version": "1.2.3", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.1" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, "app/node_modules/uri-js": { "version": "4.4.1", "dev": true, @@ -4690,6 +4426,7 @@ "version": "7.3.1", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -4779,6 +4516,7 @@ "version": "4.0.3", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -4826,17 +4564,6 @@ "url": "https://github.com/sponsors/colinhacks" } }, - "node_modules/@falkordb/canvas": { - "version": "0.0.29", - "resolved": "https://registry.npmjs.org/@falkordb/canvas/-/canvas-0.0.29.tgz", - "integrity": "sha512-dDx1H/lOinzQb9rvI5DKDh7+tpwBArkemzZ9iTefH1/iO7BQ0gMr5MsnLxQo0A0Y57RTzKlJRWlpRAwbM4IPag==", - "license": "MIT", - "dependencies": { - "d3": "^7.9.0", - "force-graph": "^1.44.4", - "react": "^19.2.3" - } - }, "node_modules/@playwright/test": { "version": "1.57.0", "dev": true, @@ -4878,11 +4605,31 @@ "node": ">=10" } }, - "node_modules/@tweenjs/tween.js": { - "version": "25.0.0", - "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-25.0.0.tgz", - "integrity": "sha512-XKLA6syeBUaPzx4j3qwMqzzq+V4uo72BnlbOjmuljLrRqdsd3qnzvZZoxvMHZ23ndsRS4aufU6JOZYpCbU6T1A==", - "license": "MIT" + "node_modules/@tanstack/query-core": { + "version": "5.90.20", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.20.tgz", + "integrity": "sha512-OMD2HLpNouXEfZJWcKeVKUgQ5n+n3A2JFmBaScpNDUqSrQSjiveC7dKMe53uJUg1nDG16ttFPz2xfilz6i2uVg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.90.21", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.21.tgz", + "integrity": "sha512-0Lu6y5t+tvlTJMTO7oh5NSpJfpg/5D41LlThfepTixPYkJ0sE2Jj0m0f6yYqujBwIXlId87e234+MxG3D3g7kg==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.90.20" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } }, "node_modules/@types/node": { "version": "22.19.3", @@ -4892,37 +4639,112 @@ "undici-types": "~6.21.0" } }, - "node_modules/accessor-fn": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/accessor-fn/-/accessor-fn-1.5.3.tgz", - "integrity": "sha512-rkAofCwe/FvYFUlMB0v0gWmhqtfAtV1IUkdPbfhTUyYniu5LrC0A0UJkTH0Jv3S8SvwkmfuAlY+mQIJATdocMA==", + "node_modules/autoprefixer": { + "version": "10.4.27", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.27.tgz", + "integrity": "sha512-NP9APE+tO+LuJGn7/9+cohklunJsXWiaWEfV3si4Gi/XHDwVNgkwr1J3RQYFIvPy76GmJ9/bW8vyoU1LcxwKHA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "license": "MIT", + "dependencies": { + "browserslist": "^4.28.1", + "caniuse-lite": "^1.0.30001774", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, "engines": { - "node": ">=12" + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" } }, - "node_modules/bezier-js": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/bezier-js/-/bezier-js-6.1.4.tgz", - "integrity": "sha512-PA0FW9ZpcHbojUCMu28z9Vg/fNkwTj5YhusSAjHHDfHDGLxJ6YUKrAN2vk1fP2MMOxVw4Oko16FMlRGVBGqLKg==", - "license": "MIT", - "funding": { - "type": "individual", - "url": "https://github.com/Pomax/bezierjs/blob/master/FUNDING.md" + "node_modules/baseline-browser-mapping": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz", + "integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" } }, - "node_modules/canvas-color-tracker": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/canvas-color-tracker/-/canvas-color-tracker-1.3.2.tgz", - "integrity": "sha512-ryQkDX26yJ3CXzb3hxUVNlg1NKE4REc5crLBq661Nxzr8TNd236SaEf2ffYLXyI5tSABSeguHLqcVq4vf9L3Zg==", + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "license": "MIT", + "peer": true, "dependencies": { - "tinycolor2": "^1.6.0" + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" }, "engines": { - "node": ">=12" + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/caniuse-lite": { + "version": "1.0.30001775", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001775.tgz", + "integrity": "sha512-s3Qv7Lht9zbVKE9XoTyRG6wVDCKdtOFIjBGg3+Yhn6JaytuNKPIjBMTMIY1AnOH3seL5mvF+x33oGAyK3hVt3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, "node_modules/commander": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", @@ -4994,12 +4816,6 @@ "node": ">=12" } }, - "node_modules/d3-binarytree": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/d3-binarytree/-/d3-binarytree-1.0.2.tgz", - "integrity": "sha512-cElUNH+sHu95L04m92pG73t2MEJXKu+GeKUN1TJkFsu93E5W8E9Sc3kHEGJKgenGvj19m6upSn2EunvMgMD2Yw==", - "license": "MIT" - }, "node_modules/d3-brush": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", @@ -5143,22 +4959,6 @@ "node": ">=12" } }, - "node_modules/d3-force-3d": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/d3-force-3d/-/d3-force-3d-3.0.6.tgz", - "integrity": "sha512-4tsKHUPLOVkyfEffZo1v6sFHvGFwAIIjt/W8IThbp08DYAsXZck+2pSHEG5W1+gQgEvFLdZkYvmJAbRM2EzMnA==", - "license": "MIT", - "dependencies": { - "d3-binarytree": "1", - "d3-dispatch": "1 - 3", - "d3-octree": "1", - "d3-quadtree": "1 - 3", - "d3-timer": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/d3-format": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", @@ -5201,12 +5001,6 @@ "node": ">=12" } }, - "node_modules/d3-octree": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/d3-octree/-/d3-octree-1.1.0.tgz", - "integrity": "sha512-F8gPlqpP+HwRPMO/8uOu5wjH110+6q4cgJvgJT6vlpy3BEaDIKlTZrgHKZSp/i1InRpVfh4puY/kvL6MxK930A==", - "license": "MIT" - }, "node_modules/d3-path": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", @@ -5277,6 +5071,7 @@ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", "license": "ISC", + "peer": true, "engines": { "node": ">=12" } @@ -5370,44 +5165,35 @@ "robust-predicates": "^3.0.2" } }, - "node_modules/float-tooltip": { - "version": "1.7.5", - "resolved": "https://registry.npmjs.org/float-tooltip/-/float-tooltip-1.7.5.tgz", - "integrity": "sha512-/kXzuDnnBqyyWyhDMH7+PfP8J/oXiAavGzcRxASOMRHFuReDtofizLLJsf7nnDLAfEaMW4pVWaXrAjtnglpEkg==", + "node_modules/electron-to-chromium": { + "version": "1.5.302", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.302.tgz", + "integrity": "sha512-sM6HAN2LyK82IyPBpznDRqlTQAtuSaO+ShzFiWTvoMJLHyZ+Y39r8VMfHzwbU8MVBzQ4Wdn85+wlZl2TLGIlwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, "license": "MIT", - "dependencies": { - "d3-selection": "2 - 3", - "kapsule": "^1.16", - "preact": "10" - }, "engines": { - "node": ">=12" + "node": ">=6" } }, - "node_modules/force-graph": { - "version": "1.51.0", - "resolved": "https://registry.npmjs.org/force-graph/-/force-graph-1.51.0.tgz", - "integrity": "sha512-aTnihCmiMA0ItLJLCbrQYS9mzriopW24goFPgUnKAAmAlPogTSmFWqoBPMXzIfPb7bs04Hur5zEI4WYgLW3Sig==", + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, "license": "MIT", - "dependencies": { - "@tweenjs/tween.js": "18 - 25", - "accessor-fn": "1", - "bezier-js": "3 - 6", - "canvas-color-tracker": "^1.3", - "d3-array": "1 - 3", - "d3-drag": "2 - 3", - "d3-force-3d": "2 - 3", - "d3-scale": "1 - 4", - "d3-scale-chromatic": "1 - 3", - "d3-selection": "2 - 3", - "d3-zoom": "2 - 3", - "float-tooltip": "^1.7", - "index-array-by": "1", - "kapsule": "^1.16", - "lodash-es": "4" - }, "engines": { - "node": ">=12" + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" } }, "node_modules/fsevents": { @@ -5425,6 +5211,19 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/globals": { + "version": "17.3.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.3.0.tgz", + "integrity": "sha512-yMqGUQVVCkD4tqjOJf3TnrvaaHDMYp4VlUSObbkIiuCPe/ofdMBFIAcBbCSRFWOnos6qRiTVStDwqPLUclaxIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", @@ -5437,15 +5236,6 @@ "node": ">=0.10.0" } }, - "node_modules/index-array-by": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/index-array-by/-/index-array-by-1.4.2.tgz", - "integrity": "sha512-SP23P27OUKzXWEC/TOyWlwLviofQkCSCKONnc62eItjp69yCZZPqDQtr3Pw5gJDnPeUMqExmKydNZaJO0FU9pw==", - "license": "MIT", - "engines": { - "node": ">=12" - } - }, "node_modules/internmap": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", @@ -5455,24 +5245,55 @@ "node": ">=12" } }, - "node_modules/kapsule": { - "version": "1.16.3", - "resolved": "https://registry.npmjs.org/kapsule/-/kapsule-1.16.3.tgz", - "integrity": "sha512-4+5mNNf4vZDSwPhKprKwz3330iisPrb08JyMgbsdFrimBCKNHecua/WBwvVg3n7vwx0C1ARjfhwIpbrbd9n5wg==", + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", "license": "MIT", "dependencies": { - "lodash-es": "4" + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" }, "engines": { - "node": ">=12" + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, - "node_modules/lodash-es": { - "version": "4.17.23", - "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.23.tgz", - "integrity": "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==", + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, "license": "MIT" }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, "node_modules/playwright": { "version": "1.57.0", "dev": true, @@ -5501,10 +5322,45 @@ "node": ">=18" } }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "license": "MIT" + }, "node_modules/preact": { - "version": "10.28.3", - "resolved": "https://registry.npmjs.org/preact/-/preact-10.28.3.tgz", - "integrity": "sha512-tCmoRkPQLpBeWzpmbhryairGnhW9tKV6c6gr/w+RhoRoKEJwsjzipwp//1oCpGPOchvSLaAPlpcJi9MwMmoPyA==", + "version": "10.28.4", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.28.4.tgz", + "integrity": "sha512-uKFfOHWuSNpRFVTnljsCluEFq57OKT+0QdOiQo8XWnQ/pSvg7OpX5eNOejELXJMWy+BwM2nobz0FkvzmnpCNsQ==", "license": "MIT", "funding": { "type": "opencollective", @@ -5516,14 +5372,35 @@ "link": true }, "node_modules/react": { - "version": "19.2.4", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", - "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0" + }, "engines": { "node": ">=0.10.0" } }, + "node_modules/react-hook-form": { + "version": "7.71.2", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.71.2.tgz", + "integrity": "sha512-1CHvcDYzuRUNOflt4MOq3ZM46AronNJtQ1S7tnX6YN4y72qhgiUItpacZUAQ0TyWYci3yz1X+rXaSxiuEm86PA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19" + } + }, "node_modules/robust-predicates": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", @@ -5542,6 +5419,15 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/tailwindcss": { "version": "4.1.17", "dev": true, @@ -5556,16 +5442,42 @@ "tailwindcss": ">=3.0.0 || insiders" } }, - "node_modules/tinycolor2": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz", - "integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==", - "license": "MIT" - }, "node_modules/undici-types": { "version": "6.21.0", "dev": true, "license": "MIT" - } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "packages/canvas": {} } } diff --git a/playwright.config.ts b/playwright.config.ts index 7f9f78fa..b48465c0 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -19,8 +19,8 @@ export default defineConfig({ forbidOnly: !!process.env.CI, /* Retry on CI only */ retries: process.env.CI ? 2 : 0, - /* Opt out of parallel tests on CI. */ - workers: process.env.CI ? 1 : undefined, + /* Pin to 4 workers on CI (matches ubuntu-latest vCPU count) */ + workers: process.env.CI ? 4 : undefined, /* Reporter to use. See https://playwright.dev/docs/test-reporters */ reporter: 'html', /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ @@ -53,15 +53,15 @@ export default defineConfig({ dependencies: ['setup'], }, - { + // Firefox is only run locally; skipped in CI to halve test time + ...(!process.env.CI ? [{ name: 'firefox', use: { ...devices['Desktop Firefox'], - // Use saved authentication state storageState: 'e2e/.auth/user.json', }, dependencies: ['setup'], - }, + }] : []), // { // name: 'webkit', diff --git a/tests/test_csrf_middleware.py b/tests/test_csrf_middleware.py new file mode 100644 index 00000000..11887e2e --- /dev/null +++ b/tests/test_csrf_middleware.py @@ -0,0 +1,170 @@ +""" +Tests for CSRF middleware (double-submit cookie pattern). +""" +import pytest +from fastapi.testclient import TestClient +from api.index import app + + +class TestCSRFMiddleware: + """Test CSRF double-submit cookie middleware.""" + + @pytest.fixture + def client(self): + """Create a test client.""" + return TestClient(app) + + # ---- Cookie provisioning ---- + + def test_get_sets_csrf_cookie(self, client): + """GET responses should include a csrf_token cookie.""" + response = client.get("/auth-status") + assert response.status_code == 200 + assert "csrf_token" in response.cookies + + def test_csrf_cookie_is_not_httponly(self, client): + """The CSRF cookie must be readable by JavaScript (not HttpOnly).""" + response = client.get("/auth-status") + # TestClient exposes raw Set-Cookie headers + set_cookie = response.headers.get("set-cookie", "") + assert "csrf_token=" in set_cookie + # httponly flag must NOT be present + assert "httponly" not in set_cookie.lower() + + # ---- Safe methods bypass ---- + + def test_get_does_not_require_csrf(self, client): + """GET requests must not be blocked by CSRF checks.""" + response = client.get("/auth-status") + assert response.status_code != 403 + + def test_head_does_not_require_csrf(self, client): + """HEAD requests must not be blocked by CSRF checks.""" + response = client.head("/auth-status") + assert response.status_code != 403 + + def test_options_does_not_require_csrf(self, client): + """OPTIONS requests must not be blocked by CSRF checks.""" + response = client.options("/auth-status") + assert response.status_code != 403 + + # ---- Blocking without token ---- + + def test_post_without_csrf_is_blocked(self, client): + """POST without CSRF header/cookie must return 403.""" + response = client.post("/logout") + assert response.status_code == 403 + assert response.json()["detail"] == "CSRF token missing or invalid" + + def test_post_with_wrong_csrf_is_blocked(self, client): + """POST with mismatched CSRF tokens must return 403.""" + response = client.post( + "/logout", + headers={"X-CSRF-Token": "wrong-value"}, + cookies={"csrf_token": "correct-value"}, + ) + assert response.status_code == 403 + + def test_csrf_rejection_sets_cookie(self, client): + """A 403 CSRF rejection should still set the csrf_token cookie.""" + response = client.post("/logout") + assert response.status_code == 403 + assert "csrf_token" in response.cookies + + # ---- Allowing with valid token ---- + + def test_post_with_valid_csrf_passes(self, client): + """POST with matching cookie + header must pass CSRF check.""" + # First get a token + get_resp = client.get("/auth-status") + csrf = get_resp.cookies["csrf_token"] + + response = client.post( + "/logout", + headers={"X-CSRF-Token": csrf}, + cookies={"csrf_token": csrf}, + ) + # Should not be 403 (will be 200 for logout) + assert response.status_code != 403 + + # ---- Bearer token bypass ---- + + def test_bearer_auth_bypasses_csrf(self, client): + """Requests with Bearer auth should skip CSRF (get 401, not 403).""" + response = client.post( + "/tokens/generate", + headers={"Authorization": "Bearer fake_token_value"}, + ) + # 401 = auth failed (expected), NOT 403 = CSRF blocked + assert response.status_code == 401 + + # ---- Exempt paths ---- + + def test_login_path_exempt(self, client): + """Login endpoints should be exempt from CSRF checks.""" + response = client.post( + "/login/email", + json={"email": "test@test.com", "password": "password"}, + ) + # Should NOT be 403 CSRF error + assert response.json().get("detail") != "CSRF token missing or invalid" + + def test_signup_path_exempt(self, client): + """Signup endpoints should be exempt from CSRF checks.""" + response = client.post( + "/signup/email", + json={ + "firstName": "Test", + "lastName": "User", + "email": "test@test.com", + "password": "password123", + }, + ) + assert response.json().get("detail") != "CSRF token missing or invalid" + + # ---- DELETE and PATCH methods ---- + + def test_delete_without_csrf_is_blocked(self, client): + """DELETE without CSRF header/cookie must return 403.""" + response = client.delete("/graphs/test-graph") + assert response.status_code == 403 + assert response.json()["detail"] == "CSRF token missing or invalid" + + def test_delete_with_valid_csrf_passes(self, client): + """DELETE with matching cookie + header must pass CSRF check.""" + get_resp = client.get("/auth-status") + csrf = get_resp.cookies["csrf_token"] + + response = client.delete( + "/graphs/test-graph", + headers={"X-CSRF-Token": csrf}, + cookies={"csrf_token": csrf}, + ) + assert response.status_code != 403 + + def test_patch_without_csrf_is_blocked(self, client): + """PATCH without CSRF header/cookie must return 403.""" + response = client.patch("/graphs/test-graph") + assert response.status_code == 403 + assert response.json()["detail"] == "CSRF token missing or invalid" + + def test_patch_with_valid_csrf_passes(self, client): + """PATCH with matching cookie + header must pass CSRF check.""" + get_resp = client.get("/auth-status") + csrf = get_resp.cookies["csrf_token"] + + response = client.patch( + "/graphs/test-graph", + headers={"X-CSRF-Token": csrf}, + cookies={"csrf_token": csrf}, + ) + assert response.status_code != 403 + + # ---- Secure flag based on scheme ---- + + def test_csrf_cookie_not_secure_on_http(self, client): + """Over plain HTTP the csrf_token cookie must NOT have the Secure flag.""" + response = client.get("/auth-status") + set_cookie = response.headers.get("set-cookie", "") + assert "csrf_token=" in set_cookie + assert "; secure" not in set_cookie.lower()